Compare commits

...

7 Commits

Author SHA1 Message Date
Luke Gustafson 10794f1e8d v2.2.1 (#754)
* fix: rdp, desktop app, and mobile app login issues and guacd issues

* fix: general fixes

* fix: mobile/desktop login isssues

* fix: mobile/desktop login isssues

* fix: mobile/desktop login isssues

* chore: format
2026-05-12 21:55:14 -05:00
LukeGus ada8a268bb fix: desktop app not launching 2026-05-06 22:02:11 -05:00
LukeGus 0d8c82d7f8 chore: update cask 2026-05-06 15:40:30 -05:00
Luke Gustafson f09f1748ec New Crowdin updates (#740)
* New translations en.json (Romanian)

[ci skip]

* New translations en.json (French)

[ci skip]

* New translations en.json (Spanish)

[ci skip]

* New translations en.json (Afrikaans)

[ci skip]

* New translations en.json (Arabic)

[ci skip]

* New translations en.json (Bulgarian)

[ci skip]

* New translations en.json (Catalan)

[ci skip]

* New translations en.json (Czech)

[ci skip]

* New translations en.json (Danish)

[ci skip]

* New translations en.json (German)

[ci skip]

* New translations en.json (Greek)

[ci skip]

* New translations en.json (Finnish)

[ci skip]

* New translations en.json (Hebrew)

[ci skip]

* New translations en.json (Hungarian)

[ci skip]

* New translations en.json (Italian)

[ci skip]

* New translations en.json (Japanese)

[ci skip]

* New translations en.json (Korean)

[ci skip]

* New translations en.json (Dutch)

[ci skip]

* New translations en.json (Norwegian)

[ci skip]

* New translations en.json (Polish)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations en.json (Russian)

[ci skip]

* New translations en.json (Serbian (Cyrillic))

[ci skip]

* New translations en.json (Swedish)

[ci skip]

* New translations en.json (Turkish)

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Chinese Simplified)

[ci skip]

* New translations en.json (Chinese Traditional)

[ci skip]

* New translations en.json (Vietnamese)

[ci skip]

* New translations en.json (Portuguese, Brazilian)

[ci skip]

* New translations en.json (Indonesian)

[ci skip]

* New translations en.json (Bengali)

[ci skip]

* New translations en.json (Thai)

[ci skip]

* New translations en.json (Hindi)

[ci skip]
2026-05-06 15:18:55 -05:00
Luke Gustafson 2768f11dfc v2.2.0 (#738)
* Improve Docker container list UI

* Rework SSH tunnel forwarding

* Update macOS Electron packaging

* Optimize frontend bundle splitting

* Add beta version update status

* Add client tunnel preset management

* Secure cookie authentication flows

* Add client tunnel bridge support

* Preserve sessions on restart

* Update runtime to Node 24

* Add client remote tunnel support

* Fix stale frontend cache handling

* Fix Docker image platforms for Node 24

* Fix Electron packaging workflows

* Fix client auth cache after upgrades

* chore: cleanup files

* fix: npm i error

* Fix OIDC auth cookie readiness

* Fix Docker npm ci config

* Add react-is peer dependency

* Fix Electron auth and cache handling

* Improve terminal clipboard and refresh actions

* feat: add API keys

* feat: improve lazy loading with loading spinners

* feat: Introduce FolderTree component with lazy-loading and motion animations for improved file manager UX (#735)

* feat: integrate FolderTree component with lazy-loading for file manager sidebar

- Add motion animation library (v12.38.0) for smooth UI transitions
- Create new FolderTree component with advanced keyboard navigation support
- Refactor kbd component: introduce KbdKey and KbdSeparator subcomponents
- Implement lazy-loading strategy for directory tree in FileManagerSidebar
- Refactor FileManagerSidebar with improved code organization and better separation of concerns
- Update keyboard shortcut displays across CommandPalette, FileViewer, and Dashboard
- Change React/ReactDOM dependency flags from dev to devOptional in package-lock.json

BREAKING CHANGE: KbdGroup component has been replaced. Use <Kbd><KbdKey>...</KbdKey><KbdSeparator /></Kbd> instead.

- Improves UX with smooth animations and better folder navigation
- Reduces initial load time through lazy-loading subdirectories
- Enhances accessibility with ARIA labels and keyboard navigation
- Maintains dark mode support and proper styling

* fix: incorrect use of the theme system and linked file manger sidebar with current folder

---------

Co-authored-by: suryacagur <suryacagur.dev@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* Enhance VNC token generation to include optional username parameter and refactor username input handling in HostGeneralTab (#733)

* Fix Docker build info generation

* Remove unused node-fetch dependency

* feat: prompt user for SSH key passphrase on use (#715)

When an encrypted SSH key has no stored passphrase, show a lightweight
dialog prompting the user to enter it at connection time instead of
failing with a parse error. Supports both desktop and mobile terminals.

Closes Termix-SSH/Support#354

* fix: prevent session crash when uploading to permission-denied directory (#716)

- Wrap writeFile sftp.stat callback in try-catch to prevent uncaught
  exceptions from escaping the callback into the event loop
- Add missing stream.stderr error handler in writeFile fallback to
  prevent unhandled error events from crashing the process
- Remove bogus activeOperations decrement in both writeFile and
  uploadFile fallback methods (counter was never incremented)
- Add res.headersSent checks in fallback disconnect paths to prevent
  ERR_HTTP_HEADERS_SENT crashes

Closes Termix-SSH/Support#652

* feat: add LOG_TIMESTAMP_FORMAT env var for 24h/ISO log timestamps (#718)

Support LOG_TIMESTAMP_FORMAT environment variable with values:
- "24h": 24-hour format (14:58:45)
- "iso": ISO 8601 format (2026-04-25T14:58:45.000Z)
- default: locale format (2:58:45 PM)

Closes Termix-SSH/Support#650

* feat: open file manager at terminal current working directory (#719)

* feat: open file manager at terminal current working directory

When right-clicking in the terminal and selecting "Open File Manager
Here", query the current working directory via a separate SSH exec
channel and pass it as the initial path to the file manager tab.

Closes Termix-SSH/Support#649

* chore: sync package-lock.json with node-fetch and deps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove undefined TerminalContextMenu from bad merge resolution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* fix: show reconnect overlay when SSH server reboots (#720)

When the remote server reboots, the SSH connection closes while the
stream is still active. The close handler only sent the "disconnected"
message when sshStream was null, so the frontend never received the
disconnect notification and hung with a blinking cursor.

Change the else-if condition to always send the "disconnected" message
regardless of stream state.

Closes Termix-SSH/Support#648

* feat: support read-only Docker container mode (#721)

Move nginx runtime files (config, pid, logs, temp dirs) from /app/nginx/
to /tmp/nginx/ so the container can run with read_only: true. Template
files remain in /app/nginx/ as read-only assets.

Users can now harden the container with:
  read_only: true
  tmpfs:
    - /tmp

Closes Termix-SSH/Support#647

* fix: allow editing host folder without re-entering password (#722)

When editing an existing host, the password field is stripped by the
backend for security. The form validation treated the empty password
as invalid, disabling the Update Host button even for non-auth changes
like folder assignment.

Use an "existing_password" sentinel (mirroring the existing
"existing_key" pattern) to represent an unchanged password during
editing, skip validation for it, and omit it from the update payload.

Closes Termix-SSH/Support#645

* fix: auto-close tab on graceful SSH disconnect (exit/Ctrl+D) (#723)

Distinguish between graceful shell exit and unexpected disconnection
using the stream close event's exit code. When the shell exits normally
(code != null), send "session_ended" instead of "disconnected". The
frontend auto-closes the tab on session_ended, and shows the reconnect
overlay only on unexpected disconnections.

Closes Termix-SSH/Support#643

* fix: reattach existing SSH session on WebSocket reconnect (#724)

WebSocket reconnection was always creating a new SSH connection with
full authentication instead of reattaching to the existing SSH session.
The condition `!isReconnectingRef.current` prevented session reattach
during reconnection, causing repeated password auth attempts that
trigger SSHGuard/fail2ban blocking.

Remove the guard so reconnection tries to reattach the persisted
session first. If the session has expired, the backend sends
sessionExpired and the frontend falls back to a new connection.

Closes Termix-SSH/Support#644

* fix: prevent browser crash when uploading large files (>100MB) (#725)

The file-to-base64 conversion used a byte-by-byte string concatenation
loop (String.fromCharCode + btoa), which allocated ~3x the file size
in intermediate strings, causing the browser tab to OOM on files over
~100MB.

Replace with FileReader.readAsDataURL which delegates base64 encoding
to the browser engine natively, avoiding the intermediate allocations.

Closes Termix-SSH/Support#577

* fix: support SSH multi-factor auth with publickey + password (#726)

When sshd requires AuthenticationMethods publickey,password, the
connection failed because the key auth branch only set privateKey
without also setting password. After publickey partial auth succeeded,
ssh2 sent keyboard-interactive (due to tryKeyboard:true) instead of
password, which the server rejected.

Pass the credential password alongside the private key so ssh2 can
complete the password step after publickey succeeds.

Closes Termix-SSH/Support#629

* feat(oidc): add OIDC_ALLOW_REGISTRATION env to bypass allow_registration for OIDC (#727)

The `allow_registration` setting blocks both password-based and OIDC user
creation. Admins who want to close password registration but still onboard
new users via a trusted IdP (with the existing `OIDC_ALLOWED_USERS` whitelist)
have no way to do that today.

Introduce an `OIDC_ALLOW_REGISTRATION` env var. When set to `true`, the OIDC
callback skips the `allow_registration` settings check while still honoring
the `OIDC_ALLOWED_USERS` whitelist. Password registration via `POST
/users/create` continues to respect `allow_registration`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf: lazy load locales, file previews, and decouple startup imports (#729)

* perf: lazy load locale bundles

* perf: lazy load file preview modules

* perf: avoid eager api client load on startup

* chore: remove dead code, tighten types, fix lint warnings (#730)

* chore: clean up low-risk lint warnings

* chore: tighten utility types

* chore: preserve backend error causes

* chore: simplify command palette host state

* chore: remove unused frontend code

* chore: prune stale frontend state

* chore: trim unused navigation code

* chore: prune unused user settings props

* chore: trim unused sidebar state

* chore: remove stale host editor imports

* chore: tighten shared frontend types

* chore: narrow desktop helper types

* chore: type network topology data

* chore: type connection log errors

* chore: use typed tab context

* chore: type api client error metadata

* chore: tighten terminal config types

* chore: type host proxy chains

* chore: type host editor form data

* chore: use typed host viewer fields

* chore: format app builder patch script

* Fix client auth cache after upgrades

* chore: fix pr checks after dev merge

* fix: remove duplicate session-expired useEffect in FullScreenAppWrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Xenthys <x@dis.gg>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: npm package warnings

* feat: reconnect after file manager disconnects

* feat: add docs button in api keys

* feat: change colors for server tunnels

* fix: fetch password from API for Copy Password button (#736)

* chore: update readme's

* feat: improve c2s UI in user profile

* feat: improve ssh key detection and move open file manager at path for terminal button

* fix: restore missing getHostPassword import in Tab.tsx (#737)

* fix: security related fixes

* feat: improve alert code

* Fix Electron clipboard handling

* fix: untranslated alert text

---------

Co-authored-by: Xenthys <x@dis.gg>
Co-authored-by: PT Kelana Tech Solutions <ptkelanatechsolutions@gmail.com>
Co-authored-by: suryacagur <suryacagur.dev@gmail.com>
Co-authored-by: zimmra <28514085+zimmra@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fuad <funtik1229@yandex.ru>
2026-05-06 15:12:07 -05:00
Luke Gustafson af9fc95b0e Update README.md 2026-05-01 15:03:45 -05:00
LukeGus 18633c5760 chore: update cask 2026-04-22 17:47:56 -05:00
216 changed files with 22522 additions and 13502 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,json,css,scss,md,yml,yaml}]
[*.{js,cjs,mjs,jsx,ts,tsx,json,css,scss,md,yml,yaml}]
indent_style = space
indent_size = 2
+3
View File
@@ -1,6 +1,8 @@
* text=auto eol=lf
*.js text eol=lf
*.cjs text eol=lf
*.mjs text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
@@ -29,3 +31,4 @@
*.woff2 binary
*.ttf binary
*.eot binary
*.icns binary
+7 -7
View File
@@ -25,12 +25,12 @@ jobs:
fetch-depth: 1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Determine tags
id: tags
@@ -57,7 +57,7 @@ jobs:
echo "ALL_TAGS=$(IFS=,; echo "${ALL_TAGS[*]}")" >> $GITHUB_ENV
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: lukegus
@@ -65,18 +65,18 @@ jobs:
- name: Login to Docker Hub (prod only)
if: ${{ github.event.inputs.build_type == 'Production' }}
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: bugattiguy527
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push multi-arch image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
with:
context: .
file: ./docker/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
tags: ${{ env.ALL_TAGS }}
build-args: |
BUILDKIT_INLINE_CACHE=1
+4 -4
View File
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install dependencies
@@ -145,7 +145,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install system dependencies for AppImage
@@ -368,7 +368,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install dependencies
@@ -895,7 +895,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install dependencies
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install dependencies
+2 -4
View File
@@ -18,12 +18,10 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
- name: Install dependencies
run: |
rm -rf node_modules package-lock.json
npm install
run: npm ci
- name: Run ESLint
run: npx eslint .
+6 -5
View File
@@ -11,8 +11,7 @@ dist
dist-ssr
*.local
.vscode/*
!.vscode/extensions.json
.vscode/
.idea
.DS_Store
*.suo
@@ -20,12 +19,14 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/db/
/release/
/.claude/
/ssl/
.env
/.mcp.json
/uploads/
/nul
/.vscode/
.env
electron/build-info.cjs
/.mcp.json
/CLAUDE.md
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+1 -1
View File
@@ -1 +1 @@
20
24
+2 -2
View File
@@ -1,6 +1,6 @@
cask "termix" do
version "2.0.0"
sha256 "2e381ac504093bfa72d16ee20043640724394729b2ca0e6b26c9eac19aeb6d08"
version "2.2.0"
sha256 "62030994ecd8de264f7edae965caf57fabb9f9014007063a35db8cab4c6e818c"
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
name "Termix"
+3 -2
View File
@@ -6,7 +6,7 @@
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release&v=1)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
@@ -37,7 +37,7 @@ free and self-hosted alternative to Termius available for all platforms.
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components.
- **Remote Desktop Access** - RDP, VNC, and Telnet support over the browser with complete customization and split screening
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring and support for -l or -r connections
- **SSH Tunnel Management** - Create and manage server-to-server SSH tunnels with automatic reconnection, health monitoring, and local, remote, or dynamic SOCKS forwarding. Desktop client-to-server tunnel settings are stored locally per desktop install, optional C2S preset snapshots can be saved to the server, renamed, loaded, or deleted when you want to move a local tunnel configuration between clients.
- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly with sudo support.
- **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them.
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders, and easily save reusable login info while being able to automate the deployment of SSH keys
@@ -46,6 +46,7 @@ free and self-hosted alternative to Termius available for all platforms.
- **RBAC** - Create roles and share hosts across users/roles
- **User Authentication** - Secure user management with admin controls and OIDC (with access control) and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together.
- **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more.
- **API Keys** - Create user-scoped API keys with expiration dates to be used for automation/CI
- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen.
+14 -12
View File
@@ -1,12 +1,14 @@
# Stage 1: Install dependencies
FROM node:22-slim AS deps
FROM node:24-slim AS deps
WORKDIR /app
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
COPY .npmrc ./
COPY vendor ./vendor
RUN npm ci --ignore-scripts --force && \
RUN npm ci --ignore-scripts && \
npm cache clean --force
# Stage 2: Build frontend
@@ -26,24 +28,26 @@ WORKDIR /app
COPY . .
RUN npm rebuild better-sqlite3 --force
RUN npm rebuild better-sqlite3
RUN npm run build:backend
# Stage 4: Production dependencies only
FROM node:22-slim AS production-deps
FROM node:24-slim AS production-deps
WORKDIR /app
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
COPY .npmrc ./
COPY vendor ./vendor
RUN npm ci --only=production --ignore-scripts --force && \
npm rebuild better-sqlite3 bcryptjs --force && \
RUN npm ci --omit=dev --ignore-scripts && \
npm rebuild better-sqlite3 bcryptjs && \
npm cache clean --force
# Stage 5: Final optimized image
FROM node:22-slim
FROM node:24-slim
WORKDIR /app
ENV DATA_DIR=/app/data \
@@ -53,11 +57,9 @@ ENV DATA_DIR=/app/data \
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu wget && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
chown -R node:node /app && \
chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx && \
touch /app/nginx/nginx.conf && \
chown node:node /app/nginx/nginx.conf
mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx && \
chown -R node:node /app /tmp/nginx && \
chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx
COPY docker/nginx.conf /app/nginx/nginx.conf.template
COPY docker/nginx-https.conf /app/nginx/nginx-https.conf.template
+5 -4
View File
@@ -7,14 +7,14 @@ PGID=${PGID:-1000}
if [ "$(id -u)" = "0" ]; then
if [ "$PUID" = "0" ]; then
echo "Running as root (PUID=0, PGID=$PGID)"
chown -R root:root /app/data /app/uploads /app/nginx 2>/dev/null || true
chown -R root:root /app/data /app/uploads /tmp/nginx 2>/dev/null || true
else
echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."
groupmod -o -g "$PGID" node 2>/dev/null || true
usermod -o -u "$PUID" node 2>/dev/null || true
chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true
chown -R node:node /app/data /app/uploads /tmp/nginx 2>/dev/null || true
echo "User node is now UID: $PUID, GID: $PGID"
@@ -38,7 +38,8 @@ else
NGINX_CONF_SOURCE="/app/nginx/nginx.conf.template"
fi
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf
mkdir -p /tmp/nginx
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf
mkdir -p /app/data /app/uploads /app/data/.opk
chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true
@@ -132,7 +133,7 @@ EOF
fi
echo "Starting nginx..."
nginx -c /app/nginx/nginx.conf
nginx -c /tmp/nginx/nginx.conf
echo "Starting backend services..."
cd /app
+58 -9
View File
@@ -1,7 +1,7 @@
worker_processes 1;
master_process off;
pid /app/nginx/nginx.pid;
error_log /app/nginx/logs/error.log warn;
pid /tmp/nginx/nginx.pid;
error_log /tmp/nginx/error.log warn;
events {
worker_connections 1024;
@@ -11,13 +11,13 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /app/nginx/logs/access.log;
access_log /tmp/nginx/access.log;
client_body_temp_path /app/nginx/client_body;
proxy_temp_path /app/nginx/proxy_temp;
fastcgi_temp_path /app/nginx/fastcgi_temp;
uwsgi_temp_path /app/nginx/uwsgi_temp;
scgi_temp_path /app/nginx/scgi_temp;
client_body_temp_path /tmp/nginx/client_body;
proxy_temp_path /tmp/nginx/proxy_temp;
fastcgi_temp_path /tmp/nginx/fastcgi_temp;
uwsgi_temp_path /tmp/nginx/uwsgi_temp;
scgi_temp_path /tmp/nginx/scgi_temp;
sendfile on;
keepalive_timeout 65;
@@ -65,16 +65,32 @@ http {
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
location = /sw.js {
root /app/html;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location = /manifest.json {
root /app/html;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /app/html;
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
location / {
root /app/html;
index index.html index.htm;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri $uri/ /index.html;
}
@@ -162,6 +178,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/c2s-tunnel-presets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $http_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 ~ ^/terminal(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -337,6 +362,21 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_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_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_buffering off;
proxy_cache off;
}
location /host/file_manager/recent {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -455,6 +495,15 @@ http {
proxy_read_timeout 60s;
}
location ~ ^/(refresh|host-updated)$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $http_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 ~ ^/global-settings(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
+58 -9
View File
@@ -1,7 +1,7 @@
worker_processes 1;
master_process off;
pid /app/nginx/nginx.pid;
error_log /app/nginx/logs/error.log warn;
pid /tmp/nginx/nginx.pid;
error_log /tmp/nginx/error.log warn;
events {
worker_connections 1024;
@@ -11,13 +11,13 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /app/nginx/logs/access.log;
access_log /tmp/nginx/access.log;
client_body_temp_path /app/nginx/client_body;
proxy_temp_path /app/nginx/proxy_temp;
fastcgi_temp_path /app/nginx/fastcgi_temp;
uwsgi_temp_path /app/nginx/uwsgi_temp;
scgi_temp_path /app/nginx/scgi_temp;
client_body_temp_path /tmp/nginx/client_body;
proxy_temp_path /tmp/nginx/proxy_temp;
fastcgi_temp_path /tmp/nginx/fastcgi_temp;
uwsgi_temp_path /tmp/nginx/uwsgi_temp;
scgi_temp_path /tmp/nginx/scgi_temp;
sendfile on;
keepalive_timeout 65;
@@ -54,16 +54,32 @@ http {
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
location = /sw.js {
root /app/html;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location = /manifest.json {
root /app/html;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /app/html;
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
location / {
root /app/html;
index index.html index.htm;
expires off;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri $uri/ /index.html;
}
@@ -151,6 +167,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/c2s-tunnel-presets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $http_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 ~ ^/terminal(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -326,6 +351,21 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_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_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_buffering off;
proxy_cache off;
}
location /host/file_manager/recent {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -444,6 +484,15 @@ http {
proxy_read_timeout 60s;
}
location ~ ^/(refresh|host-updated)$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $http_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 ~ ^/global-settings(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
+4 -4
View File
@@ -5,7 +5,8 @@
"directories": {
"output": "release"
},
"asar": false,
"asar": true,
"asarUnpack": ["dist/backend/**/*", "node_modules/**/*"],
"files": [
"dist/**/*",
"electron/**/*",
@@ -116,8 +117,8 @@
"type": "distribution",
"minimumSystemVersion": "10.15",
"mergeASARs": false,
"singleArchFiles": "**/*.node",
"x64ArchFiles": "**/*.node"
"singleArchFiles": "**/*.{node,bare}",
"x64ArchFiles": "**/*.{node,bare}"
},
"dmg": {
"artifactName": "termix_macos_${arch}_dmg.${ext}",
@@ -130,7 +131,6 @@
"entitlementsInherit": "build/entitlements.mas.inherit.plist",
"hardenedRuntime": false,
"gatekeeperAssess": false,
"asar": false,
"type": "distribution",
"category": "public.app-category.developer-tools",
"artifactName": "termix_macos_${arch}_mas.${ext}",
+1664 -61
View File
File diff suppressed because it is too large Load Diff
+33 -3
View File
@@ -1,5 +1,4 @@
const { contextBridge, ipcRenderer } = require("electron");
const { clipboard } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
@@ -10,15 +9,46 @@ contextBridge.exposeInMainWorld("electronAPI", {
getSetting: (key) => ipcRenderer.invoke("get-setting", key),
setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value),
getC2STunnelConfig: () => ipcRenderer.invoke("get-c2s-tunnel-config"),
saveC2STunnelConfig: (config) =>
ipcRenderer.invoke("save-c2s-tunnel-config", config),
checkLocalPortAvailable: (host, port) =>
ipcRenderer.invoke("check-local-port-available", host, port),
getC2STunnelPresetDefaultName: () =>
ipcRenderer.invoke("get-c2s-tunnel-preset-default-name"),
startC2STunnel: (tunnel, index) =>
ipcRenderer.invoke("start-c2s-tunnel", tunnel, index),
testC2STunnel: (tunnel, index) =>
ipcRenderer.invoke("test-c2s-tunnel", tunnel, index),
stopC2STunnel: (tunnelName) =>
ipcRenderer.invoke("stop-c2s-tunnel", tunnelName),
getC2STunnelStatuses: () => ipcRenderer.invoke("get-c2s-tunnel-statuses"),
onC2STunnelStatuses: (callback) => {
const listener = (_event, statuses) => callback(statuses);
ipcRenderer.on("c2s-tunnel-statuses", listener);
return () => ipcRenderer.removeListener("c2s-tunnel-statuses", listener);
},
startC2SAutoStartTunnels: () =>
ipcRenderer.invoke("start-c2s-autostart-tunnels"),
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
getSessionCookie: (name, targetUrl) =>
ipcRenderer.invoke("get-session-cookie", name, targetUrl),
waitForSessionCookie: (name, targetUrl, previousValue, timeoutMs) =>
ipcRenderer.invoke(
"wait-session-cookie",
name,
targetUrl,
previousValue,
timeoutMs,
),
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
});
contextBridge.exposeInMainWorld("electronClipboard", {
writeText: (text) => clipboard.writeText(text),
readText: () => clipboard.readText(),
writeText: (text) => ipcRenderer.invoke("clipboard-write-text", text),
readText: () => ipcRenderer.invoke("clipboard-read-text"),
});
window.IS_ELECTRON = true;
+20 -2
View File
@@ -2,6 +2,7 @@ import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";
@@ -12,19 +13,36 @@ export default tseslint.config([
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
],
plugins: {
"react-hooks": reactHooks,
"unused-imports": unusedImports,
},
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
"@typescript-eslint/no-unused-vars": "warn",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-expressions": "warn",
"no-empty": "warn",
"no-control-regex": "off",
"no-useless-assignment": "off",
"preserve-caught-error": "off",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error",
"react-refresh/only-export-components": "warn",
},
},
+2701 -6012
View File
File diff suppressed because it is too large Load Diff
+112 -94
View File
@@ -1,15 +1,20 @@
{
"name": "termix",
"private": true,
"version": "2.1.0",
"version": "2.2.1",
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa",
"main": "electron/main.cjs",
"type": "module",
"engines": {
"node": ">=22.12.0",
"npm": ">=11"
},
"scripts": {
"clean": "npx prettier . --write",
"format": "prettier --write .",
"format:check": "prettier --check .",
"postinstall": "node scripts/patch-app-builder-lib.cjs",
"prebuild": "node scripts/write-electron-build-info.cjs",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"type-check": "tsc --noEmit",
@@ -22,93 +27,94 @@
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
"preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
"electron:patch-builder": "node scripts/patch-app-builder-lib.cjs",
"electron:rebuild": "electron-rebuild -f -w better-sqlite3",
"build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
"build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never",
"build:linux-portable": "npm run build && npm run electron:rebuild && electron-builder --linux --dir",
"build:linux-appimage": "npm run build && npm run electron:rebuild && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && npm run electron:rebuild && electron-builder --linux tar.gz",
"build:mac": "npm run build && npm run electron:rebuild && electron-builder --mac --universal"
"build:win-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --dir",
"build:win-installer": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --publish=never",
"build:linux-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux --dir",
"build:linux-appimage": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux tar.gz",
"build:mac": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac --universal",
"build:mac-dev": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac dir --publish=never"
},
"dependencies": {
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^12.2.0",
"body-parser": "^1.20.2",
"chalk": "^4.1.2",
"axios": "^1.15.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.9.0",
"body-parser": "^2.2.2",
"chalk": "^5.6.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"guacamole-lite": "^1.2.0",
"https-proxy-agent": "^7.0.6",
"jose": "^6.2.2",
"js-yaml": "^4.1.1",
"jose": "^5.2.3",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
"multer": "^2.0.2",
"nanoid": "^5.1.5",
"node-fetch": "^3.3.2",
"motion": "^12.38.0",
"multer": "^2.1.1",
"nanoid": "^5.1.9",
"qrcode": "^1.5.4",
"react-is": "^19.2.5",
"socks": "^2.8.7",
"speakeasy": "^2.0.0",
"ssh2": "^1.16.0",
"ws": "^8.18.3"
"ssh2": "^1.17.0",
"undici": "^7.0.0",
"ws": "^8.20.0"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3",
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.23.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0",
"@hookform/resolvers": "^5.1.1",
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/search": "^6.7.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.41.1",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@deadendjs/swagger-jsdoc": "^8.1.2",
"@electron/notarize": "^3.1.1",
"@electron/rebuild": "^4.0.4",
"@eslint/js": "^9.0.0",
"@hookform/resolvers": "^5.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@tailwindcss/vite": "^4.2.4",
"@types/better-sqlite3": "^7.6.13",
"@types/cookie-parser": "^1.4.9",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/cytoscape": "^3.21.9",
"@types/express": "^5.0.3",
"@types/express": "^5.0.6",
"@types/guacamole-common-js": "^1.5.5",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0",
"@types/node": "^24.3.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-grid-layout": "^1.3.6",
"@types/multer": "^2.1.0",
"@types/node": "^24.12.2",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/speakeasy": "^2.0.10",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1",
"@vitejs/plugin-react": "^4.3.4",
"@uiw/codemirror-extensions-langs": "^4.25.9",
"@uiw/codemirror-theme-github": "^4.25.9",
"@uiw/react-codemirror": "^4.25.9",
"@vitejs/plugin-react": "^6.0.1",
"@xterm/addon-clipboard": "^0.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
@@ -118,53 +124,65 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"concurrently": "^9.2.1",
"cytoscape": "^3.33.1",
"electron": "^38.0.0",
"electron-builder": "^26.0.12",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"cytoscape": "^3.33.2",
"electron": "^41.3.0",
"electron-builder": "^26.8.1",
"eslint": "^9.0.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.5.0",
"guacamole-common-js": "^1.5.0",
"husky": "^9.1.7",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"lint-staged": "^16.2.3",
"lucide-react": "^0.525.0",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"lint-staged": "^16.4.0",
"lucide-react": "^1.11.0",
"next-themes": "^0.4.6",
"prettier": "3.6.2",
"react": "^19.1.0",
"prettier": "3.8.3",
"react": "^19.2.5",
"react-cytoscapejs": "^2.0.0",
"react-dom": "^19.1.0",
"react-grid-layout": "^2.2.2",
"react-h5-audio-player": "^3.10.1",
"react-hook-form": "^7.60.0",
"react-i18next": "^15.7.3",
"react-icons": "^5.5.0",
"react-dom": "^19.2.5",
"react-grid-layout": "^2.2.3",
"react-h5-audio-player": "^3.10.2",
"react-hook-form": "^7.73.1",
"react-i18next": "^17.0.4",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"react-pdf": "^10.1.0",
"react-pdf": "^10.4.1",
"react-photo-view": "^1.2.7",
"react-resizable-panels": "^3.0.3",
"react-simple-keyboard": "^3.8.120",
"react-syntax-highlighter": "^15.6.6",
"react-resizable-panels": "^4.10.0",
"react-simple-keyboard": "^3.8.196",
"react-syntax-highlighter": "^16.1.1",
"react-xtermjs": "^1.0.10",
"recharts": "^3.2.1",
"recharts": "^3.8.1",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"swagger-jsdoc": "^6.2.8",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5",
"zod": "^4.0.5"
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.0",
"vite": "^8.0.10",
"zod": "^4.3.6"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{js,jsx}": [
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
},
"overrides": {
"@electron/asar": "^4.2.0",
"@electron/get": "^5.0.0",
"dompurify": "^3.4.1",
"eslint-visitor-keys": "^4.2.1",
"prebuild-install": "npm:@mmomtchev/prebuild-install@1.0.2",
"rimraf": "file:vendor/rimraf-compat"
}
}
+3 -13
View File
@@ -1,8 +1,5 @@
const CACHE_NAME = "termix-v1";
const CACHE_NAME = "termix-static-v2";
const STATIC_ASSETS = [
"/",
"/index.html",
"/manifest.json",
"/favicon.ico",
"/icons/48x48.png",
"/icons/128x128.png",
@@ -66,18 +63,11 @@ self.addEventListener("fetch", (event) => {
}
if (request.mode === "navigate") {
event.respondWith(
fetch(request).catch(() => {
return caches.match("/index.html");
}),
);
event.respondWith(fetch(request));
return;
}
const isStaticAsset = STATIC_ASSETS.some((asset) => {
if (asset === "/") return url.pathname === "/";
return url.pathname === asset || url.pathname.startsWith("/assets/");
});
const isStaticAsset = STATIC_ASSETS.some((asset) => url.pathname === asset);
if (!isStaticAsset) {
return;
+4 -19
View File
@@ -1,20 +1,7 @@
# إحصائيات المستودع
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · 🇸🇦 العربية · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
إذا كنت ترغب في ذلك، يمكنك دعم المشروع هنا!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# نظرة عامة
<p align="center">
@@ -50,7 +34,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا
- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك سمات الطرفية الشائعة والخطوط والمكونات الأخرى.
- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة.
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r.
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH بين الخوادم مع إعادة الاتصال التلقائي ومراقبة الحالة وإعادة التوجيه المحلي أو البعيد أو SOCKS الديناميكي. يتم تخزين إعدادات نفق العميل-المكتبي إلى السيرفر محلياً لكل تثبيت مكتبي؛ يمكن حفظ لقطات C2S الاختيارية على الخادم وإعادة تسميتها وتحميلها أو حذفها لنقل تكوين النفق المحلي بين العملاء.
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo.
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها.
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH.
@@ -59,6 +43,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار.
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC (مع التحكم في الوصول) و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً.
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات.
- **مفاتيح API** - إنشاء مفاتيح API محددة النطاق للمستخدم مع تواريخ انتهاء صلاحية للاستخدام في الأتمتة/CI.
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات.
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS.
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين العديد من سمات واجهة المستخدم بما في ذلك الفاتح والداكن و Dracula وغيرها. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# 仓库统计
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文 ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · 🇨🇳 中文 · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
如果你愿意,可以在这里支持这个项目!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 概览
<p align="center">
@@ -50,7 +34,7 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平
- **SSH 终端访问** - 功能齐全的终端,支持分屏(最多 4 个面板),并配有类似浏览器的标签系统。包括对自定义终端的支持,如常用的终端主题、字体和其他组件。
- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能。
- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的 SSH 隧道,支持 -l 或 -r 连接
- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的服务器间 SSH 隧道,支持本地、远程或动态 SOCKS 转发。桌面客户端到服务器的隧道设置按桌面安装本地存储,可选的 C2S 预设快照可保存到服务器、重命名、加载或删除,以便在客户端之间迁移本地隧道配置
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。支持通过 sudo 无缝上传、下载、重命名、删除和移动文件。
- **Docker 管理** - 启动、停止、暂停、移除容器。查看容器统计信息。通过 docker exec 终端控制容器。它的初衷不是取代 Portainer 或 Dockge,而是为了比直接创建容器更简单地管理它们。
- **SSH 主机管理器** - 通过标签和文件夹保存、组织和管理您的 SSH 连接,轻松保存可重用的登录信息,并能自动化部署 SSH 密钥。
@@ -59,6 +43,7 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平
- **RBAC** - 创建角色并在用户/角色之间共享主机。
- **用户认证** - 安全的用户管理,具有管理员控制、OIDC(带访问控制)和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地账户链接在一起。
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多。
- **API 密钥** - 创建带有到期日期的用户范围 API 密钥,用于自动化/CI。
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据。
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向。
- **现代 UI** - 使用 React、Tailwind CSS 和 Shadcn 构建的整洁的桌面/移动友好界面。有多种 UI 主题可选,包括浅色、深色、Dracula 等。使用 URL 路由全屏打开任何连接。
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# Repo-Statistiken
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · 🇩🇪 Deutsch · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Wenn Sie möchten, können Sie das Projekt hier unterstützen!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Überblick
<p align="center">
@@ -50,7 +34,7 @@ Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-S
- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten.
- **Remote-Desktop-Zugriff** - RDP-, VNC- und Telnet-Unterstützung über den Browser mit vollständiger Anpassung und Split-Screen.
- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen.
- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie Server-zu-Server-SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie lokaler, entfernter oder dynamischer SOCKS-Weiterleitung. Desktop-Client-zu-Server-Tunneleinstellungen werden lokal pro Desktop-Installation gespeichert, optionale C2S-Preset-Snapshots können auf dem Server gespeichert, umbenannt, geladen oder gelöscht werden, um eine lokale Tunnelkonfiguration zwischen Clients zu übertragen.
- **Remote-Dateimanager** - Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstützung für das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, löschen oder verschieben Sie sie nahtlos mit Sudo-Unterstützung.
- **Docker-Verwaltung** - Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container über Docker-Exec-Terminal steuern. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen.
- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren.
@@ -59,6 +43,7 @@ Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-S
- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen.
- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- (mit Zugriffskontrolle) sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen.
- **Datenbankverschlüsselung** - Backend gespeichert als verschlüsselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security).
- **API-Schlüssel** - Erstellen Sie benutzerbezogene API-Schlüssel mit Ablaufdaten zur Verwendung für Automatisierung/CI.
- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren.
- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen.
- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen vielen verschiedenen UI-Themes einschließlich Hell, Dunkel, Dracula usw. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# Estadísticas del Repositorio
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · 🇪🇸 Español · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Si lo desea, puede apoyar el proyecto aquí.\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Descripción General
<p align="center">
@@ -50,7 +34,7 @@ Termix es una plataforma de gestión de servidores todo en uno, de código abier
- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes.
- **Acceso a Escritorio Remoto** - Soporte RDP, VNC y Telnet a través del navegador con personalización completa y pantalla dividida.
- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r.
- **Gestión de Túneles SSH** - Cree y gestione túneles SSH de servidor a servidor con reconexión automática, monitoreo de estado y reenvío local, remoto o dinámico SOCKS. La configuración de túneles de cliente de escritorio a servidor se almacena localmente por instalación de escritorio; los snapshots de presets C2S opcionales pueden guardarse en el servidor, renombrarse, cargarse o eliminarse para mover una configuración de túnel local entre clientes.
- **Gestor Remoto de Archivos** - Gestione archivos directamente en servidores remotos con soporte para visualizar y editar código, imágenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo.
- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal docker exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos.
- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH.
@@ -59,6 +43,7 @@ Termix es una plataforma de gestión de servidores todo en uno, de código abier
- **RBAC** - Cree roles y comparta hosts entre usuarios/roles.
- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC (con control de acceso) y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí.
- **Cifrado de Base de Datos** - Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentación](https://docs.termix.site/security) para más información.
- **Claves API** - Cree claves API con ámbito de usuario y fechas de vencimiento para usar en automatización/CI.
- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos.
- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS.
- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre muchos temas de UI diferentes, incluyendo claro, oscuro, Dracula, etc. Use rutas URL para abrir cualquier conexión en pantalla completa.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# Statistiques du dépôt
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · 🇫🇷 Français · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Si vous le souhaitez, vous pouvez soutenir le projet ici !\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Présentation
<p align="center">
@@ -50,7 +34,7 @@ Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jam
- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants.
- **Accès Bureau à Distance** - Support RDP, VNC et Telnet via navigateur avec personnalisation complète et écran partagé.
- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r.
- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH de serveur à serveur avec reconnexion automatique, surveillance de l'état et transfert local, distant ou SOCKS dynamique. Les paramètres de tunnel client-bureau-vers-serveur sont stockés localement par installation bureau ; des instantanés de préréglages C2S optionnels peuvent être sauvegardés sur le serveur, renommés, chargés ou supprimés pour déplacer une configuration de tunnel locale entre clients.
- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo.
- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer.
- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH.
@@ -59,6 +43,7 @@ Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jam
- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles.
- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC (avec contrôle d'accès) et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble.
- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails.
- **Clés API** - Créez des clés API à portée utilisateur avec des dates d'expiration pour une utilisation en automatisation/CI.
- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers.
- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS.
- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez parmi de nombreux thèmes d'interface utilisateur, notamment clair, sombre, Dracula, etc. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# रिपॉजिटरी आँकड़े
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · 🇮🇳 हिन्दी · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
यदि आप चाहें, तो आप यहाँ प्रोजेक्ट को सपोर्ट कर सकते हैं!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# अवलोकन
<p align="center">
@@ -50,7 +34,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु
- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है।
- **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ।
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन, हेल्थ मॉनिटरिंग और लोकल, रिमोट या डायनेमिक SOCKS फॉरवर्डिंग के साथ सर्वर-टु-सर्वर SSH टनल बनाएँ और प्रबंधित करें। डेस्कटॉप क्लाइंट-टु-सर्वर टनल सेटिंग्स प्रत्येक डेस्कटॉप इंस्टॉल में स्थानीय रूप से संग्रहीत होती हैं; वैकल्पिक C2S प्रीसेट स्नैपशॉट सर्वर पर सेव, रीनेम, लोड या डिलीट किए जा सकते हैं
- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें।
- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है।
- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें।
@@ -59,6 +43,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु
- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें।
- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC (एक्सेस कंट्रोल के साथ) और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें।
- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें।
- **API कुंजियाँ** - ऑटोमेशन/CI के लिए उपयोग हेतु समाप्ति तिथियों के साथ उपयोगकर्ता-स्कोप्ड API कुंजियाँ बनाएँ।
- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें।
- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन।
- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। लाइट, डार्क, ड्रैकुला आदि सहित कई अलग-अलग UI थीम के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें।
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# Statistiche Repo
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · 🇮🇹 Italiano
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Se lo desideri, puoi supportare il progetto qui!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Panoramica
<p align="center">
@@ -50,7 +34,7 @@ Termix è una piattaforma di gestione server tutto-in-uno, open-source, per semp
- **Accesso Terminale SSH** - Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni.
- **Accesso Desktop Remoto** - Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso.
- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r.
- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH da server a server con riconnessione automatica, monitoraggio dello stato e inoltro locale, remoto o SOCKS dinamico. Le impostazioni del tunnel da client desktop a server sono archiviate localmente per ogni installazione desktop; gli snapshot di preset C2S opzionali possono essere salvati sul server, rinominati, caricati o eliminati per spostare una configurazione di tunnel locale tra i client.
- **Gestore File Remoto** - Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo.
- **Gestione Docker** - Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Non è stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione.
- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH.
@@ -59,6 +43,7 @@ Termix è una piattaforma di gestione server tutto-in-uno, open-source, per semp
- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli.
- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC (con controllo degli accessi) e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro.
- **Crittografia Database** - Il backend è archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni.
- **Chiavi API** - Crea chiavi API con ambito utente e date di scadenza da utilizzare per automazione/CI.
- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file.
- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS.
- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra molti temi UI diversi, inclusi chiaro, scuro, Dracula, ecc. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# リポジトリ統計
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語 ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · 🇯🇵 日本語 · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
プロジェクトを支援していただける方はこちらからどうぞ!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 概要
<p align="center">
@@ -50,7 +34,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー
- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応しています。
- **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応しています。
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応しています。
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング、ローカル・リモート・ダイナミックSOCKSフォワーディングを備えたサーバー間SSHトンネルの作成・管理が可能です。デスクトップクライアント対サーバーのトンネル設定はデスクトップインストールごとにローカルに保存され、オプションのC2Sプリセットスナップショットをサーバーに保存・名前変更・読み込み・削除してクライアント間でローカルトンネル設定を移動できます。
- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理できます。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行できます。
- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易的な管理を目的としています。
- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化できます。
@@ -59,6 +43,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー
- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有できます。
- **ユーザー認証** - 管理者コントロールとOIDC(アクセス制御付き)および2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携が可能です。
- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存されます。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください。
- **APIキー** - 自動化/CI用に有効期限付きのユーザースコープAPIキーを作成できます。
- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポートが可能です。
- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理が可能です。
- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ライト、ダーク、Draculaなど、多くの異なるUIテーマから選択可能。URLルートで任意の接続をフルスクリーンで開くことができます。
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+4 -19
View File
@@ -1,20 +1,7 @@
# 리포지토리 통계
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어 ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · 🇰🇷 한국어 · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
프로젝트를 후원하고 싶으시다면 여기에서 지원해 주세요!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 개요
<p align="center">
@@ -50,7 +34,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버
- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원.
- **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원.
- **SSH 터널 관리** - 자동 재연결 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원.
- **SSH 터널 관리** - 자동 재연결, 상태 모니터링, 로컬·원격·동적 SOCKS 포워딩을 지원하는 서버 간 SSH 터널 생성 및 관리. 데스크톱 클라이언트-서버 터널 설정은 데스크톱 설치별로 로컬에 저장되며, 선택적 C2S 사전 설정 스냅샷을 서버에 저장·이름 변경·불러오기·삭제하여 클라이언트 간 로컬 터널 구성을 이동할 수 있습니다.
- **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행.
- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다.
- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화.
@@ -59,6 +43,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버
- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유.
- **사용자 인증** - 관리자 제어와 OIDC(액세스 제어 포함) 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동.
- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요.
- **API 키** - 자동화/CI에 사용할 만료일이 있는 사용자 범위 API 키 생성.
- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기.
- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리.
- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 라이트, 다크, 드라큘라 등 다양한 UI 테마 선택 가능. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
+8 -23
View File
@@ -1,20 +1,7 @@
# Estatísticas do Repositório
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · 🇧🇷 Português · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Se desejar, você pode apoiar o projeto aqui!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Visão Geral
<p align="center">
@@ -50,7 +34,7 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a
- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes.
- **Acesso à Área de Trabalho Remota** - Suporte a RDP, VNC e Telnet pelo navegador com personalização completa e tela dividida
- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH com reconexão automática e monitoramento de saúde, com suporte para conexões -l ou -r
- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH de servidor para servidor com reconexão automática, monitoramento de saúde e encaminhamento local, remoto ou SOCKS dinâmico. As configurações de túnel de cliente desktop para servidor são armazenadas localmente por instalação de desktop; snapshots de predefinições C2S opcionais podem ser salvos no servidor, renomeados, carregados ou excluídos para mover uma configuração de túnel local entre clientes.
- **Gerenciador Remoto de Arquivos** - Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar código, imagens, áudio e vídeo. Faça upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo.
- **Gerenciamento de Docker** - Inicie, pare, pause, remova contêineres. Visualize estatísticas de contêineres. Controle contêineres usando o terminal Docker Exec. Não foi feito para substituir Portainer ou Dockge, mas sim para simplesmente gerenciar seus contêineres em vez de criá-los.
- **Gerenciador de Hosts SSH** - Salve, organize e gerencie suas conexões SSH com tags e pastas, e salve facilmente informações de login reutilizáveis com a capacidade de automatizar a implantação de chaves SSH
@@ -59,6 +43,7 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a
- **RBAC** - Crie funções e compartilhe hosts entre usuários/funções
- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC (com controle de acesso) e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si.
- **Criptografia de Banco de Dados** - Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentação](https://docs.termix.site/security) para mais informações.
- **Chaves de API** - Crie chaves de API com escopo de usuário e datas de expiração para uso em automação/CI.
- **Exportação/Importação de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos
- **Configuração Automática de SSL** - Geração e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS
- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre muitos temas de interface diferentes, incluindo claro, escuro, Drácula, etc. Use rotas de URL para abrir qualquer conexão em tela cheia.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
@@ -181,7 +166,7 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
@@ -202,12 +187,12 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr
<p align="center">
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Alguns vídeos e imagens podem estar desatualizados ou podem não mostrar perfeitamente as funcionalidades.
+8 -23
View File
@@ -1,20 +1,7 @@
# Статистика репозитория
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · 🇷🇺 Русский · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Если хотите, вы можете поддержать проект здесь!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Обзор
<p align="center">
@@ -50,7 +34,7 @@ Termix — это платформа для управления сервера
- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты.
- **Доступ к удалённому рабочему столу** — Поддержка RDP, VNC и Telnet через браузер с полной настройкой и разделением экрана
- **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r
- **Управление SSH-туннелями** — Создание и управление межсерверными SSH-туннелями с автоматическим переподключением, мониторингом состояния и локальной, удалённой или динамической SOCKS-переадресацией. Настройки туннелей «десктопный клиент — сервер» хранятся локально для каждой установки; опциональные снимки C2S-пресетов можно сохранять на сервере, переименовывать, загружать или удалять для переноса конфигурации между клиентами.
- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo.
- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием.
- **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей
@@ -59,6 +43,7 @@ Termix — это платформа для управления сервера
- **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей
- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC (с контролем доступа) и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов.
- **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security)
- **API-ключи** — Создание API-ключей с областью видимости пользователя и сроками действия для использования в автоматизации/CI.
- **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера
- **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS
- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между множеством различных тем интерфейса, включая светлую, тёмную, Dracula и т. д. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
@@ -181,7 +166,7 @@ networks:
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
@@ -202,12 +187,12 @@ networks:
<p align="center">
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность.
+8 -23
View File
@@ -1,20 +1,7 @@
# Repo İstatistikleri
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · 🇹🇷 Türkçe · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Genel Bakış
<p align="center">
@@ -50,7 +34,7 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra
- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir.
- **Uzak Masaüstü Erişimi** - Tam özelleştirme ve bölünmüş ekran ile tarayıcı üzerinden RDP, VNC ve Telnet desteği
- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma ve sağlık izleme ile SSH tünelleri oluşturun ve yönetin, -l veya -r bağlantıları desteğiyle
- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma, sağlık izleme ve yerel, uzak veya dinamik SOCKS yönlendirme desteğiyle sunucular arası SSH tünelleri oluşturun ve yönetin. Masaüstü istemci-sunucu tünel ayarları her masaüstü kurulumu için yerel olarak depolanır; isteğe bağlı C2S hazır ayar anlık görüntüleri sunucuya kaydedilebilir, yeniden adlandırılabilir, yüklenebilir veya silinebilir.
- **Uzak Dosya Yöneticisi** - Uzak sunuculardaki dosyaları doğrudan yönetin; kod, görüntü, ses ve video görüntüleme ve düzenleme desteğiyle. Sudo desteğiyle dosyaları sorunsuzca yükleyin, indirin, yeniden adlandırın, silin ve taşıyın.
- **Docker Yönetimi** - Konteynerleri başlatın, durdurun, duraklatın, kaldırın. Konteyner istatistiklerini görüntüleyin. Docker exec terminali kullanarak konteyneri kontrol edin. Portainer veya Dockge'nin yerini almak için değil, konteynerlerinizi oluşturmak yerine basitçe yönetmek için tasarlanmıştır.
- **SSH Ana Bilgisayar Yöneticisi** - SSH bağlantılarınızı etiketler ve klasörlerle kaydedin, düzenleyin ve yönetin; yeniden kullanılabilir giriş bilgilerini kolayca kaydedin ve SSH anahtarlarının dağıtımını otomatikleştirin
@@ -59,6 +43,7 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra
- **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın
- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC (erişim kontrollü) ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın.
- **Veritabanı Şifreleme** - Arka uç, şifrelenmiş SQLite veritabanı dosyaları olarak depolanır. Daha fazla bilgi için [belgelere](https://docs.termix.site/security) bakın.
- **API Anahtarları** - Otomasyon/CI için kullanılmak üzere son kullanma tarihleriyle kullanıcı kapsamlı API anahtarları oluşturun.
- **Veri Dışa/İçe Aktarma** - SSH ana bilgisayarlarını, kimlik bilgilerini ve dosya yöneticisi verilerini dışa ve içe aktarın
- **Otomatik SSL Kurulumu** - HTTPS yönlendirmeleriyle yerleşik SSL sertifika oluşturma ve yönetimi
- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Işık, karanlık, Dracula vb. dahil olmak üzere birçok farklı UI teması arasından seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
@@ -181,7 +166,7 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
@@ -202,12 +187,12 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce
<p align="center">
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Bazı videolar ve görseller güncel olmayabilir veya özellikleri tam olarak yansıtmayabilir.
+8 -23
View File
@@ -1,20 +1,7 @@
# Thống Kê Repo
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · 🇻🇳 Tiếng Việt · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Nếu bạn muốn, bạn có thể hỗ trợ dự án tại đây!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Tổng Quan
<p align="center">
@@ -50,7 +34,7 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ
- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác.
- **Truy Cập Màn Hình Từ Xa** - Hỗ trợ RDP, VNC và Telnet qua trình duyệt với đầy đủ tùy chỉnh và chia màn hình
- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH với tự động kết nối lại giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r
- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH giữa các máy chủ với tự động kết nối lại, giám sát sức khỏe và chuyển tiếp cục bộ, từ xa hoặc SOCKS động. Cài đặt đường hầm từ máy khách desktop đến máy chủ được lưu trữ cục bộ cho mỗi bản cài đặt desktop; các snapshot C2S preset tùy chọn có thể được lưu trên máy chủ, đổi tên, tải hoặc xóa để di chuyển cấu hình đường hầm cục bộ giữa các máy khách.
- **Trình Quản Lý Tệp Từ Xa** - Quản lý tệp trực tiếp trên máy chủ từ xa với hỗ trợ xem và chỉnh sửa mã, hình ảnh, âm thanh và video. Tải lên, tải xuống, đổi tên, xóa và di chuyển tệp liền mạch với hỗ trợ sudo.
- **Quản Lý Docker** - Khởi động, dừng, tạm dừng, xóa container. Xem thống kê container. Điều khiển container bằng terminal docker exec. Không được tạo ra để thay thế Portainer hay Dockge mà đơn giản là để quản lý container của bạn thay vì tạo mới chúng.
- **Trình Quản Lý Máy Chủ SSH** - Lưu, sắp xếp và quản lý các kết nối SSH của bạn với thẻ và thư mục, dễ dàng lưu thông tin đăng nhập có thể tái sử dụng đồng thời có thể tự động hóa việc triển khai khóa SSH
@@ -59,6 +43,7 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ
- **RBAC** - Tạo vai trò và chia sẻ máy chủ giữa người dùng/vai trò
- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC (có kiểm soát truy cập) và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau.
- **Mã Hóa Cơ Sở Dữ Liệu** - Backend được lưu trữ dưới dạng tệp cơ sở dữ liệu SQLite được mã hóa. Xem [tài liệu](https://docs.termix.site/security) để biết thêm.
- **Khóa API** - Tạo khóa API theo phạm vi người dùng với ngày hết hạn để sử dụng cho tự động hóa/CI.
- **Xuất/Nhập Dữ Liệu** - Xuất và nhập máy chủ SSH, thông tin xác thực và dữ liệu trình quản lý tệp
- **Thiết Lập SSL Tự Động** - Tạo và quản lý chứng chỉ SSL tích hợp với chuyển hướng HTTPS
- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa nhiều chủ đề UI khác nhau bao gồm sáng, tối, Dracula, v.v. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình.
@@ -122,7 +107,7 @@ services:
- termix-net
guacd:
image: guacamole/guacd:latest
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
@@ -181,7 +166,7 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
@@ -202,12 +187,12 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b
<p align="center">
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Một số video và hình ảnh có thể đã lỗi thời hoặc không thể hiện chính xác hoàn toàn các tính năng.
+197
View File
@@ -0,0 +1,197 @@
const fs = require("node:fs");
const path = require("node:path");
const collectorPath = path.join(
__dirname,
"..",
"node_modules",
"app-builder-lib",
"out",
"node-module-collector",
"nodeModulesCollector.js",
);
const appFileCopierPath = path.join(
__dirname,
"..",
"node_modules",
"app-builder-lib",
"out",
"util",
"appFileCopier.js",
);
const moduleManagerPath = path.join(
__dirname,
"..",
"node_modules",
"app-builder-lib",
"out",
"node-module-collector",
"moduleManager.js",
);
function patchFile(filePath, replacements) {
if (!fs.existsSync(filePath)) {
return;
}
let source = fs.readFileSync(filePath, "utf8");
let changed = false;
for (const { original, patched, name, alreadyPatched = [] } of replacements) {
if (
source.includes(patched) ||
alreadyPatched.some((marker) => source.includes(marker))
) {
continue;
}
if (!source.includes(original)) {
console.warn(
`app-builder-lib patch "${name}" was not applied; expected source was not found.`,
);
continue;
}
source = source.replace(original, patched);
changed = true;
}
if (changed) {
fs.writeFileSync(filePath, source);
}
}
patchFile(collectorPath, [
{
name: "node module collector spawn shell",
original: ` shell: true, // \`true\`\` is now required: https://github.com/electron-userland/electron-builder/issues/9488`,
patched: ` shell: false, // Avoid Node DEP0190; .cmd files are wrapped through cmd.exe above.`,
},
{
name: "node module collector output flush",
alreadyPatched: [
` outStream.end();
}`,
],
original: ` outStream.close();
// https://github.com/npm/npm/issues/17624
const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list");
if (shouldIgnore) {
builder_util_1.log.debug(null, "\`npm list\` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details.");
}
if (stderr.length > 0) {
builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr");
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr);
}
const shouldResolve = code === 0 || shouldIgnore;
return shouldResolve ? resolve() : reject(new Error(\`Node module collector process exited with code \${code}:\\n\${stderr}\`));`,
patched: ` const finish = () => {
// https://github.com/npm/npm/issues/17624
const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list");
if (shouldIgnore) {
builder_util_1.log.debug(null, "\`npm list\` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details.");
}
if (stderr.length > 0) {
builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr");
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr);
}
const shouldResolve = code === 0 || shouldIgnore;
return shouldResolve ? resolve() : reject(new Error(\`Node module collector process exited with code \${code}:\\n\${stderr}\`));
};
if (outStream.writableFinished) {
finish();
}
else {
outStream.once("finish", finish);
outStream.end();
}`,
},
{
name: "node module collector finish guard",
original: ` if (outStream.writableFinished || outStream.closed) {`,
patched: ` if (outStream.writableFinished) {`,
},
]);
patchFile(moduleManagerPath, [
{
name: "node module collector npm alias resolution",
original: ` semverSatisfies(found, range) {
if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {`,
patched: ` semverSatisfies(found, range, packageNameMatches = true) {
if (!packageNameMatches) {
return true;
}
if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {`,
},
{
name: "node module collector direct alias match",
original: ` if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(direct), packageJson: json };
}`,
patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) {
return { packageDir: path.dirname(direct), packageJson: json };
}`,
},
{
name: "node module collector alias match",
original: ` if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidate), packageJson: json };
}`,
patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) {
return { packageDir: path.dirname(candidate), packageJson: json };
}`,
},
{
name: "node module collector scoped alias match",
original: ` if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}`,
patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}`,
},
{
name: "node module collector nested alias match",
original: ` if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}`,
patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}`,
},
{
name: "node module collector nested direct alias match",
original: ` if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidateDirect), packageJson: json };
}`,
patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) {
return { packageDir: path.dirname(candidateDirect), packageJson: json };
}`,
},
]);
patchFile(appFileCopierPath, [
{
name: "node module collector fallback",
original: ` const collector = (0, node_module_collector_1.getCollectorByPackageManager)(pm, dir, tempDirManager);
deps = await collector.getNodeModules({ packageName: packager.metadata.name });
if (deps.nodeModules.length > 0) {`,
patched: ` const collector = (0, node_module_collector_1.getCollectorByPackageManager)(pm, dir, tempDirManager);
try {
deps = await collector.getNodeModules({ packageName: packager.metadata.name });
}
catch (error) {
const isLastSearchDirectory = searchDirectories.indexOf(dir) >= searchDirectories.length - 1;
const isLastPackageManager = pmApproaches.indexOf(pm) >= pmApproaches.length - 1;
if (isLastSearchDirectory && isLastPackageManager) {
throw error;
}
builder_util_1.log.warn({ pm, searchDir: dir, error: error instanceof Error ? error.message : String(error) }, "node modules collection failed, trying fallback");
continue;
}
if (deps.nodeModules.length > 0) {`,
},
]);
+24
View File
@@ -0,0 +1,24 @@
const fs = require("fs");
const path = require("path");
const outputPath = path.join(__dirname, "..", "electron", "build-info.cjs");
const rawBuildTimestamp =
process.env.TERMIX_BUILD_TIMESTAMP || process.env.BUILD_TIMESTAMP;
const parsedBuildTimestamp = rawBuildTimestamp
? Number(rawBuildTimestamp)
: NaN;
const buildTimestamp = Number.isInteger(parsedBuildTimestamp)
? parsedBuildTimestamp
: Math.floor(Date.now() / 1000);
const buildInfo = {
buildTimestamp,
generatedAt: new Date().toISOString(),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(
outputPath,
`module.exports = ${JSON.stringify(buildInfo, null, 2)};\n`,
);
console.log(`Wrote Electron build info: ${buildTimestamp}`);
+71 -8
View File
@@ -8,12 +8,12 @@ import hostRoutes from "./routes/host.js";
import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js";
import snippetsRoutes from "./routes/snippets.js";
import c2sTunnelPresetRoutes from "./routes/c2s-tunnel-presets.js";
import terminalRoutes from "./routes/terminal.js";
import guacamoleRoutes from "../guacamole/routes.js";
import networkTopologyRoutes from "./routes/network-topology.js";
import rbacRoutes from "./routes/rbac.js";
import { createCorsMiddleware } from "../utils/cors-config.js";
import fetch from "node-fetch";
import fs from "fs";
import path from "path";
import os from "os";
@@ -118,6 +118,31 @@ class GitHubCache {
const githubCache = new GitHubCache();
function parseSemver(
version: string | undefined,
): [number, number, number] | null {
const match = String(version || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
if (!match) return null;
return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
}
function compareSemver(
a: string | undefined,
b: string | undefined,
): number | null {
const parsedA = parseSemver(a);
const parsedB = parseSemver(b);
if (!parsedA || !parsedB) return null;
for (let i = 0; i < 3; i += 1) {
if (parsedA[i] > parsedB[i]) return 1;
if (parsedA[i] < parsedB[i]) return -1;
}
return 0;
}
const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix";
@@ -143,7 +168,7 @@ async function fetchGitHubAPI<T>(
"User-Agent": "TermixUpdateChecker/1.0",
"X-GitHub-Api-Version": "2022-11-28",
},
agent: getProxyAgent(url),
dispatcher: getProxyAgent(url),
});
if (!response.ok) {
@@ -299,12 +324,19 @@ app.get("/version", authenticateJWT, async (req, res) => {
return res.status(401).send("Remote Version Not Found");
}
const isUpToDate = localVersion === remoteVersion;
const versionComparison = compareSemver(localVersion, remoteVersion);
const status =
versionComparison === null || versionComparison === 0
? "up_to_date"
: versionComparison > 0
? "beta"
: "requires_update";
const response = {
status: isUpToDate ? "up_to_date" : "requires_update",
status,
localVersion: localVersion,
version: remoteVersion,
remoteVersion: remoteVersion,
latest_release: {
tag_name: releaseData.data.tag_name,
name: releaseData.data.name,
@@ -624,7 +656,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
operation: "export_temp_dir_error",
tempDir,
});
throw new Error(`Failed to create temp directory: ${dirError.message}`);
throw new Error(`Failed to create temp directory: ${dirError.message}`, {
cause: dirError,
});
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
@@ -1161,7 +1195,7 @@ app.post(
mimetype: req.file.mimetype,
});
let userDataKey = DataCrypto.getUserDataKey(userId);
const userDataKey = DataCrypto.getUserDataKey(userId);
if (!userDataKey) {
throw new Error("User data not unlocked");
}
@@ -1719,6 +1753,7 @@ app.use("/host", hostRoutes);
app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes);
app.use("/snippets", snippetsRoutes);
app.use("/c2s-tunnel-presets", c2sTunnelPresetRoutes);
app.use("/terminal", terminalRoutes);
app.use("/guacamole", guacamoleRoutes);
app.use("/network-topology", networkTopologyRoutes);
@@ -1738,10 +1773,38 @@ if (frontendDist) {
databaseLogger.info(`Serving frontend from: ${frontendDist}`, {
operation: "static_files",
});
app.use(express.static(frontendDist));
app.use(
express.static(frontendDist, {
setHeaders: (res, filePath) => {
const relativePath = path
.relative(frontendDist, filePath)
.replaceAll(path.sep, "/");
if (relativePath.startsWith("assets/")) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
return;
}
if (
relativePath === "index.html" ||
relativePath === "sw.js" ||
relativePath === "manifest.json"
) {
res.setHeader(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0",
);
}
},
}),
);
app.use((req, res, next) => {
if (req.method === "GET" && req.accepts("html")) {
res.setHeader(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0",
);
res.sendFile(path.join(frontendDist, "index.html"));
} else {
next();
@@ -1750,13 +1813,13 @@ if (frontendDist) {
}
app.use(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(
err: unknown,
req: express.Request,
res: express.Response,
_next: express.NextFunction,
) => {
void _next;
apiLogger.error("Unhandled error in request", err, {
operation: "error_handler",
method: req.method,
+86 -1
View File
@@ -118,6 +118,7 @@ async function initializeDatabaseAsync(): Promise<void> {
throw new Error(
`Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`,
{ cause: error },
);
}
} else {
@@ -317,6 +318,18 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS c2s_tunnel_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL,
platform TEXT,
computer_name TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS ssh_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
@@ -424,10 +437,31 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT,
last_used_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
try {
sqlite.prepare("DELETE FROM sessions").run();
const result = sqlite
.prepare("DELETE FROM sessions WHERE expires_at <= ?")
.run(new Date().toISOString());
if (result.changes > 0) {
databaseLogger.info("Expired sessions cleaned up on startup", {
operation: "db_init_session_cleanup",
deletedSessions: result.changes,
});
}
} catch (e) {
databaseLogger.warn("Could not clear expired sessions on startup", {
operation: "db_init_session_cleanup_failed",
@@ -803,6 +837,31 @@ const migrateSchema = () => {
}
}
try {
sqlite.prepare("SELECT id FROM c2s_tunnel_presets LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS c2s_tunnel_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL,
platform TEXT,
computer_name TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create c2s_tunnel_presets table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
sqlite
.prepare("SELECT id FROM sessions LIMIT 1")
@@ -1175,6 +1234,32 @@ const migrateSchema = () => {
}
}
try {
sqlite.prepare("SELECT id FROM api_keys LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT,
last_used_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create api_keys table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
+31
View File
@@ -298,6 +298,23 @@ export const snippetFolders = sqliteTable("snippet_folders", {
.default(sql`CURRENT_TIMESTAMP`),
});
export const c2sTunnelPresets = sqliteTable("c2s_tunnel_presets", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
config: text("config").notNull(),
platform: text("platform"),
computerName: text("computer_name"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const snippetAccess = sqliteTable("snippet_access", {
id: integer("id").primaryKey({ autoIncrement: true }),
snippetId: integer("snippet_id")
@@ -572,3 +589,17 @@ export const opksshTokens = sqliteTable("opkssh_tokens", {
expiresAt: text("expires_at").notNull(),
lastUsed: text("last_used"),
});
export const apiKeys = sqliteTable("api_keys", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
tokenHash: text("token_hash").notNull(),
tokenPrefix: text("token_prefix").notNull(),
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
expiresAt: text("expires_at"),
lastUsedAt: text("last_used_at"),
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
});
+1 -2
View File
@@ -7,7 +7,6 @@ import express from "express";
import { db } from "../db/index.js";
import { dismissedAlerts } from "../db/schema.js";
import { eq, and } from "drizzle-orm";
import fetch from "node-fetch";
import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { getProxyAgent } from "../../utils/proxy-agent.js";
@@ -61,7 +60,7 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
Accept: "application/json",
"User-Agent": "TermixAlertChecker/1.0",
},
agent: getProxyAgent(url),
dispatcher: getProxyAgent(url),
});
if (!response.ok) {
@@ -0,0 +1,247 @@
import type {
AuthenticatedRequest,
TunnelConnection,
} from "../../../types/index.js";
import express from "express";
import { db } from "../db/index.js";
import { c2sTunnelPresets } from "../db/schema.js";
import { and, asc, eq, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
const router = express.Router();
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
function parsePreset(row: typeof c2sTunnelPresets.$inferSelect) {
return {
...row,
config: JSON.parse(row.config) as TunnelConnection[],
};
}
function validateConfig(config: unknown): config is TunnelConnection[] {
if (!Array.isArray(config)) return false;
return config.every((item) => {
if (!item || typeof item !== "object") return false;
const tunnel = item as Partial<TunnelConnection>;
const mode = tunnel.mode || tunnel.tunnelType;
return (
tunnel.scope === "c2s" &&
(mode === "local" || mode === "remote" || mode === "dynamic") &&
typeof tunnel.sourcePort === "number" &&
tunnel.sourcePort >= 1 &&
tunnel.sourcePort <= 65535 &&
(mode === "dynamic" ||
(typeof tunnel.endpointPort === "number" &&
tunnel.endpointPort >= 1 &&
tunnel.endpointPort <= 65535))
);
});
}
router.get(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
return res.status(400).json({ error: "Invalid userId" });
}
try {
const result = await db
.select()
.from(c2sTunnelPresets)
.where(eq(c2sTunnelPresets.userId, userId))
.orderBy(asc(c2sTunnelPresets.name));
res.json(result.map(parsePreset));
} catch (error) {
authLogger.error("Failed to fetch C2S tunnel presets", error);
res.status(500).json({ error: "Failed to fetch C2S tunnel presets" });
}
},
);
router.post(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { name, config, platform, computerName } = req.body;
if (!isNonEmptyString(userId) || !isNonEmptyString(name)) {
return res.status(400).json({ error: "Preset name is required" });
}
if (!validateConfig(config)) {
return res
.status(400)
.json({ error: "Invalid C2S tunnel configuration" });
}
const trimmedName = name.trim();
try {
const existing = await db
.select()
.from(c2sTunnelPresets)
.where(
and(
eq(c2sTunnelPresets.userId, userId),
eq(c2sTunnelPresets.name, trimmedName),
),
);
if (existing.length > 0) {
return res.status(409).json({ error: "Preset name already exists" });
}
const result = await db
.insert(c2sTunnelPresets)
.values({
userId,
name: trimmedName,
config: JSON.stringify(config),
platform: platform?.trim() || null,
computerName: computerName?.trim() || null,
})
.returning();
databaseLogger.info("C2S tunnel preset created", {
operation: "c2s_tunnel_preset_create",
userId,
presetId: result[0].id,
});
res.status(201).json(parsePreset(result[0]));
} catch (error) {
authLogger.error("Failed to create C2S tunnel preset", error);
res.status(500).json({ error: "Failed to create C2S tunnel preset" });
}
},
);
router.put(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const id = Number(req.params.id);
const { name, config, platform, computerName } = req.body;
if (!isNonEmptyString(userId) || !Number.isInteger(id)) {
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(c2sTunnelPresets)
.where(
and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)),
);
if (existing.length === 0) {
return res.status(404).json({ error: "Preset not found" });
}
const updateFields: Record<string, unknown> = {
updatedAt: sql`CURRENT_TIMESTAMP`,
};
if (name !== undefined) {
if (!isNonEmptyString(name)) {
return res.status(400).json({ error: "Preset name is required" });
}
const trimmedName = name.trim();
const duplicate = await db
.select()
.from(c2sTunnelPresets)
.where(
and(
eq(c2sTunnelPresets.userId, userId),
eq(c2sTunnelPresets.name, trimmedName),
),
);
if (duplicate.some((preset) => preset.id !== id)) {
return res.status(409).json({ error: "Preset name already exists" });
}
updateFields.name = trimmedName;
}
if (config !== undefined) {
if (!validateConfig(config)) {
return res
.status(400)
.json({ error: "Invalid C2S tunnel configuration" });
}
updateFields.config = JSON.stringify(config);
}
if (platform !== undefined)
updateFields.platform = platform?.trim() || null;
if (computerName !== undefined)
updateFields.computerName = computerName?.trim() || null;
await db
.update(c2sTunnelPresets)
.set(updateFields)
.where(
and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)),
);
const updated = await db
.select()
.from(c2sTunnelPresets)
.where(eq(c2sTunnelPresets.id, id));
res.json(parsePreset(updated[0]));
} catch (error) {
authLogger.error("Failed to update C2S tunnel preset", error);
res.status(500).json({ error: "Failed to update C2S tunnel preset" });
}
},
);
router.delete(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const id = Number(req.params.id);
if (!isNonEmptyString(userId) || !Number.isInteger(id)) {
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(c2sTunnelPresets)
.where(
and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)),
);
if (existing.length === 0) {
return res.status(404).json({ error: "Preset not found" });
}
await db
.delete(c2sTunnelPresets)
.where(
and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)),
);
res.json({ success: true });
} catch (error) {
authLogger.error("Failed to delete C2S tunnel preset", error);
res.status(500).json({ error: "Failed to delete C2S tunnel preset" });
}
},
);
export default router;
+222 -60
View File
@@ -27,6 +27,7 @@ import {
inArray,
} from "drizzle-orm";
import type { Request, Response } from "express";
import axios from "axios";
import multer from "multer";
import { sshLogger, databaseLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
@@ -42,6 +43,32 @@ const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
function notifyStatsHostUpdated(
hostId: number,
headers: Pick<Request["headers"], "authorization" | "cookie">,
operation: string,
): void {
axios
.post(
"http://localhost:30005/host-updated",
{ hostId },
{
headers: {
Authorization: headers.authorization || "",
Cookie: headers.cookie || "",
},
timeout: 5000,
},
)
.catch((err) => {
sshLogger.warn("Failed to notify stats server of host update", {
operation,
hostId,
error: err instanceof Error ? err.message : String(err),
});
});
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
@@ -50,6 +77,172 @@ function isValidPort(port: unknown): port is number {
return typeof port === "number" && port > 0 && port <= 65535;
}
function asString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function asPort(value: unknown): number | undefined {
const port =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseInt(value, 10)
: NaN;
return isValidPort(port) ? port : undefined;
}
function asInteger(value: unknown): number | undefined {
const number =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseInt(value, 10)
: NaN;
return Number.isInteger(number) ? number : undefined;
}
function asBoolean(value: unknown, fallback = false): boolean {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(normalized)) return true;
if (["false", "0", "no", "off"].includes(normalized)) return false;
}
return fallback;
}
function normalizeImportTags(value: unknown): string[] {
if (Array.isArray(value)) {
return value
.map((tag) => asString(tag))
.filter((tag): tag is string => !!tag);
}
if (typeof value === "string") {
return value
.split(",")
.map((tag) => tag.trim())
.filter(Boolean);
}
return [];
}
type NormalizedImportedHost = Record<string, unknown> & {
connectionType: string;
name?: string;
ip?: string;
port: number;
username?: string;
folder?: string;
tags: string[];
authType?: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
credentialId?: number;
pin?: unknown;
enableTerminal?: unknown;
enableTunnel?: unknown;
enableFileManager?: unknown;
enableDocker?: unknown;
showTerminalInSidebar?: unknown;
showFileManagerInSidebar?: unknown;
showTunnelInSidebar?: unknown;
showDockerInSidebar?: unknown;
showServerStatsInSidebar?: unknown;
defaultPath?: unknown;
sudoPassword?: unknown;
tunnelConnections?: unknown;
jumpHosts?: unknown;
quickActions?: unknown;
statsConfig?: unknown;
dockerConfig?: unknown;
terminalConfig?: unknown;
forceKeyboardInteractive?: unknown;
notes?: unknown;
useSocks5?: unknown;
socks5Host?: unknown;
socks5Port?: unknown;
socks5Username?: unknown;
socks5Password?: unknown;
socks5ProxyChain?: unknown;
portKnockSequence?: unknown;
overrideCredentialUsername?: unknown;
domain?: unknown;
security?: unknown;
ignoreCert?: unknown;
guacamoleConfig?: unknown;
enableSsh: boolean;
enableRdp: boolean;
enableVnc: boolean;
enableTelnet: boolean;
};
function normalizeImportedHost(
hostData: Record<string, unknown>,
): NormalizedImportedHost {
const connectionType =
asString(hostData.connectionType) ||
(asBoolean(hostData.enableRdp)
? "rdp"
: asBoolean(hostData.enableVnc)
? "vnc"
: asBoolean(hostData.enableTelnet)
? "telnet"
: "ssh");
const port =
asPort(hostData.port) ||
(connectionType === "rdp"
? asPort(hostData.rdpPort) || 3389
: connectionType === "vnc"
? asPort(hostData.vncPort) || 5900
: connectionType === "telnet"
? asPort(hostData.telnetPort) || 23
: asPort(hostData.sshPort) || 22);
return {
...hostData,
connectionType,
name: asString(hostData.name) || asString(hostData.label),
ip:
asString(hostData.ip) ||
asString(hostData.address) ||
asString(hostData.host) ||
asString(hostData.hostname),
port,
username: asString(hostData.username) || asString(hostData.user),
folder: asString(hostData.folder) || asString(hostData.group),
tags: normalizeImportTags(hostData.tags),
credentialId: asInteger(hostData.credentialId),
authType:
asString(hostData.authType) ||
asString(hostData.authMethod) ||
(hostData.credentialId ? "credential" : hostData.key ? "key" : undefined),
enableSsh:
hostData.enableSsh === undefined
? connectionType === "ssh"
: asBoolean(hostData.enableSsh),
enableRdp:
hostData.enableRdp === undefined
? connectionType === "rdp"
: asBoolean(hostData.enableRdp),
enableVnc:
hostData.enableVnc === undefined
? connectionType === "vnc"
: asBoolean(hostData.enableVnc),
enableTelnet:
hostData.enableTelnet === undefined
? connectionType === "telnet"
: asBoolean(hostData.enableTelnet),
};
}
const SENSITIVE_FIELDS = [
"password",
"key",
@@ -581,29 +774,12 @@ router.post(
name,
});
try {
const axios = (await import("axios")).default;
const statsPort = 30005;
await axios.post(
`http://localhost:${statsPort}/host-updated`,
{ hostId: createdHost.id },
{
headers: {
Authorization: req.headers.authorization || "",
Cookie: req.headers.cookie || "",
},
timeout: 5000,
},
);
} catch (err) {
sshLogger.warn("Failed to notify stats server of new host", {
operation: "host_create",
hostId: createdHost.id as number,
error: err instanceof Error ? err.message : String(err),
});
}
res.json(resolvedHost);
notifyStatsHostUpdated(
createdHost.id as number,
req.headers,
"host_create",
);
} catch (err) {
sshLogger.error("Failed to save SSH host to database", err, {
operation: "host_create",
@@ -1189,29 +1365,8 @@ router.put(
hostId: parseInt(hostId),
});
try {
const axios = (await import("axios")).default;
const statsPort = 30005;
await axios.post(
`http://localhost:${statsPort}/host-updated`,
{ hostId: parseInt(hostId) },
{
headers: {
Authorization: req.headers.authorization || "",
Cookie: req.headers.cookie || "",
},
timeout: 5000,
},
);
} catch (err) {
sshLogger.warn("Failed to notify stats server of host update", {
operation: "host_update",
hostId: parseInt(hostId),
error: err instanceof Error ? err.message : String(err),
});
}
res.json(resolvedHost);
notifyStatsHostUpdated(parseInt(hostId), req.headers, "host_update");
} catch (err) {
sshLogger.error("Failed to update SSH host in database", err, {
operation: "host_update",
@@ -1362,19 +1517,26 @@ router.get(
const ownHosts = rawData.filter((row) => row.userId === userId);
const sharedHosts = rawData.filter((row) => row.userId !== userId);
let decryptedOwnHosts: Record<string, unknown>[] = [];
try {
decryptedOwnHosts = await SimpleDBOps.select(
Promise.resolve(ownHosts),
"ssh_data",
userId,
);
} catch (decryptError) {
sshLogger.error("Failed to decrypt own hosts", decryptError, {
operation: "host_fetch_own_decrypt_failed",
userId,
});
decryptedOwnHosts = [];
const decryptedOwnHosts: Record<string, unknown>[] = [];
const userDataKey = DataCrypto.getUserDataKey(userId);
if (userDataKey) {
for (const host of ownHosts) {
try {
decryptedOwnHosts.push(
DataCrypto.decryptRecord("ssh_data", host, userId, userDataKey),
);
} catch (decryptError) {
sshLogger.warn("Skipping host with invalid encrypted fields", {
operation: "host_fetch_own_decrypt_failed",
userId,
hostId: host.id,
error:
decryptError instanceof Error
? decryptError.message
: "Unknown error",
});
}
}
}
const sanitizedSharedHosts = sharedHosts;
@@ -3319,7 +3481,7 @@ router.patch(
.update(hosts)
.set({ statsConfig: JSON.stringify(merged) })
.where(and(eq(hosts.id, host.id), eq(hosts.userId, userId)));
} catch (e) {
} catch {
errors.push(`Failed to update statsConfig for host ${host.id}`);
}
}
@@ -3385,7 +3547,7 @@ router.post(
}
for (let i = 0; i < hostsToImport.length; i++) {
const hostData = hostsToImport[i];
const hostData = normalizeImportedHost(hostsToImport[i]);
try {
const effectiveConnectionType = hostData.connectionType || "ssh";
@@ -5346,7 +5508,7 @@ router.post(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const hostId = parseInt(req.params.id);
const hostId = Number.parseInt(String(req.params.id), 10);
const userId = (req as AuthenticatedRequest).userId;
try {
+137 -21
View File
@@ -1,8 +1,14 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express";
import { db } from "../db/index.js";
import { snippets, snippetFolders } from "../db/schema.js";
import { eq, and, desc, asc, sql } from "drizzle-orm";
import {
snippets,
snippetFolders,
snippetAccess,
users,
userRoles,
} from "../db/schema.js";
import { eq, and, desc, asc, sql, or, isNull, gte } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
@@ -15,6 +21,92 @@ function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
async function getUserRoleIds(userId: string): Promise<number[]> {
const rows = await db
.select({ roleId: userRoles.roleId })
.from(userRoles)
.where(eq(userRoles.userId, userId));
return rows.map((row) => row.roleId);
}
function roleIdFilter(roleIds: number[]) {
if (roleIds.length === 0) {
return undefined;
}
return sql`${snippetAccess.roleId} IN (${sql.join(
roleIds.map((id) => sql`${id}`),
sql`, `,
)})`;
}
function activeSnippetAccessFilter(userId: string, roleIds: number[]) {
const roleFilter = roleIdFilter(roleIds);
const targetFilter = roleFilter
? or(eq(snippetAccess.userId, userId), roleFilter)
: eq(snippetAccess.userId, userId);
return and(
targetFilter,
or(
isNull(snippetAccess.expiresAt),
gte(snippetAccess.expiresAt, new Date().toISOString()),
),
);
}
function sortSnippets<
T extends { folder: string | null; order: number; updatedAt: string },
>(a: T, b: T) {
const aFolder = a.folder || "";
const bFolder = b.folder || "";
if (!aFolder && bFolder) return -1;
if (aFolder && !bFolder) return 1;
if (aFolder !== bFolder) return aFolder.localeCompare(bFolder);
if (a.order !== b.order) return a.order - b.order;
return b.updatedAt.localeCompare(a.updatedAt);
}
async function getAccessibleSnippet(snippetId: number, userId: string) {
const owned = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId)))
.limit(1);
if (owned.length > 0) {
return owned[0];
}
const roleIds = await getUserRoleIds(userId);
const shared = await db
.select({
id: snippets.id,
userId: snippets.userId,
name: snippets.name,
content: snippets.content,
description: snippets.description,
folder: snippets.folder,
order: snippets.order,
createdAt: snippets.createdAt,
updatedAt: snippets.updatedAt,
})
.from(snippetAccess)
.innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
.where(
and(
eq(snippetAccess.snippetId, snippetId),
activeSnippetAccessFilter(userId, roleIds),
),
)
.limit(1);
return shared[0] ?? null;
}
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
@@ -618,22 +710,12 @@ router.post(
}
try {
const snippetResult = await db
.select()
.from(snippets)
.where(
and(
eq(snippets.id, parseInt(snippetId)),
eq(snippets.userId, userId),
),
);
const snippet = await getAccessibleSnippet(parseInt(snippetId), userId);
if (snippetResult.length === 0) {
if (!snippet) {
return res.status(404).json({ error: "Snippet not found" });
}
const snippet = snippetResult[0];
const { Client } = await import("ssh2");
const { hosts, sshCredentials } = await import("../db/schema.js");
@@ -869,7 +951,7 @@ router.get(
}
try {
const result = await db
const ownedSnippets = await db
.select()
.from(snippets)
.where(eq(snippets.userId, userId))
@@ -880,6 +962,43 @@ router.get(
desc(snippets.updatedAt),
);
const roleIds = await getUserRoleIds(userId);
const sharedSnippets = await db
.select({
id: snippets.id,
userId: snippets.userId,
name: snippets.name,
content: snippets.content,
description: snippets.description,
folder: snippets.folder,
order: snippets.order,
createdAt: snippets.createdAt,
updatedAt: snippets.updatedAt,
ownerUsername: users.username,
permissionLevel: snippetAccess.permissionLevel,
expiresAt: snippetAccess.expiresAt,
})
.from(snippetAccess)
.innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
.innerJoin(users, eq(snippets.userId, users.id))
.where(activeSnippetAccessFilter(userId, roleIds));
const visibleSnippets = new Map<number, Record<string, unknown>>();
for (const snippet of ownedSnippets) {
visibleSnippets.set(snippet.id, { ...snippet, isShared: false });
}
for (const snippet of sharedSnippets) {
if (visibleSnippets.has(snippet.id)) continue;
visibleSnippets.set(snippet.id, { ...snippet, isShared: true });
}
const result = Array.from(visibleSnippets.values()).sort((a, b) =>
sortSnippets(
a as { folder: string | null; order: number; updatedAt: string },
b as { folder: string | null; order: number; updatedAt: string },
),
);
res.json(result);
} catch (err) {
authLogger.error("Failed to fetch snippets", err);
@@ -930,16 +1049,13 @@ router.get(
}
try {
const result = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
const result = await getAccessibleSnippet(snippetId, userId);
if (result.length === 0) {
if (!result) {
return res.status(404).json({ error: "Snippet not found" });
}
res.json(result[0]);
res.json(result);
} catch (err) {
authLogger.error("Failed to fetch snippet", err);
res.status(500).json({
+300 -48
View File
@@ -30,6 +30,7 @@ import {
networkTopology,
dashboardPreferences,
opksshTokens,
apiKeys,
} from "../db/schema.js";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
@@ -223,6 +224,13 @@ function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
function isNativeAppRequest(req: Request): boolean {
return (
(req.get("User-Agent") || "").startsWith("Termix-Mobile/") ||
req.get("X-Electron-App") === "true"
);
}
const authenticateJWT = authManager.createAuthMiddleware();
const requireAdmin = authManager.createAdminMiddleware();
@@ -1142,7 +1150,11 @@ router.get("/oidc/callback", async (req, res) => {
}
}
if (!isFirstUser) {
const oidcAllowRegistration =
(process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() ===
"true";
if (!isFirstUser && !oidcAllowRegistration) {
try {
const regRow = db.$client
.prepare(
@@ -1321,10 +1333,6 @@ router.get("/oidc/callback", async (req, res) => {
const redirectUrl = new URL(frontendOrigin);
redirectUrl.searchParams.set("success", "true");
if (deviceInfo.type === "desktop" || deviceInfo.type === "mobile") {
redirectUrl.searchParams.set("token", token);
}
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
@@ -1574,16 +1582,9 @@ router.post("/login", async (req, res) => {
success: true,
is_admin: !!userRecord.isAdmin,
username: userRecord.username,
...(isNativeAppRequest(req) ? { token } : {}),
};
const isElectron =
req.headers["x-electron-app"] === "true" ||
req.headers["X-Electron-App"] === "true";
if (isElectron) {
response.token = token;
}
const timeoutRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
@@ -1621,18 +1622,7 @@ router.post("/logout", authenticateJWT, async (req, res) => {
const userId = authReq.userId;
if (userId) {
const token =
req.cookies?.jwt || req.headers["authorization"]?.split(" ")[1];
let sessionId: string | undefined;
if (token) {
try {
const payload = await authManager.verifyJWTToken(token);
sessionId = payload?.sessionId;
} catch {
// expected - token verification may fail during logout
}
}
const sessionId = authReq.sessionId;
await authManager.logoutUser(userId, sessionId);
authLogger.info("User logged out", {
@@ -1693,6 +1683,7 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
is_oidc: !!user[0].isOidc,
is_dual_auth: isDualAuth,
totp_enabled: !!user[0].totpEnabled,
data_unlocked: authManager.isUserUnlocked(userId),
});
} catch (err) {
authLogger.error("Failed to get username", err);
@@ -1700,6 +1691,33 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
}
});
/**
* @openapi
* /users/me/token:
* get:
* summary: Get current session token
* description: Returns the JWT for the currently authenticated session. Intended for mobile WebView clients that cannot read HTTP-only cookies.
* tags:
* - Users
* responses:
* 200:
* description: Current session token.
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
* 401:
* description: Not authenticated.
*/
router.get("/me/token", authenticateJWT, (req: Request, res: Response) => {
const token = (req as Request & { cookies: Record<string, string> }).cookies
?.jwt;
res.json({ token: token || null });
});
/**
* @openapi
* /users/setup-required:
@@ -3396,10 +3414,6 @@ router.post("/totp/verify-login", async (req, res) => {
deviceInfo: deviceInfo.deviceInfo,
});
const isElectron =
req.headers["x-electron-app"] === "true" ||
req.headers["X-Electron-App"] === "true";
authLogger.success("TOTP verification successful", {
operation: "totp_verify_success",
userId: userRecord.id,
@@ -3414,12 +3428,9 @@ router.post("/totp/verify-login", async (req, res) => {
userId: userRecord.id,
is_oidc: !!userRecord.isOidc,
totp_enabled: !!userRecord.totpEnabled,
...(isNativeAppRequest(req) ? { token } : {}),
};
if (isElectron) {
response.token = token;
}
const timeoutRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
@@ -3560,9 +3571,14 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
* description: Failed to unlock data.
*/
router.post("/unlock-data", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const authReq = req as AuthenticatedRequest;
const userId = authReq.userId;
const { password } = req.body;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
}
if (!password) {
return res.status(400).json({ error: "Password is required" });
}
@@ -3570,6 +3586,19 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
try {
const unlocked = await authManager.authenticateUser(userId, password);
if (unlocked) {
const refreshedSession =
userId && authReq.sessionId
? await authManager.refreshSessionToken(userId, authReq.sessionId)
: null;
if (refreshedSession) {
res.cookie(
"jwt",
refreshedSession.token,
authManager.getSecureCookieOptions(req, refreshedSession.maxAge),
);
}
res.json({
success: true,
message: "Data unlocked successfully",
@@ -3608,9 +3637,10 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const unlocked = authManager.isUserUnlocked(userId);
res.json({
unlocked: true,
message: "Data is unlocked",
unlocked,
message: unlocked ? "Data is unlocked" : "Data is locked",
});
} catch (err) {
authLogger.error("Failed to check data status", err, {
@@ -3638,7 +3668,9 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
* description: Failed to get sessions.
*/
router.get("/sessions", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const authReq = req as AuthenticatedRequest;
const userId = authReq.userId;
const currentSessionId = authReq.sessionId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
@@ -3661,8 +3693,16 @@ router.get("/sessions", authenticateJWT, async (req, res) => {
.limit(1);
return {
...session,
id: session.id,
userId: session.userId,
username: sessionUser[0]?.username || "Unknown",
deviceType: session.deviceType,
deviceInfo: session.deviceInfo,
createdAt: session.createdAt,
expiresAt: session.expiresAt,
lastActiveAt: session.lastActiveAt,
isRevoked: session.isRevoked,
isCurrentSession: session.id === currentSessionId,
};
}),
);
@@ -3670,7 +3710,19 @@ router.get("/sessions", authenticateJWT, async (req, res) => {
return res.json({ sessions: enrichedSessions });
} else {
sessionList = await authManager.getUserSessions(userId);
return res.json({ sessions: sessionList });
return res.json({
sessions: sessionList.map((session) => ({
id: session.id,
userId: session.userId,
deviceType: session.deviceType,
deviceInfo: session.deviceInfo,
createdAt: session.createdAt,
expiresAt: session.expiresAt,
lastActiveAt: session.lastActiveAt,
isRevoked: session.isRevoked,
isCurrentSession: session.id === currentSessionId,
})),
});
}
} catch (err) {
authLogger.error("Failed to get sessions", err);
@@ -3812,12 +3864,7 @@ router.post("/sessions/revoke-all", authenticateJWT, async (req, res) => {
let currentSessionId: string | undefined;
if (exceptCurrent) {
const token =
req.cookies?.jwt || req.headers?.authorization?.split(" ")[1];
if (token) {
const payload = await authManager.verifyJWTToken(token);
currentSessionId = payload?.sessionId;
}
currentSessionId = (req as AuthenticatedRequest).sessionId;
}
const revokedCount = await authManager.revokeAllUserSessions(
@@ -4205,7 +4252,7 @@ router.post("/unlink-oidc-from-password", authenticateJWT, async (req, res) => {
* 500:
* description: Failed to get guacamole settings.
*/
router.get("/guacamole-settings", async (req, res) => {
router.get("/guacamole-settings", authenticateJWT, async (req, res) => {
try {
const enabledRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
@@ -4305,7 +4352,7 @@ router.patch("/guacamole-settings", authenticateJWT, async (req, res) => {
* 200:
* description: Current log level.
*/
router.get("/log-level", async (_req, res) => {
router.get("/log-level", authenticateJWT, async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'log_level'")
@@ -4374,7 +4421,7 @@ router.patch("/log-level", authenticateJWT, async (req, res) => {
* 200:
* description: Current session timeout hours.
*/
router.get("/session-timeout", async (_req, res) => {
router.get("/session-timeout", authenticateJWT, async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
@@ -4433,4 +4480,209 @@ router.patch("/session-timeout", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /users/api-keys:
* post:
* summary: Create an API key (admin only)
* description: Creates a new API key scoped to a specific user. The full token is returned only once.
* tags:
* - API Keys
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - userId
* properties:
* name:
* type: string
* description: Human-readable name for the key.
* userId:
* type: string
* description: ID of the user this key is scoped to.
* expiresAt:
* type: string
* format: date-time
* description: Optional expiration date. Null means the key never expires.
* responses:
* 201:
* description: API key created. Contains the full token (shown only once).
* 400:
* description: Invalid input.
* 403:
* description: Admin access required.
* 404:
* description: Target user not found.
* 500:
* description: Failed to create API key.
*/
router.post("/api-keys", requireAdmin, async (req, res) => {
try {
const { name, userId: targetUserId, expiresAt } = req.body;
if (typeof name !== "string" || !name.trim()) {
return res.status(400).json({ error: "name is required" });
}
if (typeof targetUserId !== "string" || !targetUserId.trim()) {
return res.status(400).json({ error: "userId is required" });
}
const targetUser = await db
.select()
.from(users)
.where(eq(users.id, targetUserId))
.limit(1);
if (targetUser.length === 0) {
return res.status(404).json({ error: "Target user not found" });
}
let expiresAtValue: string | null = null;
if (expiresAt) {
const parsed = new Date(expiresAt);
if (isNaN(parsed.getTime())) {
return res.status(400).json({ error: "Invalid expiresAt date" });
}
if (parsed <= new Date()) {
return res
.status(400)
.json({ error: "expiresAt must be in the future" });
}
expiresAtValue = parsed.toISOString();
}
const rawToken = "tmx_" + crypto.randomBytes(32).toString("hex");
const tokenPrefix = rawToken.substring(0, 12);
const tokenHash = await bcrypt.hash(rawToken, 10);
const keyId = nanoid();
const now = new Date().toISOString();
await db.insert(apiKeys).values({
id: keyId,
userId: targetUserId,
name: name.trim(),
tokenHash,
tokenPrefix,
createdAt: now,
expiresAt: expiresAtValue,
lastUsedAt: null,
isActive: true,
});
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
await saveMemoryDatabaseToFile();
return res.status(201).json({
id: keyId,
name: name.trim(),
userId: targetUserId,
username: targetUser[0].username,
tokenPrefix,
createdAt: now,
expiresAt: expiresAtValue,
token: rawToken,
});
} catch (err) {
authLogger.error("Failed to create API key", err);
return res.status(500).json({ error: "Failed to create API key" });
}
});
/**
* @openapi
* /users/api-keys:
* get:
* summary: List all API keys (admin only)
* description: Returns all API keys with associated usernames. Token hashes are never returned.
* tags:
* - API Keys
* responses:
* 200:
* description: List of API keys.
* 403:
* description: Admin access required.
* 500:
* description: Failed to fetch API keys.
*/
router.get("/api-keys", requireAdmin, async (_req, res) => {
try {
const keys = await db
.select({
id: apiKeys.id,
name: apiKeys.name,
userId: apiKeys.userId,
username: users.username,
tokenPrefix: apiKeys.tokenPrefix,
createdAt: apiKeys.createdAt,
expiresAt: apiKeys.expiresAt,
lastUsedAt: apiKeys.lastUsedAt,
isActive: apiKeys.isActive,
})
.from(apiKeys)
.leftJoin(users, eq(apiKeys.userId, users.id))
.orderBy(apiKeys.createdAt);
return res.json({ apiKeys: keys });
} catch (err) {
authLogger.error("Failed to list API keys", err);
return res.status(500).json({ error: "Failed to fetch API keys" });
}
});
/**
* @openapi
* /users/api-keys/{keyId}:
* delete:
* summary: Delete an API key (admin only)
* description: Permanently deletes an API key. It can no longer be used to authenticate.
* tags:
* - API Keys
* parameters:
* - in: path
* name: keyId
* required: true
* schema:
* type: string
* description: The ID of the API key to delete.
* responses:
* 200:
* description: API key deleted.
* 403:
* description: Admin access required.
* 404:
* description: API key not found.
* 500:
* description: Failed to delete API key.
*/
router.delete("/api-keys/:keyId", requireAdmin, async (req, res) => {
try {
const keyId = String(req.params.keyId);
const existing = await db
.select()
.from(apiKeys)
.where(eq(apiKeys.id, keyId))
.limit(1);
if (existing.length === 0) {
return res.status(404).json({ error: "API key not found" });
}
await db.delete(apiKeys).where(eq(apiKeys.id, keyId));
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
await saveMemoryDatabaseToFile();
return res.json({ success: true });
} catch (err) {
authLogger.error("Failed to delete API key", err, {
keyId: String(req.params.keyId),
});
return res.status(500).json({ error: "Failed to delete API key" });
}
});
export default router;
+1 -5
View File
@@ -1,12 +1,8 @@
import GuacamoleLite from "guacamole-lite";
import { parse as parseUrl } from "url";
import { guacLogger } from "../utils/logger.js";
import { AuthManager } from "../utils/auth-manager.js";
import { GuacamoleTokenService } from "./token-service.js";
import { getDb } from "../database/db/index.js";
import type { IncomingMessage } from "http";
const authManager = AuthManager.getInstance();
const tokenService = GuacamoleTokenService.getInstance();
function parseGuacUrl(url: string): { host: string; port: number } {
@@ -57,7 +53,7 @@ const clientOptions = {
},
},
allowedUnencryptedConnectionSettings: {
rdp: ["width", "height", "dpi"],
rdp: ["width", "height"],
vnc: ["width", "height"],
telnet: ["width", "height"],
},
+20 -11
View File
@@ -6,7 +6,7 @@ import { PermissionManager } from "../utils/permission-manager.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { getDb } from "../database/db/index.js";
import { hosts } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { eq } from "drizzle-orm";
import type { AuthenticatedRequest } from "../../types/index.js";
const router = express.Router();
@@ -31,7 +31,6 @@ router.use(authManager.createAuthMiddleware());
*/
router.post("/token", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
const { type, hostname, port, username, password, domain, ...options } =
req.body;
@@ -63,10 +62,15 @@ router.post("/token", async (req, res) => {
);
break;
case "vnc":
token = tokenService.createVncToken(hostname, password, {
port: port || 5900,
...options,
});
token = tokenService.createVncToken(
hostname,
username || undefined,
password,
{
port: port || 5900,
...options,
},
);
break;
case "telnet":
token = tokenService.createTelnetToken(hostname, username, password, {
@@ -129,7 +133,7 @@ router.post(
async (req: express.Request, res: express.Response) => {
try {
const userId = (req as AuthenticatedRequest).userId!;
const hostId = parseInt(req.params.hostId, 10);
const hostId = Number.parseInt(String(req.params.hostId), 10);
if (!hostId || isNaN(hostId)) {
return res.status(400).json({ error: "Invalid host ID" });
@@ -206,10 +210,15 @@ router.post(
});
break;
case "vnc":
token = tokenService.createVncToken(hostname, password, {
port: port || 5900,
...guacConfig,
});
token = tokenService.createVncToken(
hostname,
username || undefined,
password,
{
port: port || 5900,
...guacConfig,
},
);
break;
case "telnet":
token = tokenService.createTelnetToken(hostname, username, password, {
+2 -1
View File
@@ -130,7 +130,6 @@ export class GuacamoleTokenService {
username,
password,
port: 3389,
security: "nla",
"ignore-cert": true,
...options,
},
@@ -141,6 +140,7 @@ export class GuacamoleTokenService {
createVncToken(
hostname: string,
username?: string,
password?: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
): string {
@@ -149,6 +149,7 @@ export class GuacamoleTokenService {
type: "vnc",
settings: {
hostname,
...(username ? { username } : {}),
password,
port: 5900,
...options,
+20 -13
View File
@@ -1,6 +1,5 @@
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 { hosts, sshCredentials } from "../database/db/schema.js";
import { and, eq } from "drizzle-orm";
@@ -27,14 +26,18 @@ const wss = new WebSocketServer({
port: 30009,
verifyClient: async (info) => {
try {
const url = parseUrl(info.req.url || "", true);
let token = url.query.token as string;
let token: string | undefined;
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
if (!token) {
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
const authHeader = info.req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice("Bearer ".length);
}
}
@@ -239,14 +242,18 @@ async function createJumpHostChain(
}
wss.on("connection", async (ws: WebSocket, req) => {
const url = parseUrl(req.url || "", true);
let token = url.query.token as string;
let token: string | undefined;
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
if (!token) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice("Bearer ".length);
}
}
+1 -1
View File
@@ -971,7 +971,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
userId,
});
let errorStage: ConnectionStage = "error";
let errorStage: ConnectionStage;
if (
err.message.includes("ENOTFOUND") ||
err.message.includes("getaddrinfo")
+348 -132
View File
@@ -361,6 +361,31 @@ async function createJumpHostChain(
}
}
// Serializes SSH channel open requests so only one channel negotiation is
// in-flight at a time per session. Once the channel is established the slot
// is released immediately so the next open can proceed; the channels
// themselves remain open concurrently (one exec per command is short-lived,
// the SFTP channel is long-lived but only opened once).
class ChannelOpenSerializer {
private tail: Promise<void> = Promise.resolve();
// Enqueue an action that opens a channel. The action runs after the previous
// one completes (success or failure). Returns a promise that resolves with
// the action's result.
run<T>(action: () => Promise<T>): Promise<T> {
const next = this.tail.then(
() => action(),
() => action(), // run even if the previous open failed
);
// Advance tail past this slot (swallow result so the chain keeps going)
this.tail = next.then(
() => {},
() => {},
);
return next;
}
}
interface SSHSession {
client: SSHClient;
isConnected: boolean;
@@ -369,6 +394,8 @@ interface SSHSession {
activeOperations: number;
sudoPassword?: string;
sftp?: import("ssh2").SFTPWrapper;
sftpPending?: Promise<import("ssh2").SFTPWrapper>;
channelOpener: ChannelOpenSerializer;
poolKey?: string;
userId?: string;
}
@@ -393,9 +420,11 @@ interface PendingTOTPSession {
const sshSessions: Record<string, SSHSession> = {};
const pendingTOTPSessions: Record<string, PendingTOTPSession> = {};
// Keyed by "sessionId:path" to prevent concurrent requests for the same path
const activeListRequests: Record<string, boolean> = {};
function execWithSudo(
client: SSHClient,
session: SSHSession,
command: string,
sudoPassword: string,
): Promise<{ stdout: string; stderr: string; code: number }> {
@@ -403,7 +432,7 @@ function execWithSudo(
const escapedPassword = sudoPassword.replace(/'/g, "'\"'\"'");
const sudoCommand = `echo '${escapedPassword}' | sudo -S ${command} 2>&1`;
client.exec(sudoCommand, (err, stream) => {
execChannel(session, sudoCommand, (err, stream) => {
if (err) {
resolve({ stdout: "", stderr: err.message, code: 1 });
return;
@@ -438,21 +467,76 @@ function getSessionSftp(
if (session.sftp) {
return Promise.resolve(session.sftp);
}
return new Promise((resolve, reject) => {
session.client.sftp((err, sftp) => {
if (err) {
return reject(err);
// Serialization: if a channel open is already in flight, join it
if (session.sftpPending) {
return session.sftpPending;
}
const openOnce = (): Promise<import("ssh2").SFTPWrapper> =>
session.channelOpener.run(
() =>
new Promise<import("ssh2").SFTPWrapper>((resolve, reject) => {
session.client.sftp((err, sftp) => {
if (err) return reject(err);
session.sftp = sftp;
sftp.on("error", () => {
session.sftp = undefined;
});
sftp.on("close", () => {
session.sftp = undefined;
});
resolve(sftp);
});
}),
);
session.sftpPending = openOnce()
.catch((err: Error) => {
const isChannelFailure =
err.message.toLowerCase().includes("channel open failure") ||
err.message.toLowerCase().includes("open failed");
if (isChannelFailure) {
// Single retry after 500ms for transient server-side rate limiting
return new Promise<import("ssh2").SFTPWrapper>((resolve, reject) =>
setTimeout(() => openOnce().then(resolve, reject), 500),
);
}
session.sftp = sftp;
sftp.on("error", () => {
session.sftp = undefined;
});
sftp.on("close", () => {
session.sftp = undefined;
});
resolve(sftp);
return Promise.reject(err);
})
.finally(() => {
session.sftpPending = undefined;
});
});
return session.sftpPending;
}
// Wraps client.exec through the channel serializer so only one SSH channel
// negotiation is in-flight at a time. The serializer slot is released as soon
// as the channel is established (not when it closes), so channels run
// concurrently once open — we only serialize the *open handshake*.
function execChannel(
session: SSHSession,
command: string,
callback: (
err: Error | undefined,
stream: import("ssh2").ClientChannel,
) => void,
): void {
session.channelOpener
.run(
() =>
new Promise<import("ssh2").ClientChannel>((resolve, reject) => {
session.client.exec(command, (err, stream) => {
if (err) return reject(err);
resolve(stream);
});
}),
)
.then(
(stream) => callback(undefined, stream),
(err: Error) => callback(err, undefined as never),
);
}
function cleanupSession(sessionId: string) {
@@ -840,11 +924,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
port,
username,
tryKeyboard: true,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
keepaliveInterval: 10000,
keepaliveCountMax: 5,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
tcpKeepAliveInitialDelay: 5000,
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
hostId,
ip,
@@ -949,7 +1033,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
.json({ error: "Invalid SSH key format", connectionLogs });
}
} else if (resolvedCredentials.authType === "password") {
if (!resolvedCredentials.password || !resolvedCredentials.password.trim()) {
if (!resolvedCredentials.password) {
connectionLogs.push(
createConnectionLog(
"error",
@@ -1108,6 +1192,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
channelOpener: new ChannelOpenSerializer(),
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1173,7 +1258,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
error: err.message,
});
let errorStage: ConnectionStage = "error";
let errorStage: ConnectionStage;
if (
err.message.includes("ENOTFOUND") ||
err.message.includes("getaddrinfo")
@@ -1752,6 +1837,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
channelOpener: new ChannelOpenSerializer(),
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1954,6 +2040,7 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
channelOpener: new ChannelOpenSerializer(),
userId,
};
scheduleSessionCleanup(sessionId);
@@ -2048,6 +2135,10 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
const session = sshSessions[sessionId];
if (session && !verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
fileLogger.info("File manager disconnection requested", {
operation: "file_disconnect_request",
sessionId,
@@ -2149,7 +2240,7 @@ app.get("/ssh/file_manager/ssh/status", (req, res) => {
* 400:
* description: Session ID is required or session not found.
*/
app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
app.post("/ssh/file_manager/ssh/keepalive", async (req, res) => {
const { sessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2173,6 +2264,18 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
session.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
// Probe the cached SFTP channel. If stale, clear it so the next operation
// opens a fresh one via the serialized getSessionSftp.
if (session.sftp && !session.sftpPending) {
try {
await new Promise<void>((resolve, reject) => {
session.sftp!.stat("/", (err) => (err ? reject(err) : resolve()));
});
} catch {
session.sftp = undefined;
}
}
res.json({
status: "success",
connected: true,
@@ -2226,6 +2329,17 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
return res.status(403).json({ error: "Session access denied" });
}
// Drop concurrent requests for the same session+path — each would open
// a new SSH channel and can exceed the server's per-connection channel limit.
const listKey = `${sessionId}:${sshPath}`;
if (activeListRequests[listKey]) {
return res.status(409).json({ error: "List request already in progress" });
}
activeListRequests[listKey] = true;
res.on("finish", () => {
delete activeListRequests[listKey];
});
sshConn.lastActive = Date.now();
sshConn.activeOperations++;
const trySFTP = () => {
@@ -2327,6 +2441,13 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
fileLogger.warn(
`SFTP failed for listFiles, trying fallback: ${err.message}`,
);
const isChannelFailure =
err.message.toLowerCase().includes("channel open failure") ||
err.message.toLowerCase().includes("open failed");
if (isChannelFailure) {
sshConn.isConnected = false;
sshConn.sftp = undefined;
}
tryFallbackMethod();
});
} catch (sftpErr: unknown) {
@@ -2340,11 +2461,14 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
return res
.status(503)
.json({ error: "SSH session disconnected", disconnected: true });
}
try {
const escapedPath = sshPath.replace(/'/g, "'\"'\"'");
sshConn.client.exec(
execChannel(
sshConn,
`command ls -la --color=never '${escapedPath}'`,
(err, stream) => {
if (err) {
@@ -2472,7 +2596,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const escapedPassword = sshConn.sudoPassword!.replace(/'/g, "'\"'\"'");
const sudoCommand = `echo '${escapedPassword}' | sudo -S /bin/ls -la --color=never '${escapedPath}' 2>&1`;
sshConn.client.exec(sudoCommand, (err, stream) => {
execChannel(sshConn, sudoCommand, (err, stream) => {
if (err) {
sshConn.activeOperations--;
fileLogger.error("SSH sudo listFiles error:", err);
@@ -2628,6 +2752,7 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
const linkPath = decodeURIComponent(req.query.path as string);
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2637,6 +2762,10 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!linkPath) {
return res.status(400).json({ error: "Link path is required" });
}
@@ -2646,7 +2775,7 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
const escapedPath = linkPath.replace(/'/g, "'\"'\"'");
const command = `stat -L -c "%F" '${escapedPath}' && readlink -f '${escapedPath}'`;
sshConn.client.exec(command, (err, stream) => {
execChannel(sshConn, command, (err, stream) => {
if (err) {
fileLogger.error("SSH identifySymlink error:", err);
return res.status(500).json({ error: err.message });
@@ -2722,6 +2851,7 @@ app.get("/ssh/file_manager/ssh/resolvePath", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
const rawPath = decodeURIComponent(req.query.path as string);
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2731,21 +2861,26 @@ app.get("/ssh/file_manager/ssh/resolvePath", (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!rawPath) {
return res.status(400).json({ error: "Path is required" });
}
sshConn.lastActive = Date.now();
let expandPath = rawPath;
if (expandPath.startsWith("~")) {
expandPath = "$HOME" + expandPath.substring(1);
let command: string;
if (rawPath.startsWith("~")) {
const rest = rawPath.substring(1).replace(/'/g, "'\"'\"'");
command = `echo ~'${rest}'`;
} else {
const escapedPath = rawPath.replace(/'/g, "'\"'\"'");
command = `echo '${escapedPath}'`;
}
const escapedPath = expandPath.replace(/"/g, '\\"');
const command = `echo "${escapedPath}"`;
sshConn.client.exec(command, (err, stream) => {
execChannel(sshConn, command, (err, stream) => {
if (err) {
fileLogger.error("SSH resolvePath error:", err);
return res.status(500).json({ error: err.message });
@@ -2826,6 +2961,10 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!filePath) {
return res.status(400).json({ error: "File path is required" });
}
@@ -2841,7 +2980,8 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const MAX_READ_SIZE = 500 * 1024 * 1024;
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
sshConn.client.exec(
execChannel(
sshConn,
`stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`,
(sizeErr, sizeStream) => {
if (sizeErr) {
@@ -2899,7 +3039,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
});
}
sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => {
execChannel(sshConn, `cat '${escapedPath}'`, (err, stream) => {
if (err) {
fileLogger.error("SSH readFile error:", err);
return res.status(500).json({ error: err.message });
@@ -3006,6 +3146,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!filePath) {
return res.status(400).json({ error: "File path is required" });
}
@@ -3067,7 +3211,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
const chmodCommand = `chmod ${permissions} '${escapedPath}' && echo "SUCCESS"`;
sshConn.client.exec(chmodCommand, (err, stream) => {
execChannel(sshConn, chmodCommand, (err, stream) => {
if (err) {
fileLogger.warn("Failed to restore file permissions after save", {
operation: "file_write_restore_permissions",
@@ -3170,83 +3314,90 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
}
sftp.stat(filePath, (statErr, stats) => {
if (statErr) {
fileLogger.warn(
"Failed to read existing file permissions before save",
{
operation: "file_write_stat",
try {
if (statErr) {
fileLogger.warn(
"Failed to read existing file permissions before save",
{
operation: "file_write_stat",
sessionId,
userId,
path: filePath,
error: statErr.message,
},
);
} else if (stats.isFile()) {
preservedMode = stats.mode & 0o7777;
}
const writeStream = sftp.createWriteStream(filePath);
let hasError = false;
let hasFinished = false;
let isFinalizing = false;
const finalizeSuccess = () => {
if (hasError || hasFinished) return;
hasFinished = true;
isFinalizing = false;
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
error: statErr.message,
},
);
} else if (stats.isFile()) {
preservedMode = stats.mode & 0o7777;
}
const writeStream = sftp.createWriteStream(filePath);
let hasError = false;
let hasFinished = false;
let isFinalizing = false;
const finalizeSuccess = () => {
if (hasError || hasFinished) return;
hasFinished = true;
isFinalizing = false;
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
});
}
};
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished || isFinalizing) return;
hasError = true;
isFinalizing = false;
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
);
tryFallbackMethod();
});
const finishWrite = () => {
if (hasError || hasFinished || isFinalizing) return;
isFinalizing = true;
restoreOriginalMode(sftp, finalizeSuccess);
};
writeStream.on("finish", () => {
finishWrite();
});
writeStream.on("close", () => {
finishWrite();
});
try {
writeStream.write(fileBuffer);
writeStream.end();
} catch (writeErr) {
if (hasError || hasFinished) return;
hasError = true;
isFinalizing = false;
fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${(writeErr as Error).message}`,
);
tryFallbackMethod();
}
};
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished || isFinalizing) return;
hasError = true;
isFinalizing = false;
} catch (callbackErr) {
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
);
tryFallbackMethod();
});
const finishWrite = () => {
if (hasError || hasFinished || isFinalizing) return;
isFinalizing = true;
restoreOriginalMode(sftp, finalizeSuccess);
};
writeStream.on("finish", () => {
finishWrite();
});
writeStream.on("close", () => {
finishWrite();
});
try {
writeStream.write(fileBuffer);
writeStream.end();
} catch (writeErr) {
if (hasError || hasFinished) return;
hasError = true;
isFinalizing = false;
fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${writeErr.message}`,
`SFTP stat callback error, trying fallback method: ${(callbackErr as Error).message}`,
);
tryFallbackMethod();
}
@@ -3268,8 +3419,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
if (!res.headersSent) {
return res.status(500).json({ error: "SSH session disconnected" });
}
return;
}
try {
let contentBuffer: Buffer;
@@ -3292,7 +3445,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
sshConn.client.exec(writeCommand, (err, stream) => {
execChannel(sshConn, writeCommand, (err, stream) => {
if (err) {
fileLogger.error("Fallback write command failed:", err);
if (!res.headersSent) {
@@ -3318,6 +3471,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
errorData += chunk.toString();
});
stream.stderr.on("error", (stderrErr) => {
fileLogger.error("Fallback write stderr error:", stderrErr);
});
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
restoreOriginalMode(null, () => {
@@ -3357,9 +3514,9 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
} catch (fallbackErr) {
fileLogger.error("Fallback method failed:", fallbackErr);
if (!res.headersSent) {
res
.status(500)
.json({ error: `All write methods failed: ${fallbackErr.message}` });
res.status(500).json({
error: `All write methods failed: ${(fallbackErr as Error).message}`,
});
}
}
};
@@ -3411,6 +3568,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!filePath || !fileName || content === undefined) {
return res
.status(400)
@@ -3560,8 +3721,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
if (!res.headersSent) {
return res.status(500).json({ error: "SSH session disconnected" });
}
return;
}
try {
let contentBuffer: Buffer;
@@ -3606,7 +3769,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
sshConn.client.exec(writeCommand, (err, stream) => {
execChannel(sshConn, writeCommand, (err, stream) => {
if (err) {
fileLogger.error("Fallback upload command failed:", err);
if (!res.headersSent) {
@@ -3680,7 +3843,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
writeCommand += ` && echo "SUCCESS"`;
sshConn.client.exec(writeCommand, (err, stream) => {
execChannel(sshConn, writeCommand, (err, stream) => {
if (err) {
fileLogger.error("Chunked fallback upload failed:", err);
if (!res.headersSent) {
@@ -3797,6 +3960,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const { sessionId, path: filePath, fileName } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -3806,6 +3970,10 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!filePath || !fileName) {
return res.status(400).json({ error: "File path and name are required" });
}
@@ -3819,7 +3987,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`;
sshConn.client.exec(createCommand, (err, stream) => {
execChannel(sshConn, createCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH createFile error:", err);
if (!res.headersSent) {
@@ -3939,6 +4107,10 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!folderPath || !folderName) {
return res.status(400).json({ error: "Folder path and name are required" });
}
@@ -3958,7 +4130,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
sshConn.client.exec(createCommand, (err, stream) => {
execChannel(sshConn, createCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH createFolder error:", err);
if (!res.headersSent) {
@@ -4090,6 +4262,10 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!itemPath) {
return res.status(400).json({ error: "Item path is required" });
}
@@ -4111,7 +4287,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const executeDelete = (useSudo: boolean): Promise<void> => {
return new Promise((resolve) => {
if (useSudo && sshConn.sudoPassword) {
execWithSudo(sshConn.client, deleteCommand, sshConn.sudoPassword).then(
execWithSudo(sshConn, deleteCommand, sshConn.sudoPassword).then(
(result) => {
if (
result.code === 0 ||
@@ -4137,7 +4313,8 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
return;
}
sshConn.client.exec(
execChannel(
sshConn,
`${deleteCommand} && echo "SUCCESS"`,
(err, stream) => {
if (err) {
@@ -4259,6 +4436,10 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!oldPath || !newName) {
return res
.status(400)
@@ -4281,7 +4462,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const renameCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`;
sshConn.client.exec(renameCommand, (err, stream) => {
execChannel(sshConn, renameCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH renameItem error:", err);
if (!res.headersSent) {
@@ -4412,6 +4593,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
const { sessionId, oldPath, newPath } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -4421,6 +4603,10 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!oldPath || !newPath) {
return res
.status(400)
@@ -4446,7 +4632,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
}
}, 60000);
sshConn.client.exec(moveCommand, (err, stream) => {
execChannel(sshConn, moveCommand, (err, stream) => {
if (err) {
clearTimeout(commandTimeout);
fileLogger.error("SSH moveItem error:", err);
@@ -4566,7 +4752,8 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
* description: Failed to download file.
*/
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
const { sessionId, path: filePath, hostId, userId } = req.body;
const { sessionId, path: filePath, hostId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
const downloadStartTime = Date.now();
if (!sessionId || !filePath) {
@@ -4597,6 +4784,10 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
.json({ error: "SSH session not found or not connected" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
sshConn.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
fileLogger.info("Opening SFTP channel", {
@@ -4713,7 +4904,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
* description: Failed to copy item.
*/
app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const { sessionId, sourcePath, targetDir, hostId, userId } = req.body;
const { sessionId, sourcePath, targetDir, hostId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId || !sourcePath || !targetDir) {
return res.status(400).json({ error: "Missing required parameters" });
@@ -4726,6 +4918,10 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
.json({ error: "SSH session not found or not connected" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
sshConn.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
@@ -4757,7 +4953,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
}
}, 60000);
sshConn.client.exec(copyCommand, (err, stream) => {
execChannel(sshConn, copyCommand, (err, stream) => {
if (err) {
clearTimeout(commandTimeout);
fileLogger.error("SSH copyItem error:", err);
@@ -4905,6 +5101,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const { sessionId, filePath } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sshConn || !sshConn.isConnected) {
fileLogger.error(
@@ -4919,6 +5116,10 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
return res.status(400).json({ error: "SSH connection not available" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!filePath) {
return res.status(400).json({ error: "File path is required" });
}
@@ -4927,7 +5128,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const checkCommand = `test -x '${escapedPath}' && echo "EXECUTABLE" || echo "NOT_EXECUTABLE"`;
sshConn.client.exec(checkCommand, (checkErr, checkStream) => {
execChannel(sshConn, checkCommand, (checkErr, checkStream) => {
if (checkErr) {
fileLogger.error("SSH executeFile check error:", checkErr);
return res
@@ -4947,7 +5148,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const executeCommand = `cd "$(dirname '${escapedPath}')" && '${escapedPath}' 2>&1; echo "EXIT_CODE:$?"`;
sshConn.client.exec(executeCommand, (err, stream) => {
execChannel(sshConn, executeCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH executeFile error:", err);
return res.status(500).json({ error: "Failed to execute file" });
@@ -5034,6 +5235,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
const { sessionId, path, permissions } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sshConn || !sshConn.isConnected) {
fileLogger.error(
@@ -5048,6 +5250,10 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
return res.status(400).json({ error: "SSH connection not available" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
if (!path) {
return res.status(400).json({ error: "File path is required" });
}
@@ -5086,7 +5292,7 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
}
}, 10000);
sshConn.client.exec(command, (err, stream) => {
execChannel(sshConn, command, (err, stream) => {
if (err) {
clearTimeout(commandTimeout);
fileLogger.error("SSH changePermissions exec error:", err, {
@@ -5232,6 +5438,7 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
*/
app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
const { sessionId, archivePath, extractPath } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId || !archivePath) {
return res.status(400).json({ error: "Missing required parameters" });
@@ -5242,13 +5449,17 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
return res.status(400).json({ error: "SSH session not connected" });
}
if (!verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
session.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
const fileName = archivePath.split("/").pop() || "";
const fileExt = fileName.toLowerCase();
let extractCommand = "";
let extractCommand: string;
const targetPath =
extractPath || archivePath.substring(0, archivePath.lastIndexOf("/"));
@@ -5290,7 +5501,7 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
command: extractCommand,
});
session.client.exec(extractCommand, (err, stream) => {
execChannel(session, extractCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH exec error during extract:", err, {
operation: "extract_archive",
@@ -5438,6 +5649,7 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
*/
app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
const { sessionId, paths, archiveName, format } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (
!sessionId ||
@@ -5454,11 +5666,15 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
return res.status(400).json({ error: "SSH session not connected" });
}
if (!verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
session.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
const compressionFormat = format || "zip";
let compressCommand = "";
let compressCommand: string;
const firstPath = paths[0];
const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/";
@@ -5509,7 +5725,7 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
command: compressCommand,
});
session.client.exec(compressCommand, (err, stream) => {
execChannel(session, compressCommand, (err, stream) => {
if (err) {
fileLogger.error("SSH exec error during compress:", err, {
operation: "compress_files",
-2
View File
@@ -1,7 +1,6 @@
import { spawn, ChildProcess } from "child_process";
import { randomUUID } from "crypto";
import { WebSocket } from "ws";
import { IncomingMessage } from "http";
import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js";
import { sshLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.js";
@@ -13,7 +12,6 @@ import { promises as fs } from "fs";
import path from "path";
import axios from "axios";
import yaml from "js-yaml";
import { getRequestOrigin } from "../utils/request-origin.js";
const AUTH_TIMEOUT = 60 * 1000;
+74 -12
View File
@@ -4,12 +4,64 @@
// DER → SSH wire format, and patches Protocol.authPK to use the base
// algorithm in the signature wrapper (required by OpenSSH's sshkey_check_sigtype).
import type { Client, ConnectConfig } from "ssh2";
import type {
AnyAuthMethod,
AuthHandlerMiddleware,
AuthenticationType,
Client,
ConnectConfig,
PublicKeyAuthMethod,
} from "ssh2";
interface OPKSSHToken {
privateKey: string;
sshCert: string;
}
type SignCallback = (
data: Buffer,
callback: (signature: Buffer) => void,
) => void;
interface ParsedPrivateKey {
type: string;
sign: (data: Buffer, algo?: string) => Buffer | Error;
getPublicSSH: () => Buffer;
[key: symbol]: unknown;
}
interface OPKSSHProtocol {
authPK: (
user: string,
pubKey: ParsedPrivateKey,
keyAlgo: string | undefined,
cbSign?: SignCallback,
) => unknown;
_kex: {
sessionID: Buffer;
};
_packetRW: {
write: {
alloc: (payloadLength: number) => Buffer;
allocStart: number;
finalize: (packet: Buffer) => Buffer;
};
};
_authsQueue: string[];
_debug?: (message: string) => void;
_cipher: {
encrypt: (packet: Buffer) => void;
};
}
type OPKSSHClient = Client & {
_protocol?: OPKSSHProtocol;
};
type OPKSSHNextAuthHandler = (
authInfo: AuthenticationType | AnyAuthMethod | false,
) => void;
export async function setupOPKSSHCertAuth(
config: ConnectConfig,
client: Client,
@@ -26,7 +78,9 @@ export async function setupOPKSSHCertAuth(
if (parsed instanceof Error || !parsed) {
throw new Error("Failed to parse OPKSSH private key");
}
const privKey: any = Array.isArray(parsed) ? parsed[0] : parsed;
const privKey = (
Array.isArray(parsed) ? parsed[0] : parsed
) as ParsedPrivateKey;
// Extract cert type and blob from the stored certificate
const certParts = token.sshCert.trim().split(/\s+/);
@@ -78,36 +132,43 @@ export async function setupOPKSSHCertAuth(
// Set up authHandler to bypass ssh2's cert type rejection
let certAuthAttempted = false;
config.authHandler = (
const authHandler: AuthHandlerMiddleware = (
methodsLeft: string[],
_partialSuccess: boolean,
callback: (authInfo: any) => void,
callback,
) => {
const next = callback as OPKSSHNextAuthHandler;
if (
!certAuthAttempted &&
(!methodsLeft || methodsLeft.includes("publickey"))
) {
certAuthAttempted = true;
callback({ type: "publickey", username, key: privKey });
next({
type: "publickey",
username,
key: privKey as unknown as PublicKeyAuthMethod["key"],
});
} else {
callback(false);
next(false);
}
};
config.authHandler = authHandler;
// Monkey-patch Protocol.authPK after connect() to fix the signature
// wrapper algorithm for cert types.
const baseAlgo = certType.replace(/-cert-v\d+@openssh\.com$/, "");
const origConnect = client.connect.bind(client);
(client as any).connect = (cfg: any) => {
origConnect(cfg);
const proto = (client as any)._protocol;
if (!proto) return;
const patchedClient = client as OPKSSHClient;
patchedClient.connect = (cfg: ConnectConfig) => {
const connectedClient = origConnect(cfg);
const proto = patchedClient._protocol;
if (!proto) return connectedClient;
const origAuthPK = proto.authPK.bind(proto);
proto.authPK = (
user: string,
pubKey: any,
pubKey: ParsedPrivateKey,
keyAlgo: string | undefined,
cbSign?: Function,
cbSign?: SignCallback,
) => {
const isCertAuth = !!cbSign && pubKey?.type?.includes("-cert-");
if (!isCertAuth) {
@@ -232,5 +293,6 @@ export async function setupOPKSSHCertAuth(
proto._cipher.encrypt(finalized);
});
};
return connectedClient;
};
}
+7 -7
View File
@@ -1088,8 +1088,6 @@ class PollingManager {
for (const { host, viewerUserId } of hostsToRefresh) {
await this.startPollingForHost(host, { statusOnly: true, viewerUserId });
}
const skipped = this.pollingConfigs.size - hostsToRefresh.length;
}
registerViewer(hostId: number, sessionId: string, userId: string): void {
@@ -1331,9 +1329,8 @@ async function resolveHostCredentials(
const isSharedHost = userId !== ownerId;
if (isSharedHost) {
const { SharedCredentialManager } = await import(
"../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id as number,
@@ -1552,7 +1549,9 @@ async function buildSshConfig(
statsLogger.error(
`SSH key format error for host ${host.ip}: ${keyError instanceof Error ? keyError.message : "Unknown error"}`,
);
throw new Error(`Invalid SSH key format for host ${host.ip}`);
throw new Error(`Invalid SSH key format for host ${host.ip}`, {
cause: keyError,
});
}
} else if (host.authType === "none") {
// no credentials needed
@@ -1654,6 +1653,7 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise<Client> {
(proxyError instanceof Error
? proxyError.message
: "Unknown error"),
{ cause: proxyError },
);
}
}
@@ -2581,7 +2581,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
const errorMessage =
error instanceof Error ? error.message : String(error);
let errorStage: ConnectionStage = "error";
let errorStage: ConnectionStage;
if (
errorMessage.includes("ENOTFOUND") ||
+162 -31
View File
@@ -3,7 +3,6 @@ import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
import net from "net";
import dgram from "dgram";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { parse as parseUrl } from "url";
import axios from "axios";
import { getDb } from "../database/db/index.js";
import { sshCredentials, hosts } from "../database/db/schema.js";
@@ -367,14 +366,18 @@ const wss = new WebSocketServer({
port: 30002,
verifyClient: async (info) => {
try {
const url = parseUrl(info.req.url!, true);
let token = url.query.token as string;
let token: string | undefined;
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
if (!token) {
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
const authHeader = info.req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice("Bearer ".length);
}
}
@@ -414,14 +417,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sessionId: string | undefined;
try {
const url = parseUrl(req.url!, true);
let token = url.query.token as string;
let token: string | undefined;
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
if (!token) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice("Bearer ".length);
}
}
@@ -487,6 +494,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
let isConnecting = false;
let isConnected = false;
let isCleaningUp = false;
let cwdPending = false;
let cwdBuffer = "";
let isShellInitializing = false;
let warpgateAuthPromptSent = false;
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
@@ -590,6 +599,22 @@ wss.on("connection", async (ws: WebSocket, req) => {
connectData.hostConfig.userId = userId;
}
handleConnectToHost(connectData).catch((error) => {
const errMsg =
error instanceof Error ? error.message : "Unknown error";
if (
errMsg.includes("Cannot parse privateKey") &&
errMsg.includes("no passphrase")
) {
isAwaitingAuthCredentials = true;
ws.send(
JSON.stringify({
type: "passphrase_required",
message:
"The SSH key is encrypted. Please enter the passphrase to unlock it.",
}),
);
return;
}
sshLogger.error("Failed to connect to host", error, {
operation: "ssh_connect",
userId,
@@ -599,9 +624,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.send(
JSON.stringify({
type: "error",
message:
"Failed to connect to host: " +
(error instanceof Error ? error.message : "Unknown error"),
message: "Failed to connect to host: " + errMsg,
}),
);
});
@@ -729,6 +752,21 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshStream = null;
break;
case "get_cwd": {
const activeStream =
sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream;
if (!activeStream) {
ws.send(JSON.stringify({ type: "cwd", path: "/" }));
break;
}
cwdPending = true;
cwdBuffer = "";
// Split the sentinel across shell variables so the echoed command
// itself never contains "TERMIX_CWD:" — only the output line does.
activeStream.write('a=TERMIX_CWD; echo "$a:$(pwd)"\r');
break;
}
case "input": {
const inputData = data as string;
const inputStream =
@@ -898,6 +936,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
credentialsData.hostConfig.key = credentialsData.sshKey;
credentialsData.hostConfig.keyPassword = credentialsData.keyPassword;
credentialsData.hostConfig.authType = "key";
} else if (credentialsData.keyPassword) {
credentialsData.hostConfig.keyPassword = credentialsData.keyPassword;
}
isAwaitingAuthCredentials = false;
@@ -916,6 +956,22 @@ wss.on("connection", async (ws: WebSocket, req) => {
};
handleConnectToHost(reconnectData).catch((error) => {
const errMsg =
error instanceof Error ? error.message : "Unknown error";
if (
errMsg.includes("Cannot parse privateKey") &&
errMsg.includes("no passphrase")
) {
isAwaitingAuthCredentials = true;
ws.send(
JSON.stringify({
type: "passphrase_required",
message:
"The SSH key is encrypted. Please enter the passphrase to unlock it.",
}),
);
return;
}
sshLogger.error("Failed to reconnect with credentials", error, {
operation: "ssh_reconnect_with_credentials",
userId,
@@ -925,9 +981,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.send(
JSON.stringify({
type: "error",
message:
"Failed to connect with provided credentials: " +
(error instanceof Error ? error.message : "Unknown error"),
message: "Failed to connect with provided credentials: " + errMsg,
}),
);
});
@@ -1196,7 +1250,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
username: resolvedHost.username || username,
password: resolvedHost.password,
key: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyPassword: keyPassword || resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authType: resolvedHost.authType,
};
@@ -1222,7 +1276,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
username: resolvedHost.username || username,
password: resolvedHost.password,
key: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
// Preserve user-supplied keyPassword (e.g. from passphrase dialog) over the empty DB value
keyPassword: keyPassword || resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authType: resolvedHost.authType,
};
@@ -1439,9 +1494,47 @@ wss.on("connection", async (ws: WebSocket, req) => {
const boundSessionId = currentSessionId;
const CWD_SENTINEL = "TERMIX_CWD:";
stream.on("data", (data: Buffer) => {
try {
const utf8String = data.toString("utf-8");
let utf8String = data.toString("utf-8");
if (cwdPending) {
cwdBuffer += utf8String;
const sentinelIdx = cwdBuffer.indexOf(CWD_SENTINEL);
if (sentinelIdx !== -1) {
const afterSentinel = cwdBuffer.slice(
sentinelIdx + CWD_SENTINEL.length,
);
const newlineIdx = afterSentinel.search(/[\r\n]/);
if (newlineIdx !== -1) {
const cwd =
afterSentinel.slice(0, newlineIdx).trim() || "/";
cwdPending = false;
// Strip the sentinel line from output sent to terminal
const beforeSentinel = cwdBuffer.slice(0, sentinelIdx);
const afterNewline = afterSentinel.slice(newlineIdx);
utf8String = beforeSentinel + afterNewline;
cwdBuffer = "";
const attachedWs =
sessionManager.getSession(boundSessionId)?.attachedWs ??
ws;
if (attachedWs.readyState === WebSocket.OPEN) {
attachedWs.send(
JSON.stringify({ type: "cwd", path: cwd }),
);
}
} else {
return;
}
} else {
return;
}
}
if (!utf8String) return;
const session = sessionManager.getSession(boundSessionId);
if (session) {
sessionManager.bufferOutput(boundSessionId!, utf8String);
@@ -1472,15 +1565,24 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
stream.on("close", () => {
stream.on("close", (code: number | null) => {
const session = sessionManager.getSession(boundSessionId);
if (session?.attachedWs?.readyState === WebSocket.OPEN) {
session.attachedWs.send(
JSON.stringify({
type: "disconnected",
message: "Connection lost",
}),
);
if (code != null) {
session.attachedWs.send(
JSON.stringify({
type: "session_ended",
code,
}),
);
} else {
session.attachedWs.send(
JSON.stringify({
type: "disconnected",
message: "Connection lost",
}),
);
}
}
if (boundSessionId) {
sessionManager.destroySession(boundSessionId);
@@ -1740,6 +1842,31 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
if (
err.message.includes("Cannot parse privateKey") &&
err.message.includes("no passphrase")
) {
sendLog(
"auth",
"error",
"SSH key is encrypted but no passphrase was provided",
);
isAwaitingAuthCredentials = true;
if (currentSessionId) {
sessionManager.destroySession(currentSessionId);
currentSessionId = null;
}
cleanupAuthState(connectionTimeout);
ws.send(
JSON.stringify({
type: "passphrase_required",
message:
"The SSH key is encrypted. Please enter the passphrase to unlock it.",
}),
);
return;
}
if (
authMethodNotAvailable &&
resolvedCredentials.authType === "none" &&
@@ -1913,7 +2040,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}),
);
}
} else if (!sshStream) {
} else {
if (ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
@@ -2102,6 +2229,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (resolvedCredentials.keyPassword) {
connectConfig.passphrase = resolvedCredentials.keyPassword;
}
if (resolvedCredentials.password) {
connectConfig.password = resolvedCredentials.password;
}
} catch (keyError) {
sshLogger.error("SSH key format error: " + keyError.message);
ws.send(
@@ -2191,7 +2322,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
{ operation: "port_knock", hostId: hostConfig.id },
);
await performPortKnocking(hostConfig.ip, hostConfig.portKnockSequence);
} catch (err) {
} catch {
sshLogger.warn("Port knocking failed, attempting connection anyway", {
operation: "port_knock",
hostId: hostConfig.id,
+1240 -256
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -79,7 +79,6 @@ export async function collectCpuMetrics(client: Client): Promise<{
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
} catch {
cpuPercent = null;
cores = null;
loadTriplet = null;
}
+1
View File
@@ -98,6 +98,7 @@ import {
const systemCrypto = SystemCrypto.getInstance();
await systemCrypto.initializeJWTSecret();
await systemCrypto.initializeDatabaseKey();
await systemCrypto.initializeEncryptionKey();
await systemCrypto.initializeInternalAuthToken();
await AutoSSLSetup.initialize();
+8 -3
View File
@@ -1,15 +1,20 @@
import swaggerJSDoc from "swagger-jsdoc";
import swaggerJSDoc from "@deadendjs/swagger-jsdoc";
import path from "path";
import { fileURLToPath } from "url";
import { promises as fs } from "fs";
import { systemLogger } from "./utils/logger.js";
interface SwaggerOptions {
definition: object;
apis: string[];
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, "..", "..", "..");
const swaggerOptions: swaggerJSDoc.Options = {
const swaggerOptions: SwaggerOptions = {
definition: {
openapi: "3.0.3",
info: {
@@ -130,7 +135,7 @@ async function generateOpenAPISpec() {
operation: "openapi_generate_start",
});
const swaggerSpec = swaggerJSDoc(swaggerOptions);
const swaggerSpec = await swaggerJSDoc(swaggerOptions);
const outputPath = path.join(projectRoot, "openapi.json");
+299 -22
View File
@@ -1,15 +1,13 @@
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { UserCrypto } from "./user-crypto.js";
import { SystemCrypto } from "./system-crypto.js";
import { DataCrypto } from "./data-crypto.js";
import { databaseLogger, authLogger } from "./logger.js";
import type { Request, Response, NextFunction } from "express";
import {
db,
getSqlite,
saveMemoryDatabaseToFile,
} from "../database/db/index.js";
import { sessions, trustedDevices } from "../database/db/schema.js";
import bcrypt from "bcryptjs";
import { db } from "../database/db/index.js";
import { sessions, trustedDevices, apiKeys } from "../database/db/schema.js";
import { eq, and, sql } from "drizzle-orm";
import { nanoid } from "nanoid";
import type { DeviceType } from "./user-agent-parser.js";
@@ -29,12 +27,21 @@ interface JWTPayload {
userId: string;
sessionId?: string;
pendingTOTP?: boolean;
dataKeyWrap?: WrappedDataKey;
iat?: number;
exp?: number;
}
interface WrappedDataKey {
version: "v1";
iv: string;
tag: string;
data: string;
}
interface AuthenticatedRequest extends Request {
userId?: string;
sessionId?: string;
pendingTOTP?: boolean;
dataKey?: Buffer;
}
@@ -198,6 +205,113 @@ class AuthManager {
}
}
private getDataKeyAAD(userId: string, sessionId?: string): Buffer {
return Buffer.from(`${userId}:${sessionId || ""}`, "utf8");
}
private async wrapUserDataKey(
userId: string,
sessionId: string | undefined,
dataKey: Buffer,
): Promise<WrappedDataKey> {
const encryptionKey = await this.systemCrypto.getEncryptionKey();
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey, iv);
cipher.setAAD(this.getDataKeyAAD(userId, sessionId));
const encrypted = Buffer.concat([cipher.update(dataKey), cipher.final()]);
const tag = cipher.getAuthTag();
return {
version: "v1",
iv: iv.toString("base64url"),
tag: tag.toString("base64url"),
data: encrypted.toString("base64url"),
};
}
private async unwrapUserDataKey(
userId: string,
sessionId: string | undefined,
wrapped: WrappedDataKey,
): Promise<Buffer> {
if (wrapped.version !== "v1") {
throw new Error(
`Unsupported wrapped data key version: ${wrapped.version}`,
);
}
const encryptionKey = await this.systemCrypto.getEncryptionKey();
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
encryptionKey,
Buffer.from(wrapped.iv, "base64url"),
);
decipher.setAAD(this.getDataKeyAAD(userId, sessionId));
decipher.setAuthTag(Buffer.from(wrapped.tag, "base64url"));
return Buffer.concat([
decipher.update(Buffer.from(wrapped.data, "base64url")),
decipher.final(),
]);
}
private async addWrappedDataKey(payload: JWTPayload): Promise<void> {
if (payload.pendingTOTP) {
return;
}
const dataKey = this.userCrypto.getUserDataKey(payload.userId);
if (!dataKey) {
return;
}
payload.dataKeyWrap = await this.wrapUserDataKey(
payload.userId,
payload.sessionId,
dataKey,
);
}
private async restoreDataKeyFromPayload(
payload: JWTPayload,
sessionExpiresAt?: string,
): Promise<void> {
if (
!payload.dataKeyWrap ||
this.userCrypto.getUserDataKey(payload.userId)
) {
return;
}
const expiresAt = sessionExpiresAt
? new Date(sessionExpiresAt).getTime()
: payload.exp
? payload.exp * 1000
: Date.now();
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
return;
}
try {
const dataKey = await this.unwrapUserDataKey(
payload.userId,
payload.sessionId,
payload.dataKeyWrap,
);
this.userCrypto.restoreUserDataKey(payload.userId, dataKey, expiresAt);
dataKey.fill(0);
} catch (error) {
databaseLogger.warn("Failed to restore data key from session token", {
operation: "session_data_key_restore_failed",
userId: payload.userId,
sessionId: payload.sessionId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
async generateJWTToken(
userId: string,
options: {
@@ -234,6 +348,7 @@ class AuthManager {
if (!options.pendingTOTP && options.deviceType && options.deviceInfo) {
const sessionId = nanoid();
payload.sessionId = sessionId;
await this.addWrappedDataKey(payload);
const token = jwt.sign(payload, jwtSecret, {
expiresIn,
@@ -281,6 +396,7 @@ class AuthManager {
return token;
}
await this.addWrappedDataKey(payload);
return jwt.sign(payload, jwtSecret, { expiresIn } as jwt.SignOptions);
}
@@ -327,6 +443,11 @@ class AuthManager {
});
return null;
}
await this.restoreDataKeyFromPayload(
payload,
sessionRecords[0].expiresAt,
);
} catch (dbError) {
databaseLogger.error(
"Failed to check session in database during JWT verification",
@@ -338,6 +459,8 @@ class AuthManager {
);
return null;
}
} else {
await this.restoreDataKeyFromPayload(payload);
}
return payload;
} catch (error) {
@@ -350,12 +473,63 @@ class AuthManager {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async refreshSessionToken(
userId: string,
sessionId: string,
): Promise<{ token: string; maxAge: number } | null> {
const sessionRecords = await db
.select()
.from(sessions)
.where(eq(sessions.id, sessionId))
.limit(1);
if (sessionRecords.length === 0 || sessionRecords[0].userId !== userId) {
return null;
}
const expiresAt = new Date(sessionRecords[0].expiresAt).getTime();
const maxAge = expiresAt - Date.now();
if (!Number.isFinite(maxAge) || maxAge <= 0) {
return null;
}
const payload: JWTPayload = { userId, sessionId };
await this.addWrappedDataKey(payload);
const token = jwt.sign(payload, await this.systemCrypto.getJWTSecret(), {
expiresIn: Math.ceil(maxAge / 1000),
} as jwt.SignOptions);
await db
.update(sessions)
.set({
jwtToken: token,
lastActiveAt: new Date().toISOString(),
})
.where(eq(sessions.id, sessionId));
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
"Failed to save database after session token refresh",
saveError,
{
operation: "session_token_refresh_db_save_failed",
sessionId,
},
);
}
return { token, maxAge };
}
invalidateJWTToken(_token: string): void {
// expected - no-op, JWT tokens are stateless
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
invalidateUserTokens(_userId: string): void {
// expected - no-op, handled by session management
}
@@ -537,9 +711,9 @@ class AuthManager {
maxAge: number = 24 * 60 * 60 * 1000,
) {
return {
httpOnly: false,
httpOnly: true,
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
sameSite: "strict" as const,
sameSite: "lax" as const,
maxAge: maxAge,
path: "/",
};
@@ -547,13 +721,88 @@ class AuthManager {
getClearCookieOptions(req: RequestWithHeaders) {
return {
httpOnly: false,
httpOnly: true,
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
sameSite: "strict" as const,
sameSite: "lax" as const,
path: "/",
};
}
private async handleApiKeyAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
token: string,
requireAdmin = false,
): Promise<void> {
try {
const tokenPrefix = token.substring(0, 12);
const candidates = await db
.select()
.from(apiKeys)
.where(
and(eq(apiKeys.tokenPrefix, tokenPrefix), eq(apiKeys.isActive, true)),
);
if (candidates.length === 0) {
res.status(401).json({ error: "Invalid API key" });
return;
}
let matchedKey: (typeof candidates)[0] | null = null;
for (const candidate of candidates) {
if (await bcrypt.compare(token, candidate.tokenHash)) {
matchedKey = candidate;
break;
}
}
if (!matchedKey) {
res.status(401).json({ error: "Invalid API key" });
return;
}
if (matchedKey.expiresAt && new Date(matchedKey.expiresAt) < new Date()) {
res.status(401).json({ error: "API key has expired" });
return;
}
if (requireAdmin) {
const { users } = await import("../database/db/schema.js");
const userRows = await db
.select()
.from(users)
.where(eq(users.id, matchedKey.userId))
.limit(1);
if (!userRows[0]?.isAdmin) {
res.status(403).json({ error: "Admin access required" });
return;
}
}
db.update(apiKeys)
.set({ lastUsedAt: new Date().toISOString() })
.where(eq(apiKeys.id, matchedKey.id))
.then(() => {})
.catch((err) => {
databaseLogger.warn("Failed to update API key lastUsedAt", {
operation: "api_key_update_last_used",
keyId: matchedKey!.id,
error: err instanceof Error ? err.message : "Unknown",
});
});
req.userId = matchedKey.userId;
next();
} catch (error) {
databaseLogger.error("API key authentication failed", error, {
operation: "api_key_auth_failed",
});
res.status(500).json({ error: "API key authentication failed" });
}
}
createAuthMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
const authReq = req as AuthenticatedRequest;
@@ -570,10 +819,17 @@ class AuthManager {
return res.status(401).json({ error: "Missing authentication token" });
}
if (token.startsWith("tmx_")) {
return this.handleApiKeyAuth(authReq, res, next, token);
}
const payload = await this.verifyJWTToken(token);
if (!payload) {
return res.status(401).json({ error: "Invalid token" });
return res
.clearCookie("jwt", this.getClearCookieOptions(req))
.status(401)
.json({ error: "Invalid token" });
}
if (payload.pendingTOTP) {
@@ -597,10 +853,13 @@ class AuthManager {
sessionId: payload.sessionId,
userId: payload.userId,
});
return res.status(401).json({
error: "Session not found",
code: "SESSION_NOT_FOUND",
});
return res
.clearCookie("jwt", this.getClearCookieOptions(req))
.status(401)
.json({
error: "Session not found",
code: "SESSION_NOT_FOUND",
});
}
const session = sessionRecords[0];
@@ -657,10 +916,13 @@ class AuthManager {
);
});
return res.status(401).json({
error: "Session has expired",
code: "SESSION_EXPIRED",
});
return res
.clearCookie("jwt", this.getClearCookieOptions(req))
.status(401)
.json({
error: "Session has expired",
code: "SESSION_EXPIRED",
});
}
db.update(sessions)
@@ -684,6 +946,7 @@ class AuthManager {
}
authReq.userId = payload.userId;
authReq.sessionId = payload.sessionId;
authReq.pendingTOTP = payload.pendingTOTP;
next();
};
@@ -718,10 +981,23 @@ class AuthManager {
return res.status(401).json({ error: "Missing authentication token" });
}
if (token.startsWith("tmx_")) {
return this.handleApiKeyAuth(
req as AuthenticatedRequest,
res,
next,
token,
true,
);
}
const payload = await this.verifyJWTToken(token);
if (!payload) {
return res.status(401).json({ error: "Invalid token" });
return res
.clearCookie("jwt", this.getClearCookieOptions(req))
.status(401)
.json({ error: "Invalid token" });
}
if (payload.pendingTOTP) {
@@ -755,6 +1031,7 @@ class AuthManager {
const authReq = req as AuthenticatedRequest;
authReq.userId = payload.userId;
authReq.sessionId = payload.sessionId;
authReq.pendingTOTP = payload.pendingTOTP;
next();
} catch (error) {
+1
View File
@@ -171,6 +171,7 @@ IP.3 = 0.0.0.0
} catch (error) {
throw new Error(
`SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ cause: error },
);
}
}
-1
View File
@@ -53,7 +53,6 @@ export function createCorsMiddleware(
return callback(null, true);
const configured = getAllowedOrigins();
if (configured.length === 0) return callback(null, true);
if (configured.includes("*") || configured.includes(origin))
return callback(null, true);
@@ -102,6 +102,7 @@ class DatabaseFileEncryption {
});
throw new Error(
`Database buffer encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ cause: error },
);
}
}
@@ -197,6 +198,7 @@ class DatabaseFileEncryption {
});
throw new Error(
`Database file encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ cause: error },
);
}
}
@@ -237,6 +239,7 @@ class DatabaseFileEncryption {
if (!fs.existsSync(metadataPath)) {
throw new Error(
`Could not read database: Not a valid single-file format and metadata file is missing: ${metadataPath}. Error: ${singleFileError.message}`,
{ cause: singleFileError },
);
}
@@ -247,6 +250,7 @@ class DatabaseFileEncryption {
} catch (twoFileError) {
throw new Error(
`Failed to read database using both single-file and two-file formats. Error: ${twoFileError.message}`,
{ cause: twoFileError },
);
}
}
@@ -358,6 +362,7 @@ class DatabaseFileEncryption {
`- .env file readable: ${envFileReadable}\n` +
`- DATABASE_KEY in environment: ${!!process.env.DATABASE_KEY}\n` +
`Original error: ${errorMessage}`,
{ cause: error },
);
}
@@ -366,7 +371,9 @@ class DatabaseFileEncryption {
encryptedPath,
errorMessage,
});
throw new Error(`Database buffer decryption failed: ${errorMessage}`);
throw new Error(`Database buffer decryption failed: ${errorMessage}`, {
cause: error,
});
}
}
@@ -398,6 +405,7 @@ class DatabaseFileEncryption {
});
throw new Error(
`Database file decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ cause: error },
);
}
}
+3 -2
View File
@@ -50,8 +50,8 @@ export class DatabaseMigration {
}
}
let needsMigration = false;
let reason = "";
let needsMigration: boolean;
let reason: string;
if (hasEncryptedDb && hasUnencryptedDb) {
const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;
@@ -119,6 +119,7 @@ export class DatabaseMigration {
});
throw new Error(
`Backup creation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
{ cause: error },
);
}
}
@@ -12,6 +12,14 @@ interface DatabaseInstance {
export class LazyFieldEncryption {
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
key_password: "keyPassword",
sudo_password: "sudoPassword",
autostart_password: "autostartPassword",
autostart_key: "autostartKey",
autostart_key_password: "autostartKeyPassword",
socks5_password: "socks5Password",
rdp_password: "rdpPassword",
vnc_password: "vncPassword",
telnet_password: "telnetPassword",
private_key: "privateKey",
public_key: "publicKey",
password_hash: "passwordHash",
@@ -21,6 +29,14 @@ export class LazyFieldEncryption {
oidc_identifier: "oidcIdentifier",
keyPassword: "key_password",
sudoPassword: "sudo_password",
autostartPassword: "autostart_password",
autostartKey: "autostart_key",
autostartKeyPassword: "autostart_key_password",
socks5Password: "socks5_password",
rdpPassword: "rdp_password",
vncPassword: "vnc_password",
telnetPassword: "telnet_password",
privateKey: "private_key",
publicKey: "public_key",
passwordHash: "password_hash",
@@ -93,6 +109,14 @@ export class LazyFieldEncryption {
"password",
"key",
"keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
"socks5Password",
"rdpPassword",
"vncPassword",
"telnetPassword",
"privateKey",
"publicKey",
"clientSecret",
@@ -259,6 +283,10 @@ export class LazyFieldEncryption {
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
"socks5Password",
"rdpPassword",
"vncPassword",
"telnetPassword",
],
ssh_credentials: [
"password",
+13 -2
View File
@@ -1,4 +1,5 @@
import chalk from "chalk";
import type { ChalkInstance } from "chalk";
export type LogLevel = "debug" | "info" | "warn" | "error" | "success";
@@ -78,7 +79,17 @@ export class Logger {
}
private getTimeStamp(): string {
return chalk.gray(`[${new Date().toLocaleTimeString()}]`);
const now = new Date();
const format = process.env.LOG_TIMESTAMP_FORMAT?.toLowerCase();
let time: string;
if (format === "iso") {
time = now.toISOString();
} else if (format === "24h") {
time = now.toLocaleTimeString("en-GB", { hour12: false });
} else {
time = now.toLocaleTimeString();
}
return chalk.gray(`[${time}]`);
}
private sanitizeContext(context: LogContext): LogContext {
@@ -149,7 +160,7 @@ export class Logger {
return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`;
}
private getLevelColor(level: LogLevel): chalk.Chalk {
private getLevelColor(level: LogLevel): ChalkInstance {
switch (level) {
case "debug":
return chalk.magenta;
+4 -4
View File
@@ -1,7 +1,7 @@
import { HttpsProxyAgent } from "https-proxy-agent";
import type { Agent } from "http";
import { ProxyAgent } from "undici";
import type { Dispatcher } from "undici-types";
export function getProxyAgent(targetUrl?: string): Agent | undefined {
export function getProxyAgent(targetUrl?: string): Dispatcher | undefined {
const proxyUrl =
process.env.https_proxy ||
process.env.HTTPS_PROXY ||
@@ -26,5 +26,5 @@ export function getProxyAgent(targetUrl?: string): Agent | undefined {
}
}
return new HttpsProxyAgent(proxyUrl);
return new ProxyAgent(proxyUrl) as unknown as Dispatcher;
}
+66 -4
View File
@@ -7,6 +7,7 @@ class SystemCrypto {
private static instance: SystemCrypto;
private jwtSecret: string | null = null;
private databaseKey: Buffer | null = null;
private encryptionKey: Buffer | null = null;
private internalAuthToken: string | null = null;
private credentialSharingKey: Buffer | null = null;
@@ -61,7 +62,7 @@ class SystemCrypto {
databaseLogger.error("Failed to initialize JWT secret", error, {
operation: "jwt_init_failed",
});
throw new Error("JWT secret initialization failed");
throw new Error("JWT secret initialization failed", { cause: error });
}
}
@@ -103,7 +104,7 @@ class SystemCrypto {
operation: "db_key_init_failed",
dataDir: process.env.DATA_DIR || "./db/data",
});
throw new Error("Database key initialization failed");
throw new Error("Database key initialization failed", { cause: error });
}
}
@@ -114,6 +115,46 @@ class SystemCrypto {
return this.databaseKey!;
}
async initializeEncryptionKey(): Promise<void> {
try {
const dataDir = process.env.DATA_DIR || "./db/data";
const envPath = path.join(dataDir, ".env");
const envKey = process.env.ENCRYPTION_KEY;
if (envKey && envKey.length >= 64) {
this.encryptionKey = Buffer.from(envKey, "hex");
return;
}
try {
const envContent = await fs.readFile(envPath, "utf8");
const keyMatch = envContent.match(/^ENCRYPTION_KEY=(.+)$/m);
if (keyMatch && keyMatch[1] && keyMatch[1].length >= 64) {
this.encryptionKey = Buffer.from(keyMatch[1], "hex");
process.env.ENCRYPTION_KEY = keyMatch[1];
return;
}
} catch {
// expected - env file may not exist
}
await this.generateAndGuideEncryptionKey();
} catch (error) {
databaseLogger.error("Failed to initialize encryption key", error, {
operation: "encryption_key_init_failed",
dataDir: process.env.DATA_DIR || "./db/data",
});
throw new Error("Encryption key initialization failed", { cause: error });
}
}
async getEncryptionKey(): Promise<Buffer> {
if (!this.encryptionKey) {
await this.initializeEncryptionKey();
}
return this.encryptionKey!;
}
async initializeInternalAuthToken(): Promise<void> {
try {
const envToken = process.env.INTERNAL_AUTH_TOKEN;
@@ -142,7 +183,9 @@ class SystemCrypto {
databaseLogger.error("Failed to initialize internal auth token", error, {
operation: "internal_auth_init_failed",
});
throw new Error("Internal auth token initialization failed");
throw new Error("Internal auth token initialization failed", {
cause: error,
});
}
}
@@ -186,7 +229,9 @@ class SystemCrypto {
dataDir: process.env.DATA_DIR || "./db/data",
},
);
throw new Error("Credential sharing key initialization failed");
throw new Error("Credential sharing key initialization failed", {
cause: error,
});
}
}
@@ -230,6 +275,23 @@ class SystemCrypto {
});
}
private async generateAndGuideEncryptionKey(): Promise<void> {
const newKey = crypto.randomBytes(32);
const newKeyHex = newKey.toString("hex");
const instanceId = crypto.randomBytes(8).toString("hex");
this.encryptionKey = newKey;
await this.updateEnvFile("ENCRYPTION_KEY", newKeyHex);
databaseLogger.success("Encryption key auto-generated and saved to .env", {
operation: "encryption_key_auto_generated",
instanceId,
envVarName: "ENCRYPTION_KEY",
note: "Used to wrap session data keys - no restart required",
});
}
private async generateAndGuideInternalAuthToken(): Promise<void> {
const newToken = crypto.randomBytes(32).toString("hex");
const instanceId = crypto.randomBytes(8).toString("hex");
+6 -10
View File
@@ -255,16 +255,12 @@ function parseMacVersion(userAgent: string): string {
* Ignores minor version numbers to handle browser auto-updates.
*/
export function generateDeviceFingerprint(deviceInfo: DeviceInfo): string {
let fingerprintString = "";
if (deviceInfo.type === "desktop") {
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`;
} else if (deviceInfo.type === "mobile") {
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`;
} else {
const browserMajor = deviceInfo.version.split(".")[0];
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser} ${browserMajor}|${deviceInfo.os}`;
}
const fingerprintString =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`
: `${deviceInfo.type}|${deviceInfo.browser} ${
deviceInfo.version.split(".")[0]
}|${deviceInfo.os}`;
return crypto.createHash("sha256").update(fingerprintString).digest("hex");
}
+13
View File
@@ -265,6 +265,19 @@ class UserCrypto {
return session.dataKey;
}
restoreUserDataKey(userId: string, dataKey: Buffer, expiresAt: number): void {
const oldSession = this.userSessions.get(userId);
if (oldSession) {
oldSession.dataKey.fill(0);
}
this.userSessions.set(userId, {
dataKey: Buffer.from(dataKey),
expiresAt,
lastActivity: Date.now(),
});
}
logoutUser(userId: string): void {
const session = this.userSessions.get(userId);
if (session) {
+1 -1
View File
@@ -482,7 +482,7 @@ class UserDataImport {
return await this.importUserData(targetUserId, exportData, options);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("Invalid JSON format in import data");
throw new Error("Invalid JSON format in import data", { cause: error });
}
throw error;
}
+677
View File
@@ -0,0 +1,677 @@
"use client";
import React, {
useState,
useCallback,
createContext,
useContext,
useRef,
useEffect,
} from "react";
import {
motion,
AnimatePresence,
easeInOut,
type Variants,
} from "motion/react";
import {
ChevronRight,
Folder,
FolderOpen,
File,
type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
const animationVariants: Variants = {
rootInitial: { opacity: 0, y: 20 },
rootAnimate: { opacity: 1, y: 0 },
itemInitial: { opacity: 0, x: -10 },
itemAnimate: { opacity: 1, x: 0 },
contentHidden: { opacity: 0, height: 0 },
contentVisible: { opacity: 1, height: "auto" },
chevronClosed: { rotate: 0 },
chevronOpen: { rotate: 90 },
};
const transitions = {
root: { duration: 0.4 },
item: { duration: 0.2 },
content: { duration: 0.3, ease: easeInOut },
chevron: { duration: 0.2 },
};
interface ExpansionContextType {
expandedIds: Set<string>;
toggleExpanded: (id: string) => void;
}
interface SelectionContextType {
selectedId: string | null;
setSelected: (id: string) => void;
onSelect?: (id: string, label: string) => void;
}
interface TreeContextType {
focusedId: string | null;
setFocusedId: (id: string | null) => void;
treeId: string;
setKeyboardMode: (mode: boolean) => void;
keyboardMode: boolean;
}
interface LevelContextType {
level: number;
}
const ExpansionContext = createContext<ExpansionContextType | null>(null);
const SelectionContext = createContext<SelectionContextType | null>(null);
const TreeContext = createContext<TreeContextType | null>(null);
const LevelContext = createContext<LevelContextType>({ level: 0 });
const useExpansion = () => {
const context = useContext(ExpansionContext);
if (!context) {
throw new Error(
"FolderTree components must be used within FolderTree.Root",
);
}
return context;
};
const useSelection = () => {
const context = useContext(SelectionContext);
if (!context) {
throw new Error(
"FolderTree components must be used within FolderTree.Root",
);
}
return context;
};
const useTree = () => {
const context = useContext(TreeContext);
if (!context) {
throw new Error(
"FolderTree components must be used within FolderTree.Root",
);
}
return context;
};
const useLevel = () => {
return useContext(LevelContext);
};
const getPaddingClass = (level: number): string => {
const paddingMap: Record<number, string> = {
0: "pl-3",
1: "pl-8",
2: "pl-12",
3: "pl-16",
4: "pl-20",
5: "pl-24",
6: "pl-28",
7: "pl-32",
};
return paddingMap[level] || `pl-[${Math.min(level * 4 + 12, 48)}px]`;
};
interface CustomBadge {
content: React.ReactNode;
className?: string;
ariaLabel?: string;
}
interface RootProps {
defaultExpanded?: string[];
defaultSelected?: string;
selectedId?: string | null;
expandedIds?: Set<string>;
onSelect?: (id: string, label: string) => void;
className?: string;
children: React.ReactNode;
id?: string;
}
interface ItemProps {
id: string;
label: string;
icon?: LucideIcon;
badge?: string | number;
modified?: boolean | CustomBadge;
untracked?: boolean | CustomBadge;
className?: string;
children?: React.ReactNode;
}
interface TriggerProps {
className?: string;
}
interface ContentProps {
children: React.ReactNode;
className?: string;
}
const Root: React.FC<RootProps> = ({
defaultExpanded = [],
defaultSelected,
selectedId: controlledSelectedId,
expandedIds: additionalExpandedIds,
onSelect,
className = "",
children,
id = "folder-tree",
}) => {
const [expandedIds, setExpandedIds] = useState<Set<string>>(
new Set(defaultExpanded),
);
const [internalSelectedId, setInternalSelectedId] = useState<string | null>(
defaultSelected || null,
);
const selectedId =
controlledSelectedId !== undefined
? controlledSelectedId
: internalSelectedId;
const [focusedId, setFocusedId] = useState<string | null>(null);
const [keyboardMode, setKeyboardMode] = useState(false);
const treeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!additionalExpandedIds || additionalExpandedIds.size === 0) return;
setExpandedIds((prev) => {
const merged = new Set(prev);
let changed = false;
for (const id of additionalExpandedIds) {
if (!merged.has(id)) {
merged.add(id);
changed = true;
}
}
return changed ? merged : prev;
});
}, [additionalExpandedIds]);
const toggleExpanded = useCallback((id: string) => {
setExpandedIds((prev) => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}, []);
const setSelected = useCallback((id: string) => {
setInternalSelectedId(id);
}, []);
const getVisibleItemIds = useCallback(() => {
const items = Array.from(
treeRef.current?.querySelectorAll('[role="treeitem"]') || [],
);
return items
.filter((item) => {
const element = item as HTMLElement;
return element.offsetHeight > 0 && element.offsetWidth > 0;
})
.map((item) => item.getAttribute("data-id"))
.filter(Boolean) as string[];
}, []);
const getAllItemIds = useCallback(() => {
const items = Array.from(
treeRef.current?.querySelectorAll('[role="treeitem"]') || [],
);
return items
.map((item) => item.getAttribute("data-id"))
.filter(Boolean) as string[];
}, []);
const [treeHasFocus, setTreeHasFocus] = useState(false);
const handleTreeFocus = useCallback(() => {
if (!treeHasFocus) {
setTreeHasFocus(true);
setKeyboardMode(true);
}
}, [treeHasFocus]);
const handleTreeBlur = useCallback((e: React.FocusEvent) => {
if (!treeRef.current?.contains(e.relatedTarget as Node)) {
setTreeHasFocus(false);
setFocusedId(null);
setKeyboardMode(false);
}
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const getVisibleItems = () => {
return Array.from(
treeRef.current?.querySelectorAll('[role="treeitem"]') || [],
).filter((item) => {
const element = item as HTMLElement;
return element.offsetHeight > 0 && element.offsetWidth > 0;
});
};
if (e.key === "Tab") {
if (treeHasFocus && !focusedId) {
const visibleItemIds = getVisibleItemIds();
if (visibleItemIds.length > 0) {
setFocusedId(visibleItemIds[0]);
e.preventDefault();
return;
}
}
if (focusedId) {
const visibleItems = getVisibleItems();
const currentIndex = visibleItems.findIndex(
(item) => item.getAttribute("data-id") === focusedId,
);
if (e.shiftKey) {
if (currentIndex === 0) {
setFocusedId(null);
setTreeHasFocus(false);
setKeyboardMode(false);
return;
}
const nextIndex = Math.max(0, currentIndex - 1);
const nextItem = visibleItems[nextIndex] as HTMLElement;
const nextId = nextItem?.getAttribute("data-id");
if (nextId) {
setFocusedId(nextId);
e.preventDefault();
}
} else {
if (currentIndex === visibleItems.length - 1) {
setFocusedId(null);
setTreeHasFocus(false);
setKeyboardMode(false);
return;
}
const nextIndex = Math.min(
visibleItems.length - 1,
currentIndex + 1,
);
const nextItem = visibleItems[nextIndex] as HTMLElement;
const nextId = nextItem?.getAttribute("data-id");
if (nextId) {
setFocusedId(nextId);
e.preventDefault();
}
}
}
return;
}
if (!keyboardMode || !focusedId) return;
const visibleItems = getVisibleItems();
const currentIndex = visibleItems.findIndex(
(item) => item.getAttribute("data-id") === focusedId,
);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
if (currentIndex < visibleItems.length - 1) {
const nextItem = visibleItems[currentIndex + 1] as HTMLElement;
const nextId = nextItem.getAttribute("data-id");
if (nextId) setFocusedId(nextId);
}
break;
case "ArrowUp":
e.preventDefault();
if (currentIndex > 0) {
const prevItem = visibleItems[currentIndex - 1] as HTMLElement;
const prevId = prevItem.getAttribute("data-id");
if (prevId) setFocusedId(prevId);
}
break;
case "ArrowRight":
e.preventDefault();
if (!expandedIds.has(focusedId)) {
toggleExpanded(focusedId);
}
break;
case "ArrowLeft":
e.preventDefault();
if (expandedIds.has(focusedId)) {
toggleExpanded(focusedId);
}
break;
case "Enter":
case " ":
e.preventDefault();
setSelected(focusedId);
if (onSelect) {
const currentItem = visibleItems[currentIndex] as HTMLElement;
const label =
currentItem.querySelector("span:nth-of-type(2)")?.textContent ||
"";
onSelect(focusedId, label);
}
break;
}
},
[
focusedId,
keyboardMode,
expandedIds,
toggleExpanded,
setSelected,
onSelect,
getVisibleItemIds,
treeHasFocus,
],
);
useEffect(() => {
const handleMouseDown = () => setKeyboardMode(false);
document.addEventListener("mousedown", handleMouseDown);
return () => {
document.removeEventListener("mousedown", handleMouseDown);
};
}, []);
const expansionValue: ExpansionContextType = {
expandedIds,
toggleExpanded,
};
const selectionValue: SelectionContextType = {
selectedId,
setSelected,
onSelect,
};
const treeValue: TreeContextType = {
focusedId,
setFocusedId,
treeId: id,
setKeyboardMode,
keyboardMode,
};
return (
<ExpansionContext.Provider value={expansionValue}>
<SelectionContext.Provider value={selectionValue}>
<TreeContext.Provider value={treeValue}>
<LevelContext.Provider value={{ level: 0 }}>
<motion.div
ref={treeRef}
variants={animationVariants}
initial="rootInitial"
animate="rootAnimate"
transition={transitions.root}
className={cn(
"bg-canvas border border-edge rounded-lg overflow-hidden",
className,
)}
role="tree"
aria-labelledby={`${id}-label`}
tabIndex={0}
onKeyDown={handleKeyDown}
onFocus={handleTreeFocus}
onBlur={handleTreeBlur}
>
<div className="w-full overflow-y-auto bg-canvas text-sm">
{children}
</div>
</motion.div>
</LevelContext.Provider>
</TreeContext.Provider>
</SelectionContext.Provider>
</ExpansionContext.Provider>
);
};
const ItemContext = createContext<{
itemId: string;
hasChildren: boolean;
isExpanded: boolean;
toggleExpanded: () => void;
} | null>(null);
const Item: React.FC<ItemProps> = ({
id,
label,
icon,
badge,
modified,
untracked,
className = "",
children,
}) => {
const expansionContext = useExpansion();
const selectionContext = useSelection();
const treeContext = useTree();
const { level } = useLevel();
const itemRef = useRef<HTMLDivElement>(null);
const keyboardMode = treeContext.keyboardMode;
const hasChildren = React.Children.count(children) > 0;
const isExpanded = expansionContext.expandedIds.has(id);
const isSelected = selectionContext.selectedId === id;
const isFocused = treeContext.focusedId === id;
const handleItemClick = useCallback(() => {
treeContext.setKeyboardMode(false);
selectionContext.setSelected(id);
treeContext.setFocusedId(id);
if (selectionContext.onSelect) {
selectionContext.onSelect(id, label);
}
}, [id, label, selectionContext, treeContext]);
const toggleExpanded = useCallback(() => {
if (hasChildren) {
expansionContext.toggleExpanded(id);
}
}, [id, hasChildren, expansionContext]);
const handleFocus = useCallback(() => {
treeContext.setFocusedId(id);
}, [id, treeContext]);
useEffect(() => {
if (isFocused && itemRef.current) {
itemRef.current.focus();
}
}, [isFocused]);
const IconComponent =
icon || (hasChildren ? (isExpanded ? FolderOpen : Folder) : File);
const itemContextValue = {
itemId: id,
hasChildren,
isExpanded,
toggleExpanded,
};
const renderBadge = (
badgeData: boolean | CustomBadge | undefined,
defaultContent: string,
defaultClassName: string,
) => {
if (!badgeData) return null;
if (typeof badgeData === "boolean") {
return (
<span
className={defaultClassName}
aria-label={`${defaultContent} status`}
>
{defaultContent}
</span>
);
}
return (
<span
className={cn(
"ml-auto text-xs px-2 py-0.5 rounded-full",
badgeData.className,
)}
aria-label={badgeData.ariaLabel || `Custom badge: ${badgeData.content}`}
>
{badgeData.content}
</span>
);
};
return (
<ItemContext.Provider value={itemContextValue}>
<LevelContext.Provider value={{ level: level + 1 }}>
<div>
<motion.div
ref={itemRef}
variants={animationVariants}
initial="itemInitial"
animate="itemAnimate"
transition={{ ...transitions.item, delay: level * 0.05 }}
data-selected={isSelected ? "true" : "false"}
data-id={id}
className={cn(
"flex items-center gap-2 py-1.5 text-sm transition-colors cursor-pointer select-none",
getPaddingClass(level),
className,
isSelected
? "bg-accent text-accent-foreground border-r-2 border-ring"
: "",
!isSelected && "hover:bg-hover",
keyboardMode && isFocused
? "focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-inset"
: "focus:outline-hidden",
)}
onClick={(e: React.MouseEvent) => {
handleItemClick();
e.stopPropagation();
toggleExpanded();
}}
onFocus={handleFocus}
role="treeitem"
tabIndex={isFocused ? 0 : -1}
aria-expanded={hasChildren ? isExpanded : undefined}
aria-selected={isSelected}
aria-label={`${hasChildren ? "Folder" : "File"}: ${label}`}
aria-level={level + 1}
>
{hasChildren && (
<motion.span
className="shrink-0 cursor-pointer"
variants={animationVariants}
animate={isExpanded ? "chevronOpen" : "chevronClosed"}
transition={transitions.chevron}
aria-hidden="true"
>
<ChevronRight size={14} className="text-muted-foreground" />
</motion.span>
)}
{!hasChildren && <span className="w-3 mr-2" aria-hidden="true" />}
{IconComponent && (
<IconComponent
size={16}
data-selected={isSelected ? "true" : "false"}
data-child={hasChildren ? "true" : "false"}
className={cn(
"mr-1 shrink-0 text-muted-foreground data-[child=true]:text-primary data-[selected=true]:text-accent-foreground",
)}
aria-hidden="true"
/>
)}
<span className="flex-1">{label}</span>
{badge && (
<span
className="ml-auto text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full"
aria-label={`Badge: ${badge}`}
>
{badge}
</span>
)}
{renderBadge(
modified,
"M",
"ml-auto text-xs bg-yellow-200 dark:bg-yellow-700 text-yellow-800 dark:text-yellow-200 px-2 py-0.5 rounded-full",
)}
{renderBadge(
untracked,
"U",
"ml-auto text-xs bg-green-200 dark:bg-green-700 text-green-800 dark:text-green-200 px-2 py-0.5 rounded-full",
)}
</motion.div>
{children}
</div>
</LevelContext.Provider>
</ItemContext.Provider>
);
};
const Trigger: React.FC<TriggerProps> = ({ className = "" }) => {
const itemContext = useContext(ItemContext);
if (!itemContext || !itemContext.hasChildren) {
return null;
}
return (
<motion.span
className={cn("mr-2 shrink-0 cursor-pointer", className)}
variants={animationVariants}
animate={itemContext.isExpanded ? "chevronOpen" : "chevronClosed"}
transition={transitions.chevron}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
itemContext.toggleExpanded();
}}
role="button"
aria-label={itemContext.isExpanded ? "Collapse" : "Expand"}
tabIndex={-1}
>
<ChevronRight size={14} className="text-muted-foreground" />
</motion.span>
);
};
const Content: React.FC<ContentProps> = ({ children, className = "" }) => {
const itemContext = useContext(ItemContext);
if (!itemContext) {
return <>{children}</>;
}
const hasContent = React.Children.count(children) > 0;
return (
<AnimatePresence>
{hasContent && itemContext.isExpanded && (
<motion.div
variants={animationVariants}
initial="contentHidden"
animate="contentVisible"
exit="contentHidden"
transition={transitions.content}
style={{ overflow: "hidden" }}
className={className}
role="group"
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
const FolderTree = {
Root,
Item,
Trigger,
Content,
};
export default FolderTree;
+55 -24
View File
@@ -1,28 +1,59 @@
import { type ComponentProps, type ReactNode } from "react";
import { cn } from "@/lib/utils";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className,
)}
{...props}
/>
);
}
export type KbdProps = ComponentProps<"span"> & {
children: ReactNode;
};
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
);
}
export const Kbd = ({ className, children, ...props }: KbdProps) => (
<span
className={cn(
"inline-flex select-none items-center rounded-md border px-2 py-1 text-[10px] font-mono font-medium relative",
"bg-linear-to-b from-gray-100 to-gray-200 border-gray-300 shadow-[0_2px_0_#ccc,0_3px_2px_rgba(0,0,0,0.25)]",
"dark:from-zinc-800 dark:to-zinc-900 dark:border-zinc-700 dark:shadow-[0_2px_0_#222,0_3px_2px_rgba(0,0,0,0.4)]",
"dark:text-zinc-200",
className,
)}
{...props}
>
{children}
</span>
);
export { Kbd, KbdGroup };
export type KbdKeyProps = ComponentProps<"span"> & {
"aria-label"?: string;
className?: string;
};
export const KbdKey = ({ className, children, ...props }: KbdKeyProps) => (
<span
className={cn(
"px-1 py-px rounded-sm select-none text-[10px] font-mono font-medium bg-transparent",
className,
)}
{...props}
>
{children}
</span>
);
export type KbdSeparatorProps = ComponentProps<"span"> & {
children?: ReactNode;
className?: string;
};
export const KbdSeparator = ({
className,
children = "+",
...props
}: KbdSeparatorProps) => (
<span
className={cn(
"text-muted-foreground/70 text-[10px] mx-0.5 select-none pointer-events-none",
className,
)}
{...props}
>
{children}
</span>
);
+12 -8
View File
@@ -1,15 +1,19 @@
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import {
Group as ResizableGroup,
Panel as ResizablePrimitivePanel,
Separator as ResizableSeparator,
} from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
}: React.ComponentProps<typeof ResizableGroup>) {
return (
<ResizablePrimitive.PanelGroup
<ResizableGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
@@ -22,19 +26,19 @@ function ResizablePanelGroup({
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}: React.ComponentProps<typeof ResizablePrimitivePanel>) {
return <ResizablePrimitivePanel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
}: React.ComponentProps<typeof ResizableSeparator> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
<ResizableSeparator
data-slot="resizable-handle"
className={cn(
"relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-edge-hover hover:bg-interact active:bg-pressed transition-colors duration-150",
@@ -47,7 +51,7 @@ function ResizableHandle({
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
</ResizableSeparator>
);
}
+2 -2
View File
@@ -598,9 +598,9 @@ function SidebarMenuSkeleton({
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
const [width] = React.useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
});
return (
<div
+17 -2
View File
@@ -1,13 +1,13 @@
import React from "react";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
import { Button } from "@/components/ui/button.tsx";
import { ExternalLink, Download, AlertTriangle } from "lucide-react";
import { ExternalLink, Download, AlertTriangle, Info } from "lucide-react";
import { useTranslation } from "react-i18next";
interface VersionAlertProps {
updateInfo: {
success: boolean;
status?: "up_to_date" | "requires_update";
status?: "up_to_date" | "requires_update" | "beta";
localVersion?: string;
remoteVersion?: string;
latest_release?: {
@@ -53,6 +53,21 @@ export function VersionAlert({ updateInfo, onDownload }: VersionAlertProps) {
);
}
if (updateInfo.status === "beta") {
return (
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>{t("versionCheck.betaVersion")}</AlertTitle>
<AlertDescription>
{t("versionCheck.betaVersionDesc", {
current: updateInfo.localVersion,
latest: updateInfo.remoteVersion,
})}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "requires_update") {
return (
<Alert variant="destructive">
+1 -1
View File
@@ -144,7 +144,7 @@ export function useConfirmation() {
setPendingConfirmCallback(null);
setPendingResolve(null);
},
} as any);
} as NonNullable<Parameters<typeof toast>[1]>);
if (confirmOnEnter) {
setActiveToastId(toastId);
+4 -4
View File
@@ -3,8 +3,9 @@ import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
const [isMobile, setIsMobile] = React.useState<boolean>(
() =>
typeof window !== "undefined" && window.innerWidth < MOBILE_BREAKPOINT,
);
React.useEffect(() => {
@@ -13,9 +14,8 @@ export function useIsMobile() {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
return isMobile;
}
+29 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from "react";
import { isElectron } from "@/ui/main-axios";
import { isElectron } from "@/lib/electron";
import { getBasePath } from "@/lib/base-path";
interface ServiceWorkerState {
@@ -40,27 +40,54 @@ export function useServiceWorker(): ServiceWorkerState {
if (!isSupported) return;
const shouldReloadOnControllerChange = Boolean(
navigator.serviceWorker.controller,
);
let hasReloadedForUpdate = false;
const handleControllerChange = () => {
if (!shouldReloadOnControllerChange || hasReloadedForUpdate) {
return;
}
hasReloadedForUpdate = true;
window.location.reload();
};
const registerSW = async () => {
try {
const registration = await navigator.serviceWorker.register(
`${getBasePath()}/sw.js`,
{ updateViaCache: "none" },
);
setState((prev) => ({ ...prev, isRegistered: true }));
registration.addEventListener("updatefound", () =>
handleUpdateFound(registration),
);
await registration.update();
} catch (error) {
console.error("[SW] Registration failed:", error);
}
};
navigator.serviceWorker.addEventListener(
"controllerchange",
handleControllerChange,
);
if (document.readyState === "complete") {
registerSW();
} else {
window.addEventListener("load", registerSW);
return () => window.removeEventListener("load", registerSW);
}
return () => {
window.removeEventListener("load", registerSW);
navigator.serviceWorker.removeEventListener(
"controllerchange",
handleControllerChange,
);
};
}, [handleUpdateFound]);
return state;
+71 -174
View File
@@ -1,84 +1,82 @@
import i18n from "i18next";
import i18n, { type BackendModule, type ResourceKey } from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import enTranslation from "../locales/en.json";
import afTranslation from "../locales/translated/af_ZA.json";
import arTranslation from "../locales/translated/ar_SA.json";
import bnTranslation from "../locales/translated/bn_BD.json";
import bgTranslation from "../locales/translated/bg_BG.json";
import caTranslation from "../locales/translated/ca_ES.json";
import csTranslation from "../locales/translated/cs_CZ.json";
import daTranslation from "../locales/translated/da_DK.json";
import deTranslation from "../locales/translated/de_DE.json";
import elTranslation from "../locales/translated/el_GR.json";
import esESTranslation from "../locales/translated/es_ES.json";
import fiTranslation from "../locales/translated/fi_FI.json";
import frTranslation from "../locales/translated/fr_FR.json";
import heTranslation from "../locales/translated/he_IL.json";
import hiTranslation from "../locales/translated/hi_IN.json";
import huTranslation from "../locales/translated/hu_HU.json";
import idTranslation from "../locales/translated/id_ID.json";
import itTranslation from "../locales/translated/it_IT.json";
import jaTranslation from "../locales/translated/ja_JP.json";
import koTranslation from "../locales/translated/ko_KR.json";
import nlTranslation from "../locales/translated/nl_NL.json";
import noTranslation from "../locales/translated/no_NO.json";
import plTranslation from "../locales/translated/pl_PL.json";
import ptPTTranslation from "../locales/translated/pt_PT.json";
import ptBRTranslation from "../locales/translated/pt_BR.json";
import roTranslation from "../locales/translated/ro_RO.json";
import ruTranslation from "../locales/translated/ru_RU.json";
import srTranslation from "../locales/translated/sr_SP.json";
import svSETranslation from "../locales/translated/sv_SE.json";
import thTranslation from "../locales/translated/th_TH.json";
import trTranslation from "../locales/translated/tr_TR.json";
import ukTranslation from "../locales/translated/uk_UA.json";
import viTranslation from "../locales/translated/vi_VN.json";
import zhCNTranslation from "../locales/translated/zh_CN.json";
import zhTWTranslation from "../locales/translated/zh_TW.json";
type LocaleModule = { default: ResourceKey };
const localeLoaders = {
af: () => import("../locales/translated/af_ZA.json"),
ar: () => import("../locales/translated/ar_SA.json"),
bn: () => import("../locales/translated/bn_BD.json"),
bg: () => import("../locales/translated/bg_BG.json"),
ca: () => import("../locales/translated/ca_ES.json"),
cs: () => import("../locales/translated/cs_CZ.json"),
da: () => import("../locales/translated/da_DK.json"),
de: () => import("../locales/translated/de_DE.json"),
el: () => import("../locales/translated/el_GR.json"),
"es-ES": () => import("../locales/translated/es_ES.json"),
fi: () => import("../locales/translated/fi_FI.json"),
fr: () => import("../locales/translated/fr_FR.json"),
he: () => import("../locales/translated/he_IL.json"),
hi: () => import("../locales/translated/hi_IN.json"),
hu: () => import("../locales/translated/hu_HU.json"),
id: () => import("../locales/translated/id_ID.json"),
it: () => import("../locales/translated/it_IT.json"),
ja: () => import("../locales/translated/ja_JP.json"),
ko: () => import("../locales/translated/ko_KR.json"),
nl: () => import("../locales/translated/nl_NL.json"),
no: () => import("../locales/translated/no_NO.json"),
pl: () => import("../locales/translated/pl_PL.json"),
"pt-PT": () => import("../locales/translated/pt_PT.json"),
"pt-BR": () => import("../locales/translated/pt_BR.json"),
ro: () => import("../locales/translated/ro_RO.json"),
ru: () => import("../locales/translated/ru_RU.json"),
sr: () => import("../locales/translated/sr_SP.json"),
"sv-SE": () => import("../locales/translated/sv_SE.json"),
th: () => import("../locales/translated/th_TH.json"),
tr: () => import("../locales/translated/tr_TR.json"),
uk: () => import("../locales/translated/uk_UA.json"),
vi: () => import("../locales/translated/vi_VN.json"),
"zh-CN": () => import("../locales/translated/zh_CN.json"),
"zh-TW": () => import("../locales/translated/zh_TW.json"),
} satisfies Record<string, () => Promise<LocaleModule>>;
const supportedLngs = ["en", ...Object.keys(localeLoaders)];
const localeBackend: BackendModule = {
type: "backend",
init: () => {},
read: (language, _namespace, callback) => {
if (language === "en") {
callback(null, enTranslation);
return;
}
const loadLocale = localeLoaders[language];
if (!loadLocale) {
callback(new Error(`Unsupported language: ${language}`), false);
return;
}
loadLocale()
.then((module) => callback(null, module.default))
.catch((error: unknown) => {
callback(
error instanceof Error ? error : new Error(String(error)),
false,
);
});
},
};
i18n
.use(localeBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
supportedLngs: [
"en",
"af",
"ar",
"bn",
"bg",
"ca",
"cs",
"da",
"de",
"el",
"es-ES",
"fi",
"fr",
"he",
"hi",
"hu",
"id",
"it",
"ja",
"ko",
"nl",
"no",
"pl",
"pt-PT",
"pt-BR",
"ro",
"ru",
"sr",
"sv-SE",
"th",
"tr",
"uk",
"vi",
"zh-CN",
"zh-TW",
],
supportedLngs,
fallbackLng: "en",
debug: false,
@@ -94,109 +92,8 @@ i18n
en: {
translation: enTranslation,
},
af: {
translation: afTranslation,
},
ar: {
translation: arTranslation,
},
bn: {
translation: bnTranslation,
},
bg: {
translation: bgTranslation,
},
ca: {
translation: caTranslation,
},
cs: {
translation: csTranslation,
},
da: {
translation: daTranslation,
},
de: {
translation: deTranslation,
},
el: {
translation: elTranslation,
},
"es-ES": {
translation: esESTranslation,
},
fi: {
translation: fiTranslation,
},
fr: {
translation: frTranslation,
},
he: {
translation: heTranslation,
},
hi: {
translation: hiTranslation,
},
hu: {
translation: huTranslation,
},
id: {
translation: idTranslation,
},
it: {
translation: itTranslation,
},
ja: {
translation: jaTranslation,
},
ko: {
translation: koTranslation,
},
nl: {
translation: nlTranslation,
},
no: {
translation: noTranslation,
},
pl: {
translation: plTranslation,
},
"pt-PT": {
translation: ptPTTranslation,
},
"pt-BR": {
translation: ptBRTranslation,
},
ro: {
translation: roTranslation,
},
ru: {
translation: ruTranslation,
},
sr: {
translation: srTranslation,
},
"sv-SE": {
translation: svSETranslation,
},
th: {
translation: thTranslation,
},
tr: {
translation: trTranslation,
},
uk: {
translation: ukTranslation,
},
vi: {
translation: viTranslation,
},
"zh-CN": {
translation: zhCNTranslation,
},
"zh-TW": {
translation: zhTWTranslation,
},
},
partialBundledLanguages: true,
interpolation: {
escapeValue: false,
+4
View File
@@ -553,3 +553,7 @@
.skinny-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
.h-fade {
mask-image: linear-gradient(transparent, #000 6%, #000 94%, transparent);
}
+45
View File
@@ -0,0 +1,45 @@
const CLIENT_CACHE_VERSION_KEY = "termix_client_cache_version";
const CURRENT_CLIENT_VERSION = import.meta.env.VITE_APP_VERSION || "0.0.0";
async function clearCacheStorage(): Promise<void> {
if (!("caches" in window)) return;
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((name) => caches.delete(name)));
}
async function clearServiceWorkers(): Promise<void> {
if (!("serviceWorker" in navigator)) return;
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(
registrations.map((registration) => registration.unregister()),
);
}
function storeCurrentVersion(): void {
try {
localStorage.setItem(CLIENT_CACHE_VERSION_KEY, CURRENT_CLIENT_VERSION);
} catch {
// expected - storage can be unavailable in restricted contexts
}
}
export async function prepareClientCacheVersion(): Promise<void> {
if (typeof window === "undefined") return;
let storedVersion: string | null = null;
try {
storedVersion = localStorage.getItem(CLIENT_CACHE_VERSION_KEY);
} catch {
storedVersion = null;
}
if (storedVersion === CURRENT_CLIENT_VERSION) {
return;
}
await Promise.allSettled([clearCacheStorage(), clearServiceWorkers()]);
storeCurrentVersion();
}
+13 -5
View File
@@ -12,6 +12,12 @@ export class RobustClipboardProvider implements IClipboardProvider {
if (this.pendingWrite !== null) {
const text = this.pendingWrite;
this.pendingWrite = null;
if (window.electronClipboard) {
window.electronClipboard.writeText(text).catch(() => {
this.pendingWrite = text;
});
return;
}
navigator.clipboard.writeText(text).catch(() => {
this.pendingWrite = text;
});
@@ -25,18 +31,20 @@ export class RobustClipboardProvider implements IClipboardProvider {
this.pendingWrite = null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
readText(selection: ClipboardSelectionType): string {
return "";
readText(_selection: ClipboardSelectionType): string | Promise<string> {
if (window.electronClipboard) {
return window.electronClipboard.readText();
}
return navigator.clipboard?.readText?.() ?? "";
}
async writeText(
selection: ClipboardSelectionType,
_selection: ClipboardSelectionType,
text: string,
): Promise<void> {
try {
if (window.electronClipboard) {
window.electronClipboard.writeText(text);
await window.electronClipboard.writeText(text);
return;
}
await navigator.clipboard.writeText(text);
+19 -6
View File
@@ -14,7 +14,18 @@
* to reflect the current UX contract: users can keep working regardless
* of backend hiccups and are simply informed via a toast.
*/
type EventListener = (...args: any[]) => void;
type EventListener = (...args: unknown[]) => void;
interface HttpLikeError {
message?: string;
code?: string;
response?: {
data?: {
error?: string;
code?: string;
};
};
}
class DatabaseHealthMonitor {
private static instance: DatabaseHealthMonitor;
@@ -47,7 +58,7 @@ class DatabaseHealthMonitor {
}
}
private emit(event: string, ...args: any[]): void {
private emit(event: string, ...args: unknown[]): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
eventListeners.forEach((listener) => listener(...args));
@@ -58,9 +69,11 @@ class DatabaseHealthMonitor {
this.emit("session-expired", { timestamp: Date.now() });
}
reportDatabaseError(error: any, _wasAuthenticated: boolean = false) {
const errorMessage = error?.response?.data?.error || error?.message || "";
const errorCode = error?.response?.data?.code || error?.code;
reportDatabaseError(error: unknown) {
const errorLike = error as HttpLikeError;
const errorMessage =
errorLike.response?.data?.error || errorLike.message || "";
const errorCode = errorLike.response?.data?.code || errorLike.code;
const lowerMessage = errorMessage.toLowerCase();
const isDatabaseError =
@@ -78,7 +91,7 @@ class DatabaseHealthMonitor {
errorCode === "ETIMEDOUT" ||
errorCode === "ERR_CANCELED" ||
(lowerMessage.includes("network error") &&
error?.response === undefined) ||
errorLike.response === undefined) ||
lowerMessage.includes("request aborted") ||
lowerMessage.includes("timeout");
+18
View File
@@ -0,0 +1,18 @@
type ElectronWindow = Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: {
isElectron?: boolean;
};
};
export function isElectron(): boolean {
if (typeof window === "undefined") return false;
const win = window as ElectronWindow;
const hasISElectron = win.IS_ELECTRON === true;
const hasElectronAPI = !!win.electronAPI;
const isElectronProp = win.electronAPI?.isElectron === true;
return hasISElectron || hasElectronAPI || isElectronProp;
}
+147 -29
View File
@@ -402,6 +402,8 @@
"currentVersion": "You are running version {{version}}",
"updateAvailable": "Update Available",
"newVersionAvailable": "A new version is available! You are running {{current}}, but {{latest}} is available.",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "Released on {{date}}",
"downloadUpdate": "Download Update",
"dismiss": "Dismiss",
@@ -426,6 +428,8 @@
"warning": "Warning",
"info": "Info",
"success": "Success",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "Loading...",
"required": "Required",
"optional": "Optional",
@@ -471,6 +475,7 @@
"chinese": "Chinese",
"german": "German",
"cancel": "Cancel",
"done": "Done",
"username": "Username",
"name": "Name",
"login": "Login",
@@ -486,6 +491,7 @@
"save": "Save",
"saving": "Saving...",
"delete": "Delete",
"rename": "Rename",
"edit": "Edit",
"add": "Add",
"search": "Search",
@@ -559,7 +565,8 @@
"passwordCopied": "Password copied to clipboard",
"sudoPasswordCopied": "Sudo password copied to clipboard",
"noPasswordAvailable": "No password available",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "Admin Settings",
@@ -876,7 +883,42 @@
"passwordMinLength": "Password must be at least 6 characters",
"currentRoles": "Current Roles",
"noRolesAssigned": "No roles assigned",
"assignNewRole": "Assign New Role"
"assignNewRole": "Assign New Role",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "Host Manager",
@@ -970,28 +1012,8 @@
"enableDocker": "Enable Docker",
"defaultPath": "Default Path",
"defaultPathDesc": "Default directory when opening file manager for this host",
"tunnelConnections": "Tunnel Connections",
"connection": "Connection",
"remove": "Remove",
"sourcePort": "Source Port",
"sourcePortDesc": " (Source refers to the Current Connection Details in the General tab)",
"endpointPort": "Endpoint Port",
"endpointSshConfig": "Endpoint SSH Configuration",
"tunnelForwardDescription": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum number of retry attempts for tunnel connection.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"autoStartContainer": "Auto Start on Container Launch",
"autoStartDesc": "Automatically start this tunnel when the container launches",
"addConnection": "Add Tunnel Connection",
"tunnelType": "Tunnel Type",
"tunnelTypeLocal": "Local (-L)",
"tunnelTypeRemote": "Remote (-R)",
"tunnelTypeLocalDesc": "Forward local port to remote endpoint",
"tunnelTypeRemoteDesc": "Forward remote port to local machine",
"tunnelForwardDescriptionLocal": "This tunnel will forward traffic from local port {{sourcePort}} to port {{endpointPort}} on the endpoint machine.",
"tunnelForwardDescriptionRemote": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.",
"sshpassRequired": "Sshpass Required For Password Authentication",
"sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.",
"otherInstallMethods": "Other installation methods:",
@@ -1656,6 +1678,7 @@
"automaticFallback": "Automatically trying {{method}} authentication...",
"totpTimeout": "TOTP verification timeout. Please reconnect.",
"passwordTimeout": "Password verification timeout. Please reconnect.",
"sessionEnded": "Session ended.",
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled.",
"sessionTakenOver": "Session was opened in another tab. Reconnecting...",
@@ -1993,12 +2016,10 @@
"ascending": "Ascending",
"descending": "Descending"
},
"tunnel": {
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started"
},
"tunnels": {
"title": "SSH Tunnels",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "No SSH Tunnels",
"createFirstTunnelMessage": "You haven't created any SSH tunnels yet. Configure tunnel connections in the Host Manager to get started.",
"connected": "Connected",
@@ -2019,27 +2040,100 @@
"disconnect": "Disconnect",
"cancel": "Cancel",
"port": "Port",
"localPort": "Local Port",
"remotePort": "Remote Port",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "Attempt {{current}} of {{max}}",
"nextRetryIn": "Next retry in {{seconds}} seconds",
"checkDockerLogs": "Check your Docker logs for the error reason, join the",
"orCreate": "or create a ",
"noTunnelConnections": "No tunnel connections configured",
"tunnelConnections": "Tunnel Connections",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "Add Tunnel",
"editTunnel": "Edit Tunnel",
"deleteTunnel": "Delete Tunnel",
"tunnelName": "Tunnel Name",
"localPort": "Local Port",
"remoteHost": "Remote Host",
"remotePort": "Remote Port",
"autoStart": "Auto Start",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "Status",
"active": "Active",
"inactive": "Inactive",
"start": "Start",
"stop": "Stop",
"test": "Test",
"restart": "Restart",
"connectionType": "Connection Type",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "Local",
"remote": "Remote",
"dynamic": "Dynamic",
@@ -2223,6 +2317,8 @@
"sshProvideCredentialsDescription": "Please provide your SSH credentials to connect to this server.",
"sshPasswordDescription": "Enter the password for this SSH connection.",
"sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "Step 1: Scan the QR code with your authenticator app",
"manualEntryCode": "Manual Entry Code",
"cannotScanQRText": "If you can't scan the QR code, enter this code manually in your authenticator app",
@@ -2297,7 +2393,7 @@
"failedCompleteReset": "Failed to complete password reset",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Failed to start OIDC login",
"failedUserInfo": "Failed to get user info after OIDC login",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC authentication failed",
"noTokenReceived": "No token received from login",
"invalidAuthUrl": "Invalid authorization URL received from backend",
@@ -2373,6 +2469,23 @@
"showHostTagsDesc": "Display tags under each host in the sidebar. Disable to hide all tags.",
"account": "Account",
"appearance": "Appearance",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "Language & Localization",
"fileManagerSettings": "File Manager",
"terminalSettings": "Terminal",
@@ -2421,6 +2534,10 @@
"description": "SSH credential description",
"searchCredentials": "Search credentials by name, username, or tags...",
"sshConfig": "endpoint ssh configuration",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/home",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
@@ -2600,6 +2717,7 @@
"version": "Version",
"upToDate": "Up to Date",
"updateAvailable": "Update Available",
"beta": "Beta",
"uptime": "Uptime",
"database": "Database",
"healthy": "Healthy",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Jy gebruik weergawe {{version}}",
"updateAvailable": "Opdatering beskikbaar",
"newVersionAvailable": "'n Nuwe weergawe is beskikbaar! Jy gebruik {{current}}, maar {{latest}} is beskikbaar.",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "Uitgereik op {{date}}",
"downloadUpdate": "Laai opdatering af",
"dismiss": "Maak toe",
@@ -426,6 +428,8 @@
"warning": "Waarskuwing",
"info": "Inligting",
"success": "Sukses",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "Laai tans...",
"required": "Vereis",
"optional": "Opsioneel",
@@ -471,6 +475,7 @@
"chinese": "Chinees",
"german": "Duits",
"cancel": "Kanselleer",
"done": "Done",
"username": "Gebruikersnaam",
"name": "Naam",
"login": "Aanmeld",
@@ -486,6 +491,7 @@
"save": "Stoor",
"saving": "Stoor...",
"delete": "Vee uit",
"rename": "Rename",
"edit": "Wysig",
"add": "Voeg by",
"search": "Soek",
@@ -559,7 +565,8 @@
"passwordCopied": "Wagwoord gekopieer na knipbord",
"sudoPasswordCopied": "Sudo-wagwoord na knipbord gekopieer",
"noPasswordAvailable": "Geen wagwoord beskikbaar nie",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "Admin-instellings",
@@ -868,7 +875,42 @@
"passwordMinLength": "Wagwoord moet ten minste 6 karakters wees",
"currentRoles": "Huidige Rolle",
"noRolesAssigned": "Geen rolle toegeken nie",
"assignNewRole": "Ken Nuwe Rol Toe"
"assignNewRole": "Ken Nuwe Rol Toe",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "Gasheerbestuurder",
@@ -962,28 +1004,8 @@
"enableDocker": "Aktiveer Docker",
"defaultPath": "Standaardpad",
"defaultPathDesc": "Standaardgids wanneer lêerbestuurder vir hierdie gasheer oopgemaak word",
"tunnelConnections": "Tonnelverbindings",
"connection": "Verbinding",
"remove": "Verwyder",
"sourcePort": "Bronpoort",
"sourcePortDesc": " (Bron verwys na die Huidige Verbindingsbesonderhede in die Algemeen-oortjie)",
"endpointPort": "Eindpuntpoort",
"endpointSshConfig": "Eindpunt SSH-konfigurasie",
"tunnelForwardDescription": "Hierdie tonnel sal verkeer vanaf poort {{sourcePort}} op die bronmasjien (huidige verbindingsbesonderhede in die algemene oortjie) na poort {{endpointPort}} op die eindpuntmasjien aanstuur.",
"maxRetries": "Maksimum herprobeer",
"maxRetriesDescription": "Maksimum aantal herprobeerpogings vir tonnelverbinding.",
"retryInterval": "Herprobeerinterval (sekondes)",
"retryIntervalDescription": "Tyd om te wag tussen herpogings.",
"autoStartContainer": "Outomatiese begin met houerlansering",
"autoStartDesc": "Begin hierdie tonnel outomaties wanneer die houer begin",
"addConnection": "Voeg tonnelverbinding by",
"tunnelType": "Tonnel Tipe",
"tunnelTypeLocal": "Plaaslik (-L)",
"tunnelTypeRemote": "Afstandbeheer (-R)",
"tunnelTypeLocalDesc": "Stuur plaaslike poort na afgeleë eindpunt aan",
"tunnelTypeRemoteDesc": "Stuur afgeleë poort na plaaslike masjien aan",
"tunnelForwardDescriptionLocal": "Hierdie tonnel sal verkeer vanaf die plaaslike poort {{sourcePort}} na poort {{endpointPort}} op die eindpuntmasjien aanstuur.",
"tunnelForwardDescriptionRemote": "Hierdie tonnel sal verkeer vanaf poort {{sourcePort}} op die bronmasjien (huidige verbindingsbesonderhede in die algemene oortjie) na poort {{endpointPort}} op die eindpuntmasjien aanstuur.",
"sshpassRequired": "Sshpass word vereis vir wagwoordverifikasie",
"sshpassRequiredDesc": "Vir wagwoordverifikasie in tonnels moet sshpass op die stelsel geïnstalleer wees.",
"otherInstallMethods": "Ander installasiemetodes:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Vertikaal verdeel",
"closePanel": "Maak paneel toe",
"reconnect": "Herkoppel",
"sessionEnded": "Sessie geëindig",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "FOUT: {{message}}",
"disconnected": "Ontkoppel",
@@ -1933,22 +1955,10 @@
"ascending": "Stygend",
"descending": "Dalend"
},
"tunnel": {
"log": {
"connecting": "Vestig tans tonnelverbinding...",
"sourceConnecting": "Verbind met brongasheer {{host}}...",
"sourceConnected": "Gekoppel aan brongasheer",
"endpointResolving": "Los eindpuntgasheer {{host}}op ...",
"endpointResolved": "Eindpuntgasheer opgelos",
"forwarding": "Stel poortaanstuuring op {{localPort}} → {{remotePort}}...",
"connected": "Tonnel suksesvol gebou",
"retrying": "Verbinding het misluk, probeer weer oor {{countdown}}s (poging {{attempt}}/{{max}})...",
"retryExhausted": "Maksimum herprobeerpogings bereik",
"error": "Tonnelverbinding het misluk: {{error}}"
}
},
"tunnels": {
"title": "SSH-tonnels",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "Geen SSH-tonnels nie",
"createFirstTunnelMessage": "Jy het nog geen SSH-tonnels geskep nie. Konfigureer tonnelverbindings in die Gasheerbestuurder om te begin.",
"connected": "Verbonde",
@@ -1969,27 +1979,100 @@
"disconnect": "Ontkoppel",
"cancel": "Kanselleer",
"port": "Hawe",
"localPort": "Plaaslike Hawe",
"remotePort": "Afstandpoort",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "Poging {{current}} van {{max}}",
"nextRetryIn": "Volgende herprobeer oor {{seconds}} sekondes",
"checkDockerLogs": "Gaan jou Docker-logboeke na vir die foutrede, sluit aan by die",
"orCreate": "of skep 'n ",
"noTunnelConnections": "Geen tonnelverbindings gekonfigureer nie",
"tunnelConnections": "Tonnelverbindings",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "Voeg tonnel by",
"editTunnel": "Wysig Tonnel",
"deleteTunnel": "Vee Tonnel uit",
"tunnelName": "Tonnelnaam",
"localPort": "Plaaslike Hawe",
"remoteHost": "Afstandgasheer",
"remotePort": "Afstandpoort",
"autoStart": "Outomatiese Begin",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "Status",
"active": "Aktief",
"inactive": "Onaktief",
"start": "Begin",
"stop": "Stop",
"test": "Test",
"restart": "Herbegin",
"connectionType": "Verbindingstipe",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "Plaaslik",
"remote": "Afstandsbediening",
"dynamic": "Dinamiese",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Verskaf asseblief u SSH-besonderhede om aan hierdie bediener te koppel.",
"sshPasswordDescription": "Voer die wagwoord vir hierdie SSH-verbinding in.",
"sshKeyPasswordDescription": "As jou SSH-sleutel geïnkripteer is, voer die wagwoordfrase hier in.",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "Stap 1: Skandeer die QR-kode met jou verifikasie-app",
"manualEntryCode": "Handmatige Invoerkode",
"cannotScanQRText": "As jy nie die QR-kode kan skandeer nie, voer hierdie kode handmatig in jou verifikasie-app in.",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Kon nie wagwoordterugstelling voltooi nie",
"invalidTotpCode": "Ongeldige TOTP-kode",
"failedOidcLogin": "Kon nie OIDC-aanmelding begin nie",
"failedUserInfo": "Kon nie gebruikersinligting kry na OIDC-aanmelding nie",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC-verifikasie het misluk",
"noTokenReceived": "Geen teken ontvang van aanmelding nie",
"invalidAuthUrl": "Ongeldige magtigings-URL ontvang van backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Wys etikette onder elke gasheer in die sybalk. Deaktiveer om alle etikette te versteek.",
"account": "Rekening",
"appearance": "Voorkoms",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "Taal en lokalisering",
"fileManagerSettings": "Lêerbestuurder",
"terminalSettings": "Terminaal",
@@ -2370,6 +2472,10 @@
"description": "SSH-bewysbeskrywing",
"searchCredentials": "Soek geloofsbriewe volgens naam, gebruikersnaam of etikette...",
"sshConfig": "eindpunt ssh-konfigurasie",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/tuis",
"clientId": "jou-kliënt-ID",
"clientSecret": "jou-kliënt-geheim",
@@ -2549,6 +2655,7 @@
"version": "Weergawe",
"upToDate": "Op datum",
"updateAvailable": "Opdatering beskikbaar",
"beta": "Beta",
"uptime": "Optyd",
"database": "Databasis",
"healthy": "Gesond",
@@ -2955,6 +3062,20 @@
"error": "Metrieke-insameling het misluk: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Vestig tans tonnelverbinding...",
"sourceConnecting": "Verbind met brongasheer {{host}}...",
"sourceConnected": "Gekoppel aan brongasheer",
"endpointResolving": "Los eindpuntgasheer {{host}}op ...",
"endpointResolved": "Eindpuntgasheer opgelos",
"forwarding": "Stel poortaanstuuring op {{localPort}} → {{remotePort}}...",
"connected": "Tonnel suksesvol gebou",
"retrying": "Verbinding het misluk, probeer weer oor {{countdown}}s (poging {{attempt}}/{{max}})...",
"retryExhausted": "Maksimum herprobeerpogings bereik",
"error": "Tonnelverbinding het misluk: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Verbind met SFTP-bediener...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "أنت تقوم بتشغيل الإصدار {{version}}",
"updateAvailable": "تحديث متوفر",
"newVersionAvailable": "يتوفر إصدار جديد! أنت تقوم بتشغيل {{current}}، ولكن {{latest}} متاح.",
"betaVersion": "نسخة بيتا",
"betaVersionDesc": "أنت تقوم بتشغيل {{current}}، وهو أحدث من أحدث إصدار مستقر {{latest}}.",
"releasedOn": "صدر على {{date}}",
"downloadUpdate": "تنزيل التحديث",
"dismiss": "تجاهل",
@@ -426,6 +428,8 @@
"warning": "تحذير",
"info": "معلومات",
"success": "نجاح",
"unsavedChanges": "التغييرات غير المحفوظة",
"dismiss": "تجاهل",
"loading": "تحميل...",
"required": "مطلوب",
"optional": "اختياري",
@@ -471,6 +475,7 @@
"chinese": "الصينية",
"german": "الألمانية",
"cancel": "إلغاء",
"done": "تم",
"username": "اسم المستخدم",
"name": "الاسم",
"login": "تسجيل الدخول",
@@ -486,6 +491,7 @@
"save": "حفظ",
"saving": "حفظ...",
"delete": "حذف",
"rename": "إعادة تسمية",
"edit": "تحرير",
"add": "إضافة",
"search": "البحث",
@@ -559,7 +565,8 @@
"passwordCopied": "تم نسخ كلمة المرور إلى الحافظة",
"sudoPasswordCopied": "تم نسخ كلمة المرور إلى الحافظة",
"noPasswordAvailable": "لا توجد كلمة مرور متاحة",
"failedToCopyPassword": "فشل في نسخ كلمة المرور"
"failedToCopyPassword": "فشل في نسخ كلمة المرور",
"openFileManager": "فتح مدير الملفات"
},
"admin": {
"title": "إعدادات المدير",
@@ -868,7 +875,42 @@
"passwordMinLength": "يجب أن تتكون كلمة المرور من 6 أحرف على الأقل",
"currentRoles": "الأدوار الحالية",
"noRolesAssigned": "لا توجد أدوار مسندة",
"assignNewRole": "تعيين دور جديد"
"assignNewRole": "تعيين دور جديد",
"apiKeys": {
"tabLabel": "مفاتيح API",
"title": "مفاتيح API",
"createApiKey": "إنشاء مفتاح API",
"createApiKeyDescription": "إنشاء مفتاح API جديد تم نطاقه لمستخدم محدد. الرمز المميز يظهر لمرة واحدة فقط.",
"keyCreated": "تم إنشاء مفتاح API",
"keyCreatedDescription": "انسخ هذا المفتاح الآن - لن يظهر مرة أخرى.",
"keyName": "اسم المفتاح",
"keyNamePlaceholder": "مثال: CI/CD Pipeline",
"scopedUser": "المستخدم المستعرض",
"selectUser": "اختر مستخدم...",
"searchUsers": "البحث عن المستخدمين...",
"noUsersFound": "لم يتم العثور على مستخدمين.",
"expiresAt": "تنتهي في",
"optional": "اختياري",
"expiresAtHelp": "اتركه فارغاً لمفتاح لا تنتهي صلاحيته أبداً.",
"copyWarningTitle": "حفظ مفتاح API الخاص بك",
"copyWarningDescription": "سيتم عرض هذا المفتاح مرة واحدة فقط. قم بتخزينه في مكان آمن.",
"apiKey": "مفتاح API",
"tokenCopied": "تم نسخ الرمز المميز إلى الحافظة",
"creating": "إنشاء...",
"nameRequired": "اسم المفتاح مطلوب",
"userRequired": "الرجاء تحديد مستخدم",
"failedToCreate": "فشل إنشاء مفتاح API",
"noKeys": "لم يتم العثور على مفاتيح API.",
"name": "الاسم",
"prefix": "بادئة",
"lastUsed": "آخر استخدام",
"never": "لا",
"revokeKey": "إلغاء المفتاح",
"confirmRevoke": "هل أنت متأكد من أنك تريد إلغاء مفتاح API \"{{name}}\"؟ هذا لا يمكن التراجع عنه.",
"revokedSuccessfully": "تم إلغاء مفتاح API بنجاح",
"failedToRevoke": "فشل في إلغاء مفتاح API",
"failedToFetch": "فشل في جلب مفاتيح API"
}
},
"hosts": {
"title": "مدير المضيف",
@@ -962,28 +1004,8 @@
"enableDocker": "تمكين Docker",
"defaultPath": "المسار الافتراضي",
"defaultPathDesc": "الدليل الافتراضي عند فتح مدير الملفات لهذا المضيف",
"tunnelConnections": "اتصالات النفق",
"connection": "اتصال",
"remove": "إزالة",
"sourcePort": "منفذ المصدر",
"sourcePortDesc": " (المصدر يشير إلى تفاصيل الاتصال الحالية في علامة التبويب العام)",
"endpointPort": "منفذ نقطة النهاية",
"endpointSshConfig": "تكوين SSH نقطة النهاية",
"tunnelForwardDescription": "سيؤدي هذا النفق إلى نقل حركة المرور من المنفذ {{sourcePort}} على جهاز المصدر (تفاصيل الاتصال الحالي في التبويب العام) إلى المنفذ {{endpointPort}} على جهاز نقطة النهاية.",
"maxRetries": "الحد الاقصى لمحاولات",
"maxRetriesDescription": "الحد الأقصى لعدد محاولات إعادة المحاولة للاتصال بالأنفاق.",
"retryInterval": "الفاصل الزمني لإعادة المحاولة (بالثواني)",
"retryIntervalDescription": "حان وقت الانتظار بين محاولات إعادة المحاولة.",
"autoStartContainer": "بدء التشغيل التلقائي عند تشغيل الحاوية",
"autoStartDesc": "تشغيل هذا النفق تلقائياً عند تشغيل الحاوية",
"addConnection": "إضافة اتصال نفق",
"tunnelType": "نوع النفق",
"tunnelTypeLocal": "محلي (-L)",
"tunnelTypeRemote": "عن بعد (-R)",
"tunnelTypeLocalDesc": "إعادة توجيه المنفذ المحلي إلى نقطة النهاية البعيدة",
"tunnelTypeRemoteDesc": "إعادة توجيه المنفذ البعيد إلى الجهاز المحلي",
"tunnelForwardDescriptionLocal": "سيؤدي هذا النفق إلى نقل الحركة من المنفذ المحلي {{sourcePort}} إلى المنفذ {{endpointPort}} على آلة نقطة النهاية.",
"tunnelForwardDescriptionRemote": "سيؤدي هذا النفق إلى نقل حركة المرور من المنفذ {{sourcePort}} على جهاز المصدر (تفاصيل الاتصال الحالي في التبويب العام) إلى المنفذ {{endpointPort}} على جهاز نقطة النهاية.",
"sshpassRequired": "تجاوز مطلوب للمصادقة على كلمة المرور",
"sshpassRequiredDesc": "للمصادقة بكلمة المرور في الأنفاق، يجب تثبيت Sshpassعلى النظام.",
"otherInstallMethods": "طرق تثبيت أخرى:",
@@ -1529,7 +1551,7 @@
"splitVertical": "تقسيم عمودي",
"closePanel": "أغلق اللوحة",
"reconnect": "إعادة الاتصال",
"sessionEnded": "انتهت الجلسة",
"sessionEnded": "انتهت الجلسة.",
"connectionLost": "تم فقدان الاتصال",
"error": "خطأ: {{message}}",
"disconnected": "قطع",
@@ -1933,22 +1955,10 @@
"ascending": "تصاعدي",
"descending": "تنازلي"
},
"tunnel": {
"log": {
"connecting": "إنشاء اتصال بالنفق...",
"sourceConnecting": "جاري الاتصال بمضيف المصدر {{host}}...",
"sourceConnected": "متصل بالمستضيف المصدر",
"endpointResolving": "حل مضيف نقطة النهاية {{host}}...",
"endpointResolved": "تم حل مضيف نقطة النهاية",
"forwarding": "إعداد إعادة توجيه المنفذ {{localPort}} → {{remotePort}}...",
"connected": "تم إنشاء النفق بنجاح",
"retrying": "فشل الاتصال، إعادة المحاولة في {{countdown}}s (محاولة {{attempt}}/{{max}})...",
"retryExhausted": "وصلت محاولات إعادة المحاولة القصوى",
"error": "فشل الاتصال بالنفق: {{error}}"
}
},
"tunnels": {
"title": "أنفاق SSH",
"noTunnelsConfigured": "لم يتم تكوين الأنفاق",
"configureTunnelsInHostSettings": "تكوين اتصالات الأنفاق في مدير المضيف للبدء",
"noSshTunnels": "لا توجد أنفاق SSH",
"createFirstTunnelMessage": "لم تقم بإنشاء أي أنفاق SSH حتى الآن. تكوين اتصالات الأنفاق في مدير المضيف للبدء.",
"connected": "متصل",
@@ -1969,27 +1979,100 @@
"disconnect": "قطع الاتصال",
"cancel": "إلغاء",
"port": "المنفذ",
"localPort": "المنفذ المحلي",
"remotePort": "المنفذ البعيد",
"currentHostPort": "منفذ المضيف الحالي",
"endpointPort": "منفذ نقطة النهاية",
"bindIp": "IP المحلي",
"currentHostIp": "المستضيف الحالي IP",
"endpointSshConfig": "تكوين SSH نقطة النهاية",
"endpointSshConfigRequired": "تكوين نقطة النهاية SSH مطلوب",
"endpointSshHost": "مضيف SSH نقطة النهاية",
"endpointSshHostPlaceholder": "حدد مضيف مكون",
"endpointSshHostRequired": "حدد مضيف SSH لنقطة النهاية لكل نفق عميل.",
"attempt": "محاولة {{current}} من {{max}}",
"nextRetryIn": "إعادة المحاولة التالية في {{seconds}} ثانية",
"checkDockerLogs": "تحقق من سجلات Docker لسبب الخطأ، انضم إلى",
"orCreate": "أو إنشاء ",
"noTunnelConnections": "لا توجد اتصالات نفق تم تكوينها",
"tunnelConnections": "اتصالات النفق",
"serverTunnels": "أنفاق الخادم",
"serverTunnelsDesc": "الأنفاق المدارة الخلفية المخزنة مع هذا المضيف.",
"clientTunnels": "أنفاق العميل",
"clientTunnelsUnavailable": "تتطلب أنفاق العميل عميل سطح المكتب.",
"serverTunnel": "نفق الخادم",
"clientTunnel": "نفق العميل",
"addServerTunnel": "إضافة نفق خادم",
"addClientTunnel": "إضافة نفق للعميل",
"noServerTunnels": "لم يتم تكوين أنفاق الخادم.",
"noClientTunnels": "لم يتم تكوين أنفاق العميل على هذا المكتب.",
"manageClientTunnels": "إدارة أنفاق العميل",
"addTunnel": "إضافة نفق",
"editTunnel": "تعديل النفق",
"deleteTunnel": "حذف النفق",
"tunnelName": "اسم النفق",
"localPort": "المنفذ المحلي",
"remoteHost": "المضيف البعيد",
"remotePort": "المنفذ البعيد",
"autoStart": "بدء التشغيل التلقائي",
"autoStartContainer": "بدء التشغيل التلقائي عند التشغيل",
"autoStartContainerDesc": "تشغيل هذا النفق تلقائياً عند تشغيل خادم Termix الخاص بك.",
"autoStartEnableFailed": "تم حفظ المضيف، ولكن فشل في بدء الأنفاق تلقائياً ل {{name}}.",
"clientAutoStartDesc": "يبدأ عندما يفتح عميل سطح المكتب هذا ويبقى متصلا.",
"clientManualStartDesc": "استخدام البدء ووقف من هذا الصف. لا تفتحه Terمزيكس تلقائياً.",
"clientRemoteServerNote": "قد تتطلب إعادة التوجيه عن بعد AllowTcpForarding و بوابة المنافذ على خادم SSH نقطة النهائية. المنفذ البعيد يغلق عند قطع اتصال سطح المكتب.",
"clientTunnelStarted": "بدأ نفق العميل",
"clientTunnelStopped": "توقف نفق العميل",
"tunnelTestSucceeded": "نجاح اختبار النفق",
"tunnelTestFailed": "فشل اختبار النفق",
"localSaved": "أنفاق العميل المحفوظة",
"localSaveError": "فشل في حفظ أنفاق العميل المحلي",
"invalidBindIp": "يجب أن يكون IP المحلي عنوان IPv4 صالح.",
"invalidLocalTargetIp": "يجب أن يكون عنوان IP المستهدف المحلي عنوان IPv4 صالح.",
"invalidCurrentHostIp": "يجب أن يكون عنوان IP المضيف الحالي عنوان IPv4 صالح.",
"invalidLocalPort": "يجب أن يكون المنفذ المحلي بين 1 و 65535.",
"invalidRemotePort": "يجب أن يكون المنفذ البعيد بين 1 و 65535.",
"invalidLocalTargetPort": "يجب أن يكون المنفذ المستهدف المحلي بين 1 و 65535.",
"invalidEndpointPort": "منفذ نقطة النهاية يجب أن يكون بين 1 و 65535.",
"duplicateAutoStartBind": "يمكن استخدام نفق عميل واحد لبدء التشغيل التلقائي فقط {{bind}}.",
"manualControlError": "فشل تحديث حالة النفق.",
"saveHostBeforeManualControl": "حفظ هذا المضيف قبل بدء أو إيقاف أنفاق الخادم.",
"status": "الحالة",
"active": "نشط",
"inactive": "غير نشط",
"start": "ابدأ",
"stop": "توقف",
"test": "اختبار",
"restart": "إعادة التشغيل",
"connectionType": "نوع الاتصال",
"type": "نوع النفق",
"typeLocal": "محلي (-L)",
"typeRemote": "عن بعد (-R)",
"typeDynamic": "ديناميكية (-D)",
"typeServerLocalDesc": "المضيف الحالي إلى نقطة النهاية.",
"typeServerRemoteDesc": "نقطة العودة إلى المضيف الحالي.",
"typeClientLocalDesc": "كمبيوتر محلي لنهاية النقاش.",
"typeClientRemoteDesc": "نهاية العودة إلى الكمبيوتر المحلي.",
"typeClientDynamicDesc": "SOCKS على الكمبيوتر المحلي.",
"typeDynamicDesc": "إعادة توجيه حركة الاتصال SOCKS5 عبر SSH",
"forwardDescriptionServerLocal": "المضيف الحالي {{sourcePort}} → نقطة النهاية {{endpointPort}}.",
"forwardDescriptionServerRemote": "نقطة النهاية {{endpointPort}} → المضيف الحالي {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS على المضيف الحالي {{sourcePort}}.",
"forwardDescriptionClientLocal": "محلي {{sourcePort}} → {{endpointPort}} البعيد.",
"forwardDescriptionClientRemote": "عن بعد {{sourcePort}} → المحلي {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS على المنفذ المحلي {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "محلي {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → المحلي {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} عبر {{endpoint}}",
"route": "المسار:",
"lastStarted": "بدأت آخر مرة",
"lastTested": "اختبارها الأخير",
"lastError": "آخر خطأ",
"maxRetries": "الحد الاقصى لمحاولات",
"maxRetriesDescription": "الحد الأقصى لمحاولات إعادة المحاولة.",
"retryInterval": "الفاصل الزمني لإعادة المحاولة (بالثواني)",
"retryIntervalDescription": "حان وقت الانتظار بين محاولات إعادة المحاولة.",
"local": "محلي",
"remote": "عن بعد",
"dynamic": "ديناميكية",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "الرجاء تقديم بيانات اعتماد SSH الخاصة بك للاتصال بهذا الخادم.",
"sshPasswordDescription": "أدخل كلمة المرور لهذا الاتصال SSH.",
"sshKeyPasswordDescription": "إذا تم تشفير مفتاح SSH الخاص بك، أدخل كلمة المرور هنا.",
"passphraseRequired": "كلمة المرور مطلوبة",
"passphraseRequiredDescription": "مفتاح SSH مشفر. الرجاء إدخال عبارة المرور لفتحها.",
"step1ScanQR": "الخطوة 1: مسح رمز QR باستخدام تطبيق المصادقة الخاص بك",
"manualEntryCode": "رمز الإدخال اليدوي",
"cannotScanQRText": "إذا لم تتمكن من مسح رمز QR ، أدخل هذا الرمز يدوياً في تطبيق المصادقة الخاص بك",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "فشل في إكمال إعادة تعيين كلمة المرور",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "فشل في بدء تسجيل الدخول OIDC",
"failedUserInfo": "فشل الحصول على معلومات المستخدم بعد تسجيل الدخول OIDC",
"failedUserInfo": "فشل الحصول على معلومات المستخدم بعد تسجيل الدخول",
"oidcAuthFailed": "فشل مصادقة OIDC",
"noTokenReceived": "لم يتم استلام رمز من تسجيل الدخول",
"invalidAuthUrl": "رابط تفويض غير صالح مستلم من الخلفية",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "عرض العلامات تحت كل مضيف في الشريط الجانبي. تعطيل لإخفاء جميع العلامات.",
"account": "حساب",
"appearance": "المظهر",
"c2sTunnelConfigDesc": "أنفاق سطح المكتب المحلية التي تستهدف مضيفي SSH.",
"c2sTunnelPresets": "الإعدادات المسبقة للنفق للعميل",
"c2sTunnelPresetsDesc": "حفظ قائمة النفق المحلية لهذا العميل سطح المكتب كخادم مسبقا، أو تحميل الإعداد المسبق لهذا العميل مرة أخرى.",
"c2sTunnelPresetsUnavailable": "تتوفر الإعدادات المسبقة لنفق العميل فقط في عميل سطح المكتب.",
"c2sPresetName": "اسم الإعداد المسبق",
"c2sPresetNamePlaceholder": "اسم العميل المسبق",
"c2sPresetToLoad": "الإعداد المسبق للتحميل",
"c2sNoPresetSelected": "لم يتم تحديد الإعداد المسبق",
"c2sNoPresets": "لم يتم حفظ أي إعدادات مسبقة",
"c2sLoadPreset": "تحميل",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "الإعداد المسبق عبارة عن لقطات واضحة؛ تحميل واحد يحل محل قائمة نفق عميل سطح المكتب هذه.",
"c2sPresetSaved": "تم حفظ الإعداد المسبق لنفق العميل",
"c2sPresetLoaded": "تم تحميل نفق العميل مسبقاً محلياً",
"c2sPresetRenamed": "تم إعادة تسمية نفق العميل مسبقاً",
"c2sPresetDeleted": "تم حذف الضبط المسبق لنفق العميل",
"c2sPresetLoadError": "فشل تحميل الإعدادات المسبقة لنفق العميل",
"languageLocalization": "اللغة والتعريب",
"fileManagerSettings": "مدير الملفات",
"terminalSettings": "المحطة",
@@ -2370,6 +2472,10 @@
"description": "وصف اعتماد SSH",
"searchCredentials": "البحث عن بيانات الاعتماد حسب الاسم أو اسم المستخدم أو العلامات...",
"sshConfig": "تكوين ssh نقطة النهاية",
"bindLocalhost": "127.0.0.1 (ربط بالمضيف المحلي)",
"localListenerHost": "127.0.0.1 (الاستماع محليا)",
"localTargetHost": "127-0.0.1 (الهدف على هذا الحاسوب)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/ منزل",
"clientId": "معرف عميلك-العميل",
"clientSecret": "سر-عملاءك",
@@ -2549,6 +2655,7 @@
"version": "الإصدار",
"upToDate": "حتى التاريخ",
"updateAvailable": "تحديث متوفر",
"beta": "بيتا",
"uptime": "وقت التحديث",
"database": "قاعدة البيانات",
"healthy": "صحي",
@@ -2955,6 +3062,20 @@
"error": "فشلت مجموعة المقاييس: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "إنشاء اتصال بالنفق...",
"sourceConnecting": "جاري الاتصال بمضيف المصدر {{host}}...",
"sourceConnected": "متصل بالمستضيف المصدر",
"endpointResolving": "حل مضيف نقطة النهاية {{host}}...",
"endpointResolved": "تم حل مضيف نقطة النهاية",
"forwarding": "إعداد إعادة توجيه المنفذ {{localPort}} → {{remotePort}}...",
"connected": "تم إنشاء النفق بنجاح",
"retrying": "فشل الاتصال، إعادة المحاولة في {{countdown}}s (محاولة {{attempt}}/{{max}})...",
"retryExhausted": "وصلت محاولات إعادة المحاولة القصوى",
"error": "فشل الاتصال بالنفق: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "جاري الاتصال بخادم SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Използвате версия {{version}}",
"updateAvailable": "Налична е актуализация",
"newVersionAvailable": "Налична е нова версия! Използвате {{current}}, но {{latest}} е наличен.",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "Публикувано на {{date}}",
"downloadUpdate": "Изтегляне на актуализация",
"dismiss": "Отхвърляне",
@@ -426,6 +428,8 @@
"warning": "Предупреждение",
"info": "Информация",
"success": "Успех",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "Зареждане...",
"required": "Задължително",
"optional": "По избор",
@@ -471,6 +475,7 @@
"chinese": "китайски",
"german": "Немски",
"cancel": "Отказ",
"done": "Done",
"username": "Потребителско име",
"name": "Име",
"login": "Вход",
@@ -486,6 +491,7 @@
"save": "Запазване",
"saving": "Запазване...",
"delete": "Изтриване",
"rename": "Rename",
"edit": "Редактиране",
"add": "Добавяне",
"search": "Търсене",
@@ -559,7 +565,8 @@
"passwordCopied": "Паролата е копирана в клипборда",
"sudoPasswordCopied": "Паролата за Sudo е копирана в клипборда",
"noPasswordAvailable": "Няма налична парола",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "Административни настройки",
@@ -868,7 +875,42 @@
"passwordMinLength": "Паролата трябва да е поне 6 знака",
"currentRoles": "Текущи роли",
"noRolesAssigned": "Няма зададени роли",
"assignNewRole": "Присвояване на нова роля"
"assignNewRole": "Присвояване на нова роля",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "Мениджър на домакини",
@@ -962,28 +1004,8 @@
"enableDocker": "Активиране на Докер",
"defaultPath": "Път по подразбиране",
"defaultPathDesc": "Директория по подразбиране при отваряне на файловия мениджър за този хост",
"tunnelConnections": "Тунелни връзки",
"connection": "Връзка",
"remove": "Премахване",
"sourcePort": "Източник Порт",
"sourcePortDesc": " (Източникът се отнася до „Текущи данни за връзката“ в раздела „Общи“)",
"endpointPort": "Порт за крайна точка",
"endpointSshConfig": "SSH конфигурация на крайна точка",
"tunnelForwardDescription": "Този тунел ще пренасочва трафика от порт {{sourcePort}} на изходната машина (текущи подробности за връзката в раздела „Общи“) към порт {{endpointPort}} на крайната машина.",
"maxRetries": "Максимален брой повторни опити",
"maxRetriesDescription": "Максимален брой повторни опити за тунелна връзка.",
"retryInterval": "Интервал на повторен опит (секунди)",
"retryIntervalDescription": "Време за изчакване между опитите за повторен опит.",
"autoStartContainer": "Автоматично стартиране при стартиране на контейнер",
"autoStartDesc": "Автоматично стартиране на този тунел при стартиране на контейнера",
"addConnection": "Добавяне на тунелна връзка",
"tunnelType": "Тип тунел",
"tunnelTypeLocal": "Местен (-L)",
"tunnelTypeRemote": "Дистанционно (-R)",
"tunnelTypeLocalDesc": "Пренасочване на локален порт към отдалечена крайна точка",
"tunnelTypeRemoteDesc": "Пренасочване на отдалечен порт към локална машина",
"tunnelForwardDescriptionLocal": "Този тунел ще пренасочва трафика от локалния порт {{sourcePort}} към порт {{endpointPort}} на крайната машина.",
"tunnelForwardDescriptionRemote": "Този тунел ще пренасочва трафика от порт {{sourcePort}} на машината източник (текущи подробности за връзката в раздела „Общи“) към порт {{endpointPort}} на машината източник.",
"sshpassRequired": "Sshpass е необходим за удостоверяване с парола",
"sshpassRequiredDesc": "За удостоверяване с парола в тунели, sshpass трябва да бъде инсталиран на системата.",
"otherInstallMethods": "Други методи за монтаж:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Разделяне по вертикала",
"closePanel": "Затвори панела",
"reconnect": "Свържете се отново",
"sessionEnded": "Сесията приключи",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "ГРЕШКА: {{message}}",
"disconnected": "Изключен",
@@ -1933,22 +1955,10 @@
"ascending": "Възходящ",
"descending": "Низходящо"
},
"tunnel": {
"log": {
"connecting": "Установяване на тунелна връзка...",
"sourceConnecting": "Свързване с изходния хост {{host}}...",
"sourceConnected": "Свързан с изходния хост",
"endpointResolving": "Разрешаване на хост на крайна точка {{host}}...",
"endpointResolved": "Хостът на крайната точка е разрешен",
"forwarding": "Настройване на пренасочване на портове {{localPort}} → {{remotePort}}...",
"connected": "Тунелът е построен успешно",
"retrying": "Връзката не бе успешна, опитът ще бъде повторен след {{countdown}}сек (опит {{attempt}}/{{max}})...",
"retryExhausted": "Достигнат е максималният брой опити за повторен опит",
"error": "Тунелна връзка неуспешна: {{error}}"
}
},
"tunnels": {
"title": "SSH тунели",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "Няма SSH тунели",
"createFirstTunnelMessage": "Все още не сте създали SSH тунели. Конфигурирайте тунелни връзки в Host Manager, за да започнете.",
"connected": "Свързан",
@@ -1969,27 +1979,100 @@
"disconnect": "Изключване",
"cancel": "Отказ",
"port": "Порт",
"localPort": "Локален порт",
"remotePort": "Отдалечен порт",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "Опит {{current}} от {{max}}",
"nextRetryIn": "Следващ опит след {{seconds}} секунди",
"checkDockerLogs": "Проверете лог файловете на Docker за причината за грешката, присъединете се към",
"orCreate": "или създайте ",
"noTunnelConnections": "Няма конфигурирани тунелни връзки",
"tunnelConnections": "Тунелни връзки",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "Добавяне на тунел",
"editTunnel": "Редактиране на тунел",
"deleteTunnel": "Изтриване на тунел",
"tunnelName": "Име на тунела",
"localPort": "Локален порт",
"remoteHost": "Отдалечен хост",
"remotePort": "Отдалечен порт",
"autoStart": "Автоматично стартиране",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "Статус",
"active": "Активен",
"inactive": "Неактивен",
"start": "Старт",
"stop": "Спри",
"test": "Test",
"restart": "Рестартиране",
"connectionType": "Тип връзка",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "Местно",
"remote": "Дистанционно",
"dynamic": "Динамичен",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Моля, предоставете вашите SSH идентификационни данни, за да се свържете с този сървър.",
"sshPasswordDescription": "Въведете паролата за тази SSH връзка.",
"sshKeyPasswordDescription": "Ако вашият SSH ключ е криптиран, въведете паролата тук.",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "Стъпка 1: Сканирайте QR кода с приложението си за удостоверяване",
"manualEntryCode": "Код за ръчно въвеждане",
"cannotScanQRText": "Ако не можете да сканирате QR кода, въведете го ръчно в приложението си за удостоверяване",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Неуспешно завършване на нулирането на паролата",
"invalidTotpCode": "Невалиден TOTP код",
"failedOidcLogin": "Стартирането на входа в OIDC не бе успешно",
"failedUserInfo": "Неуспешно получаване на потребителска информация след влизане в OIDC",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC удостоверяването не беше успешно",
"noTokenReceived": "Не е получен токен от вход",
"invalidAuthUrl": "Невалиден URL адрес за оторизация, получен от бекенд системата",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Показване на етикети под всеки хост в страничната лента. Деактивирайте, за да скриете всички етикети.",
"account": "Профил",
"appearance": "Външен вид",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "Език и локализация",
"fileManagerSettings": "Файлов мениджър",
"terminalSettings": "Терминал",
@@ -2370,6 +2472,10 @@
"description": "Описание на SSH идентификационните данни",
"searchCredentials": "Търсене на идентификационни данни по име, потребителско име или тагове...",
"sshConfig": "SSH конфигурация на крайната точка",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/начало",
"clientId": "вашият клиентски идентификатор",
"clientSecret": "вашата клиентска тайна",
@@ -2549,6 +2655,7 @@
"version": "Версия",
"upToDate": "Актуално",
"updateAvailable": "Налична е актуализация",
"beta": "Beta",
"uptime": "Време на работа",
"database": "База данни",
"healthy": "Здравословен",
@@ -2955,6 +3062,20 @@
"error": "Събирането на показатели не бе успешно: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Установяване на тунелна връзка...",
"sourceConnecting": "Свързване с изходния хост {{host}}...",
"sourceConnected": "Свързан с изходния хост",
"endpointResolving": "Разрешаване на хост на крайна точка {{host}}...",
"endpointResolved": "Хостът на крайната точка е разрешен",
"forwarding": "Настройване на пренасочване на портове {{localPort}} → {{remotePort}}...",
"connected": "Тунелът е построен успешно",
"retrying": "Връзката не бе успешна, опитът ще бъде повторен след {{countdown}}сек (опит {{attempt}}/{{max}})...",
"retryExhausted": "Достигнат е максималният брой опити за повторен опит",
"error": "Тунелна връзка неуспешна: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Свързване със SFTP сървър...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "তুমি {{version}}সংস্করণটি চালাচ্ছো",
"updateAvailable": "আপডেট উপলব্ধ",
"newVersionAvailable": "একটি নতুন সংস্করণ উপলব্ধ! আপনি {{current}}ব্যবহার করছেন, কিন্তু {{latest}} উপলব্ধ।",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "মুক্তিপ্রাপ্ত {{date}}",
"downloadUpdate": "আপডেট ডাউনলোড করুন",
"dismiss": "খারিজ করুন",
@@ -426,6 +428,8 @@
"warning": "সতর্কতা",
"info": "তথ্য",
"success": "সাফল্য",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "লোড হচ্ছে...",
"required": "প্রয়োজনীয়",
"optional": "ঐচ্ছিক",
@@ -471,6 +475,7 @@
"chinese": "চীনা",
"german": "জার্মান",
"cancel": "বাতিল করুন",
"done": "Done",
"username": "ব্যবহারকারীর নাম",
"name": "নাম",
"login": "লগইন",
@@ -486,6 +491,7 @@
"save": "সংরক্ষণ করুন",
"saving": "সংরক্ষণ করা হচ্ছে...",
"delete": "মুছে ফেলুন",
"rename": "Rename",
"edit": "সম্পাদনা",
"add": "যোগ করুন",
"search": "অনুসন্ধান করুন",
@@ -559,7 +565,8 @@
"passwordCopied": "পাসওয়ার্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"sudoPasswordCopied": "সুডো পাসওয়ার্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"noPasswordAvailable": "কোনও পাসওয়ার্ড নেই",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "অ্যাডমিন সেটিংস",
@@ -868,7 +875,42 @@
"passwordMinLength": "পাসওয়ার্ড কমপক্ষে ৬ অক্ষরের হতে হবে",
"currentRoles": "বর্তমান ভূমিকা",
"noRolesAssigned": "কোনও ভূমিকা বরাদ্দ করা হয়নি",
"assignNewRole": "নতুন ভূমিকা বরাদ্দ করুন"
"assignNewRole": "নতুন ভূমিকা বরাদ্দ করুন",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "হোস্ট ম্যানেজার",
@@ -962,28 +1004,8 @@
"enableDocker": "ডকার সক্ষম করুন",
"defaultPath": "ডিফল্ট পথ",
"defaultPathDesc": "এই হোস্টের জন্য ফাইল ম্যানেজার খোলার সময় ডিফল্ট ডিরেক্টরি",
"tunnelConnections": "টানেল সংযোগ",
"connection": "সংযোগ",
"remove": "অপসারণ",
"sourcePort": "সোর্স পোর্ট",
"sourcePortDesc": " (উৎসটি সাধারণ ট্যাবে বর্তমান সংযোগের বিবরণ উল্লেখ করে)",
"endpointPort": "এন্ডপয়েন্ট পোর্ট",
"endpointSshConfig": "এন্ডপয়েন্ট SSH কনফিগারেশন",
"tunnelForwardDescription": "এই টানেলটি সোর্স মেশিনের {{sourcePort}} পোর্ট থেকে ট্র্যাফিককে এন্ডপয়েন্ট মেশিনের {{endpointPort}} পোর্টে ফরোয়ার্ড করবে।",
"maxRetries": "সর্বোচ্চ পুনঃপ্রচেষ্টা",
"maxRetriesDescription": "টানেল সংযোগের জন্য সর্বোচ্চ সংখ্যক পুনঃপ্রচেষ্টা।",
"retryInterval": "পুনরায় চেষ্টা করার ব্যবধান (সেকেন্ড)",
"retryIntervalDescription": "পুনরায় চেষ্টা করার মাঝে অপেক্ষা করার সময়।",
"autoStartContainer": "কন্টেইনার লঞ্চে স্বয়ংক্রিয়ভাবে শুরু",
"autoStartDesc": "কন্টেইনার চালু হলে এই টানেলটি স্বয়ংক্রিয়ভাবে চালু করুন",
"addConnection": "টানেল সংযোগ যোগ করুন",
"tunnelType": "টানেলের ধরণ",
"tunnelTypeLocal": "স্থানীয় (-L)",
"tunnelTypeRemote": "রিমোট (-R)",
"tunnelTypeLocalDesc": "স্থানীয় পোর্টকে দূরবর্তী এন্ডপয়েন্টে ফরোয়ার্ড করুন",
"tunnelTypeRemoteDesc": "রিমোট পোর্ট স্থানীয় মেশিনে ফরোয়ার্ড করুন",
"tunnelForwardDescriptionLocal": "এই টানেলটি স্থানীয় পোর্ট {{sourcePort}} থেকে এন্ডপয়েন্ট মেশিনে পোর্ট {{endpointPort}} এ ট্র্যাফিক ফরোয়ার্ড করবে।",
"tunnelForwardDescriptionRemote": "এই টানেলটি সোর্স মেশিনের {{sourcePort}} পোর্ট থেকে ট্র্যাফিককে এন্ডপয়েন্ট মেশিনের {{endpointPort}} পোর্টে ফরোয়ার্ড করবে।",
"sshpassRequired": "পাসওয়ার্ড প্রমাণীকরণের জন্য Sshpass প্রয়োজন",
"sshpassRequiredDesc": "টানেলগুলিতে পাসওয়ার্ড প্রমাণীকরণের জন্য, সিস্টেমে sshpass ইনস্টল করা আবশ্যক।",
"otherInstallMethods": "অন্যান্য ইনস্টলেশন পদ্ধতি:",
@@ -1529,7 +1551,7 @@
"splitVertical": "উল্লম্বভাবে বিভক্ত করুন",
"closePanel": "প্যানেল বন্ধ করুন",
"reconnect": "পুনঃসংযোগ করুন",
"sessionEnded": "অধিবেশন শেষ হয়েছে",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "ত্রুটি: {{message}}",
"disconnected": "সংযোগ বিচ্ছিন্ন",
@@ -1933,22 +1955,10 @@
"ascending": "আরোহী",
"descending": "অবরোহী"
},
"tunnel": {
"log": {
"connecting": "টানেল সংযোগ স্থাপন করা হচ্ছে...",
"sourceConnecting": "সোর্স হোস্টের সাথে সংযোগ করা হচ্ছে {{host}}...",
"sourceConnected": "সোর্স হোস্টের সাথে সংযুক্ত",
"endpointResolving": "এন্ডপয়েন্ট হোস্ট সমাধান করা হচ্ছে {{host}}...",
"endpointResolved": "এন্ডপয়েন্ট হোস্ট সমাধান করা হয়েছে",
"forwarding": "পোর্ট ফরওয়ার্ডিং সেট আপ করা হচ্ছে {{localPort}} → {{remotePort}}...",
"connected": "টানেল সফলভাবে স্থাপিত হয়েছে",
"retrying": "সংযোগ ব্যর্থ হয়েছে, {{countdown}}s (চেষ্টা {{attempt}}/{{max}} ) এ আবার চেষ্টা করা হচ্ছে...",
"retryExhausted": "সর্বোচ্চ পুনঃচেষ্টার প্রচেষ্টায় পৌঁছেছেন",
"error": "টানেল সংযোগ ব্যর্থ হয়েছে: {{error}}"
}
},
"tunnels": {
"title": "SSH টানেল",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "কোনও SSH টানেল নেই",
"createFirstTunnelMessage": "আপনি এখনও কোনও SSH টানেল তৈরি করেননি। শুরু করতে হোস্ট ম্যানেজারে টানেল সংযোগগুলি কনফিগার করুন।",
"connected": "সংযুক্ত",
@@ -1969,27 +1979,100 @@
"disconnect": "সংযোগ বিচ্ছিন্ন করুন",
"cancel": "বাতিল করুন",
"port": "বন্দর",
"localPort": "স্থানীয় বন্দর",
"remotePort": "রিমোট পোর্ট",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "{{current}} এর {{max}}প্রচেষ্টা",
"nextRetryIn": "পরবর্তী চেষ্টা {{seconds}} সেকেন্ডের মধ্যে",
"checkDockerLogs": "ত্রুটির কারণের জন্য আপনার ডকার লগগুলি পরীক্ষা করুন, যোগদান করুন",
"orCreate": "অথবা একটি তৈরি করুন ",
"noTunnelConnections": "কোনও টানেল সংযোগ কনফিগার করা হয়নি",
"tunnelConnections": "টানেল সংযোগ",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "টানেল যোগ করুন",
"editTunnel": "টানেল সম্পাদনা করুন",
"deleteTunnel": "টানেল মুছুন",
"tunnelName": "টানেলের নাম",
"localPort": "স্থানীয় বন্দর",
"remoteHost": "রিমোট হোস্ট",
"remotePort": "রিমোট পোর্ট",
"autoStart": "স্বয়ংক্রিয় শুরু",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "অবস্থা",
"active": "সক্রিয়",
"inactive": "নিষ্ক্রিয়",
"start": "শুরু করুন",
"stop": "থামো",
"test": "Test",
"restart": "পুনরারম্ভ করুন",
"connectionType": "সংযোগের ধরণ",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "স্থানীয়",
"remote": "দূরবর্তী",
"dynamic": "গতিশীল",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "এই সার্ভারের সাথে সংযোগ স্থাপনের জন্য আপনার SSH শংসাপত্রগুলি প্রদান করুন।",
"sshPasswordDescription": "এই SSH সংযোগের জন্য পাসওয়ার্ডটি লিখুন।",
"sshKeyPasswordDescription": "যদি আপনার SSH কী এনক্রিপ্ট করা থাকে, তাহলে এখানে পাসফ্রেজটি লিখুন।",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "ধাপ ১: আপনার প্রমাণীকরণকারী অ্যাপ দিয়ে QR কোড স্ক্যান করুন",
"manualEntryCode": "ম্যানুয়াল এন্ট্রি কোড",
"cannotScanQRText": "যদি আপনি QR কোডটি স্ক্যান করতে না পারেন, তাহলে আপনার প্রমাণীকরণকারী অ্যাপে এই কোডটি ম্যানুয়ালি লিখুন।",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "পাসওয়ার্ড রিসেট সম্পূর্ণ করতে ব্যর্থ হয়েছে",
"invalidTotpCode": "অবৈধ TOTP কোড",
"failedOidcLogin": "OIDC লগইন শুরু করতে ব্যর্থ হয়েছে",
"failedUserInfo": "OIDC লগইন করার পরে ব্যবহারকারীর তথ্য পেতে ব্যর্থ হয়েছে।",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC প্রমাণীকরণ ব্যর্থ হয়েছে",
"noTokenReceived": "লগইন থেকে কোনও টোকেন পাওয়া যায়নি",
"invalidAuthUrl": "ব্যাকএন্ড থেকে অবৈধ অনুমোদনের URL পাওয়া গেছে",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "সাইডবারে প্রতিটি হোস্টের নিচে ট্যাগ প্রদর্শন করুন। সমস্ত ট্যাগ লুকানোর জন্য অক্ষম করুন।",
"account": "হিসাব",
"appearance": "চেহারা",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "ভাষা ও স্থানীয়করণ",
"fileManagerSettings": "ফাইল ম্যানেজার",
"terminalSettings": "টার্মিনাল",
@@ -2370,6 +2472,10 @@
"description": "SSH শংসাপত্রের বর্ণনা",
"searchCredentials": "নাম, ব্যবহারকারীর নাম, অথবা ট্যাগ দ্বারা শংসাপত্র অনুসন্ধান করুন...",
"sshConfig": "এন্ডপয়েন্ট ssh কনফিগারেশন",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/বাড়ি",
"clientId": "আপনার-ক্লায়েন্ট-আইডি",
"clientSecret": "আপনার ক্লায়েন্টের গোপনীয়তা",
@@ -2549,6 +2655,7 @@
"version": "সংস্করণ",
"upToDate": "আপ টু ডেট",
"updateAvailable": "আপডেট উপলব্ধ",
"beta": "Beta",
"uptime": "আপটাইম",
"database": "ডাটাবেস",
"healthy": "সুস্থ",
@@ -2955,6 +3062,20 @@
"error": "মেট্রিক্স সংগ্রহ ব্যর্থ হয়েছে: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "টানেল সংযোগ স্থাপন করা হচ্ছে...",
"sourceConnecting": "সোর্স হোস্টের সাথে সংযোগ করা হচ্ছে {{host}}...",
"sourceConnected": "সোর্স হোস্টের সাথে সংযুক্ত",
"endpointResolving": "এন্ডপয়েন্ট হোস্ট সমাধান করা হচ্ছে {{host}}...",
"endpointResolved": "এন্ডপয়েন্ট হোস্ট সমাধান করা হয়েছে",
"forwarding": "পোর্ট ফরওয়ার্ডিং সেট আপ করা হচ্ছে {{localPort}} → {{remotePort}}...",
"connected": "টানেল সফলভাবে স্থাপিত হয়েছে",
"retrying": "সংযোগ ব্যর্থ হয়েছে, {{countdown}}s (চেষ্টা {{attempt}}/{{max}} ) এ আবার চেষ্টা করা হচ্ছে...",
"retryExhausted": "সর্বোচ্চ পুনঃচেষ্টার প্রচেষ্টায় পৌঁছেছেন",
"error": "টানেল সংযোগ ব্যর্থ হয়েছে: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "SFTP সার্ভারের সাথে সংযোগ করা হচ্ছে...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Esteu executant la versió {{version}}",
"updateAvailable": "Actualització disponible",
"newVersionAvailable": "Hi ha una nova versió disponible! Esteu executant {{current}}, però {{latest}} està disponible.",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "Publicat el {{date}}",
"downloadUpdate": "Descarrega l'actualització",
"dismiss": "Ignora",
@@ -426,6 +428,8 @@
"warning": "Avís",
"info": "Informació",
"success": "Èxit",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "S'està carregant...",
"required": "Obligatori",
"optional": "Opcional",
@@ -471,6 +475,7 @@
"chinese": "xinès",
"german": "Alemany",
"cancel": "Cancel·la",
"done": "Done",
"username": "Nom d'usuari",
"name": "Nom",
"login": "Inicia la sessió",
@@ -486,6 +491,7 @@
"save": "Desa",
"saving": "Desant...",
"delete": "Suprimeix",
"rename": "Rename",
"edit": "Edita",
"add": "Afegeix",
"search": "Cerca",
@@ -559,7 +565,8 @@
"passwordCopied": "La contrasenya s'ha copiat al porta-retalls",
"sudoPasswordCopied": "Contrasenya de Sudo copiada al porta-retalls",
"noPasswordAvailable": "No hi ha cap contrasenya disponible",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "Configuració de l'administrador",
@@ -868,7 +875,42 @@
"passwordMinLength": "La contrasenya ha de tenir com a mínim 6 caràcters",
"currentRoles": "Rols actuals",
"noRolesAssigned": "Sense rols assignats",
"assignNewRole": "Assigna un nou rol"
"assignNewRole": "Assigna un nou rol",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "Gestor d'amfitrions",
@@ -962,28 +1004,8 @@
"enableDocker": "Activa Docker",
"defaultPath": "Camí predeterminat",
"defaultPathDesc": "Directori per defecte en obrir el gestor de fitxers per a aquest amfitrió",
"tunnelConnections": "Connexions de túnels",
"connection": "Connexió",
"remove": "Elimina",
"sourcePort": "Port d'origen",
"sourcePortDesc": " (La font fa referència als detalls de la connexió actual a la pestanya General)",
"endpointPort": "Port de punt final",
"endpointSshConfig": "Configuració SSH del punt final",
"tunnelForwardDescription": "Aquest túnel redirigirà el trànsit des del port {{sourcePort}} de la màquina d'origen (detalls de la connexió actual a la pestanya general) fins al port {{endpointPort}} de la màquina final.",
"maxRetries": "Màxim d'intents",
"maxRetriesDescription": "Nombre màxim d'intents de reintent per a la connexió del túnel.",
"retryInterval": "Interval de reintent (segons)",
"retryIntervalDescription": "Temps d'espera entre intents de reintent.",
"autoStartContainer": "Inici automàtic en llançar contenidors",
"autoStartDesc": "Inicia automàticament aquest túnel quan s'iniciï el contenidor",
"addConnection": "Afegeix una connexió de túnel",
"tunnelType": "Tipus de túnel",
"tunnelTypeLocal": "Local (-L)",
"tunnelTypeRemote": "Comandament a distància (-R)",
"tunnelTypeLocalDesc": "Reenvia el port local a un punt final remot",
"tunnelTypeRemoteDesc": "Reenvia el port remot a la màquina local",
"tunnelForwardDescriptionLocal": "Aquest túnel redirigirà el trànsit del port local {{sourcePort}} al port {{endpointPort}} de la màquina terminal.",
"tunnelForwardDescriptionRemote": "Aquest túnel redirigirà el trànsit del port {{sourcePort}} de la màquina d'origen (detalls de la connexió actual a la pestanya general) al port {{endpointPort}} de la màquina final.",
"sshpassRequired": "Sshpass necessari per a l'autenticació amb contrasenya",
"sshpassRequiredDesc": "Per a l'autenticació amb contrasenya en túnels, cal instal·lar sshpass al sistema.",
"otherInstallMethods": "Altres mètodes d'instal·lació:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Dividir verticalment",
"closePanel": "Tanca el panell",
"reconnect": "Reconnecta",
"sessionEnded": "Sessió finalitzada",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "ERROR: {{message}}",
"disconnected": "Desconnectat",
@@ -1933,22 +1955,10 @@
"ascending": "Ascendent",
"descending": "Descendent"
},
"tunnel": {
"log": {
"connecting": "Establint la connexió del túnel...",
"sourceConnecting": "Connectant a l'amfitrió d'origen {{host}}...",
"sourceConnected": "Connectat a l'amfitrió d'origen",
"endpointResolving": "Resolució de l'amfitrió del punt final {{host}}...",
"endpointResolved": "Amfitrió del punt final resolt",
"forwarding": "Configuració del reenviament de ports {{localPort}} → {{remotePort}}...",
"connected": "Túnel establert amb èxit",
"retrying": "La connexió ha fallat, es tornarà a intentar en {{countdown}}s (intent {{attempt}}/{{max}})...",
"retryExhausted": "S'ha arribat al màxim d'intents de reintent",
"error": "La connexió del túnel ha fallat: {{error}}"
}
},
"tunnels": {
"title": "Túnels SSH",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "Sense túnels SSH",
"createFirstTunnelMessage": "Encara no has creat cap túnel SSH. Configureu les connexions del túnel al Gestor d'amfitrions per començar.",
"connected": "Connectat",
@@ -1969,27 +1979,100 @@
"disconnect": "Desconnecta",
"cancel": "Cancel·la",
"port": "Port",
"localPort": "Port local",
"remotePort": "Port remot",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "Intent {{current}} de {{max}}",
"nextRetryIn": "Proper intent en {{seconds}} segons",
"checkDockerLogs": "Reviseu els registres de Docker per saber el motiu de l'error i uniu-vos al",
"orCreate": "o crear un ",
"noTunnelConnections": "No hi ha connexions de túnel configurades",
"tunnelConnections": "Connexions de túnels",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "Afegeix un túnel",
"editTunnel": "Edita el túnel",
"deleteTunnel": "Suprimeix el túnel",
"tunnelName": "Nom del túnel",
"localPort": "Port local",
"remoteHost": "Amfitrió remot",
"remotePort": "Port remot",
"autoStart": "Inici automàtic",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "Estat",
"active": "Actiu",
"inactive": "Inactiu",
"start": "Inici",
"stop": "Atura",
"test": "Test",
"restart": "Reinicia",
"connectionType": "Tipus de connexió",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "Local",
"remote": "Remot",
"dynamic": "Dinàmic",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Si us plau, proporcioneu les vostres credencials SSH per connectar-vos a aquest servidor.",
"sshPasswordDescription": "Introduïu la contrasenya per a aquesta connexió SSH.",
"sshKeyPasswordDescription": "Si la clau SSH està xifrada, introduïu la contrasenya aquí.",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "Pas 1: escaneja el codi QR amb l'aplicació d'autenticació",
"manualEntryCode": "Codi d'entrada manual",
"cannotScanQRText": "Si no podeu escanejar el codi QR, introduïu-lo manualment a l'aplicació d'autenticació.",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "No s'ha pogut completar el restabliment de la contrasenya",
"invalidTotpCode": "Codi TOTP no vàlid",
"failedOidcLogin": "No s'ha pogut iniciar l'inici de sessió a l'OIDC.",
"failedUserInfo": "No s'ha pogut obtenir la informació de l'usuari després de l'inici de sessió a l'OIDC.",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "L'autenticació OIDC ha fallat",
"noTokenReceived": "No s'ha rebut cap testimoni de l'inici de sessió",
"invalidAuthUrl": "URL d'autorització no vàlida rebuda del backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Mostra les etiquetes sota cada amfitrió a la barra lateral. Desactiva-les per ocultar totes les etiquetes.",
"account": "Compte",
"appearance": "Aspecte",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "Idioma i localització",
"fileManagerSettings": "Gestor de fitxers",
"terminalSettings": "Terminal",
@@ -2370,6 +2472,10 @@
"description": "Descripció de les credencials SSH",
"searchCredentials": "Cerca credencials per nom, nom d'usuari o etiquetes...",
"sshConfig": "configuració ssh del punt final",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/llar",
"clientId": "el-teu-ID-de-client",
"clientSecret": "el-secret-del-teu-client",
@@ -2549,6 +2655,7 @@
"version": "Versió",
"upToDate": "Actualitzat",
"updateAvailable": "Actualització disponible",
"beta": "Beta",
"uptime": "Temps de funcionament",
"database": "Base de dades",
"healthy": "Saludable",
@@ -2955,6 +3062,20 @@
"error": "La recopilació de mètriques ha fallat: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Establint la connexió del túnel...",
"sourceConnecting": "Connectant a l'amfitrió d'origen {{host}}...",
"sourceConnected": "Connectat a l'amfitrió d'origen",
"endpointResolving": "Resolució de l'amfitrió del punt final {{host}}...",
"endpointResolved": "Amfitrió del punt final resolt",
"forwarding": "Configuració del reenviament de ports {{localPort}} → {{remotePort}}...",
"connected": "Túnel establert amb èxit",
"retrying": "La connexió ha fallat, es tornarà a intentar en {{countdown}}s (intent {{attempt}}/{{max}})...",
"retryExhausted": "S'ha arribat al màxim d'intents de reintent",
"error": "La connexió del túnel ha fallat: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Connectant al servidor SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Používáte verzi {{version}}",
"updateAvailable": "Dostupná aktualizace",
"newVersionAvailable": "Je dostupná nová verze! Používáte {{current}}, ale {{latest}} je k dispozici.",
"betaVersion": "Beta verze",
"betaVersionDesc": "Používáte {{current}}, což je novější než poslední stabilní vydání {{latest}}.",
"releasedOn": "Vydáno v {{date}}",
"downloadUpdate": "Stáhnout aktualizaci",
"dismiss": "Odmítnout",
@@ -426,6 +428,8 @@
"warning": "Varování",
"info": "Info",
"success": "Úspěšně",
"unsavedChanges": "Neuložené změny",
"dismiss": "Odmítnout",
"loading": "Načítám...",
"required": "Požadováno",
"optional": "Nepovinné",
@@ -471,6 +475,7 @@
"chinese": "Čínština",
"german": "Němčina",
"cancel": "Zrušit",
"done": "Hotovo",
"username": "Uživatelské jméno",
"name": "Název",
"login": "Přihlásit se",
@@ -486,6 +491,7 @@
"save": "Uložit",
"saving": "Ukládání...",
"delete": "Vymazat",
"rename": "Přejmenovat",
"edit": "Upravit",
"add": "Přidat",
"search": "Hledat",
@@ -559,7 +565,8 @@
"passwordCopied": "Heslo zkopírováno do schránky",
"sudoPasswordCopied": "Sudo heslo zkopírováno do schránky",
"noPasswordAvailable": "Žádné heslo není k dispozici",
"failedToCopyPassword": "Kopírování hesla se nezdařilo"
"failedToCopyPassword": "Kopírování hesla se nezdařilo",
"openFileManager": "Otevřít správce souborů"
},
"admin": {
"title": "Nastavení správce",
@@ -868,7 +875,42 @@
"passwordMinLength": "Heslo musí mít alespoň 6 znaků",
"currentRoles": "Aktuální role",
"noRolesAssigned": "Nebyly přiřazeny žádné role",
"assignNewRole": "Přiřadit novou roli"
"assignNewRole": "Přiřadit novou roli",
"apiKeys": {
"tabLabel": "API klíče",
"title": "API klíče",
"createApiKey": "Vytvořit klíč API",
"createApiKeyDescription": "Vytvoření nového API klíče pro konkrétního uživatele. Token se zobrazí pouze jednou.",
"keyCreated": "API klíč vytvořen",
"keyCreatedDescription": "Zkopírujte tento klíč nyní - nebude znovu zobrazen.",
"keyName": "Název klíče",
"keyNamePlaceholder": "např. CI/CD potrubí",
"scopedUser": "Šifrovaný uživatel",
"selectUser": "Vyberte uživatele...",
"searchUsers": "Hledat uživatele...",
"noUsersFound": "Nenalezeni žádní uživatelé.",
"expiresAt": "Vyprší v",
"optional": "volitelné",
"expiresAtHelp": "Ponechte prázdné pro klíč, který nikdy nevyprší.",
"copyWarningTitle": "Uložit váš API klíč",
"copyWarningDescription": "Tento klíč se zobrazí pouze jednou. Uložte jej na bezpečném místě.",
"apiKey": "API klíč",
"tokenCopied": "Token zkopírován do schránky",
"creating": "Vytváření...",
"nameRequired": "Je vyžadován název klíče",
"userRequired": "Vyberte uživatele",
"failedToCreate": "Nepodařilo se vytvořit API klíč",
"noKeys": "Nebyly nalezeny žádné klíče API.",
"name": "Název",
"prefix": "Předpona",
"lastUsed": "Naposledy použito",
"never": "Nikdy",
"revokeKey": "Zrušit klíč",
"confirmRevoke": "Opravdu chcete zrušit klíč API \"{{name}}\"? Tuto akci nelze vrátit zpět.",
"revokedSuccessfully": "API klíč byl úspěšně odebrán",
"failedToRevoke": "Nepodařilo se zrušit klíč API",
"failedToFetch": "Nepodařilo se načíst API klíče"
}
},
"hosts": {
"title": "Správce hostitelů",
@@ -962,28 +1004,8 @@
"enableDocker": "Povolit Docker",
"defaultPath": "Výchozí cesta",
"defaultPathDesc": "Výchozí adresář při otevírání správce souborů pro tohoto hostitele",
"tunnelConnections": "Připojení tunelu",
"connection": "Připojení",
"remove": "Odebrat",
"sourcePort": "Zdrojový port",
"sourcePortDesc": " (Odběr se týká podrobností o aktuálním připojení v obecné kartě)",
"endpointPort": "Port koncového bodu",
"endpointSshConfig": "Konfigurace koncového bodu SSH",
"tunnelForwardDescription": "Tento tunel přeposílá provoz z portu {{sourcePort}} na zdrojovém počítači (aktuální údaje o připojení obecně) do portu {{endpointPort}} na koncovém počítači.",
"maxRetries": "Maximální počet opakování",
"maxRetriesDescription": "Maximální počet pokusů o připojení k tunelu.",
"retryInterval": "Interval opakovat (sekundy)",
"retryIntervalDescription": "Čas čekat mezi pokusy opakovat.",
"autoStartContainer": "Automatické spuštění při spuštění kontejneru",
"autoStartDesc": "Automaticky spustit tunel při spuštění kontejneru",
"addConnection": "Přidat připojení k tunelu",
"tunnelType": "Typ tunelu",
"tunnelTypeLocal": "Místní (-L)",
"tunnelTypeRemote": "Vzdálené (-R)",
"tunnelTypeLocalDesc": "Přeposlat místní port na vzdálený koncový bod",
"tunnelTypeRemoteDesc": "Přeposlat vzdálený port do místního stroje",
"tunnelForwardDescriptionLocal": "Tento tunel přeposílá provoz z lokálního portu {{sourcePort}} do portu {{endpointPort}} na koncovém počítači.",
"tunnelForwardDescriptionRemote": "Tento tunel přeposílá provoz z portu {{sourcePort}} na zdrojovém počítači (aktuální údaje o připojení obecně) do portu {{endpointPort}} na koncovém počítači.",
"sshpassRequired": "Sshpass vyžadován pro ověření hesla",
"sshpassRequiredDesc": "Pro ověřování hesla v tunelech musí být v systému nainstalován sshpass.",
"otherInstallMethods": "Jiné způsoby instalace:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Rozdělit vertikálně",
"closePanel": "Zavřít panel",
"reconnect": "Znovu připojit",
"sessionEnded": "Relace skončila",
"sessionEnded": "Relace byla ukončena.",
"connectionLost": "Spojení ztraceno",
"error": "CHYBA: {{message}}",
"disconnected": "Odpojeno",
@@ -1933,22 +1955,10 @@
"ascending": "Vzestupně",
"descending": "sestupně"
},
"tunnel": {
"log": {
"connecting": "Navazování připojení tunelu...",
"sourceConnecting": "Připojování ke zdrojovému hostiteli {{host}}...",
"sourceConnected": "Připojeno ke zdrojovému hostiteli",
"endpointResolving": "Vyhledávání koncového bodu hostitele {{host}}...",
"endpointResolved": "Hostitel koncového bodu vyřešen",
"forwarding": "Nastavení portu přesměrování {{localPort}} → {{remotePort}}...",
"connected": "Tunel byl úspěšně vytvořen",
"retrying": "Připojení se nezdařilo, opakuji v {{countdown}}s (pokus {{attempt}}/{{max}})...",
"retryExhausted": "Dosažen maximální počet pokusů o opakování",
"error": "Připojení k tunelu selhalo: {{error}}"
}
},
"tunnels": {
"title": "SSH tunely",
"noTunnelsConfigured": "Nejsou nastaveny žádné tunely",
"configureTunnelsInHostSettings": "Konfigurace připojení tunelu ve Správci hostitelů pro spuštění",
"noSshTunnels": "Žádné SSH tunely",
"createFirstTunnelMessage": "Zatím jste nevytvořili žádné SSH tunely. Nakonfigurujte připojení tunelu v hostitelském manažeru, abyste mohli začít.",
"connected": "Připojeno",
@@ -1969,27 +1979,100 @@
"disconnect": "Odpojit",
"cancel": "Zrušit",
"port": "Port",
"localPort": "Místní port",
"remotePort": "Vzdálený port",
"currentHostPort": "Aktuální port hostitele",
"endpointPort": "Přístav koncového bodu",
"bindIp": "Místní IP",
"currentHostIp": "Aktuální IP hostitele",
"endpointSshConfig": "Konfigurace koncového bodu SSH",
"endpointSshConfigRequired": "Konfigurace koncového bodu SSH je povinná",
"endpointSshHost": "Koncový bod SSH hostitel",
"endpointSshHostPlaceholder": "Vyberte nakonfigurovaného hostitele",
"endpointSshHostRequired": "Vyberte koncový SSH hostitele pro každý klientský tunel.",
"attempt": "Pokusit se {{current}} o {{max}}",
"nextRetryIn": "Další opakování za {{seconds}} sekund",
"checkDockerLogs": "Zkontroluj své dokovací protokoly z důvodu chyby, připojte se k",
"orCreate": "nebo vytvořte ",
"noTunnelConnections": "Žádné připojení k tunelu není nastaveno",
"tunnelConnections": "Připojení tunelu",
"serverTunnels": "Tuny serveru",
"serverTunnelsDesc": "Tunely spravované backend-managementem uložené s tímto hostitelem.",
"clientTunnels": "Tunely klienta",
"clientTunnelsUnavailable": "Klientské tunely vyžadují desktopového klienta.",
"serverTunnel": "Serverový tunel",
"clientTunnel": "Zákaznický tunel",
"addServerTunnel": "Přidat serverový tunel",
"addClientTunnel": "Přidat klientský tunel",
"noServerTunnels": "Žádné tunely serveru nejsou nastaveny.",
"noClientTunnels": "Na této ploše nejsou nakonfigurovány žádné tunely klienta.",
"manageClientTunnels": "Správa tunelů klienta",
"addTunnel": "Přidat tunel",
"editTunnel": "Upravit tunel",
"deleteTunnel": "Odstranit tunel",
"tunnelName": "Název tunelu",
"localPort": "Místní port",
"remoteHost": "Vzdálený server",
"remotePort": "Vzdálený port",
"autoStart": "Automatický start",
"autoStartContainer": "Automatické spuštění při spuštění",
"autoStartContainerDesc": "Automaticky spustit tento tunel při spuštění serveru Termix.",
"autoStartEnableFailed": "Hostitel uložen, ale nepodařilo se spustit automatické nastartování tunelů pro {{name}}.",
"clientAutoStartDesc": "Začne, když se tento desktopový klient otevře a zůstane připojen.",
"clientManualStartDesc": "Používejte Start a Zastavit z tohoto řádku. Termix jej automaticky neotevře.",
"clientRemoteServerNote": "Vzdálené přesměrování může vyžadovat AllowTcpForwarding a GatewayPorts na serveru SSH serveru. Vzdálený port se uzavře, když se tato plocha odpojí.",
"clientTunnelStarted": "Klientský tunel spuštěn",
"clientTunnelStopped": "Klientský tunel zastaven",
"tunnelTestSucceeded": "Test tunelu byl úspěšný",
"tunnelTestFailed": "Test tunelu selhal",
"localSaved": "Klientské tunely uloženy",
"localSaveError": "Nepodařilo se uložit místní klientské tunely",
"invalidBindIp": "Místní IP adresa musí být platná IPv4 adresa.",
"invalidLocalTargetIp": "Místní cílová IP musí být platná IPv4 adresa.",
"invalidCurrentHostIp": "Současná IP adresa hostitele musí být platnou IPv4 adresou.",
"invalidLocalPort": "Místní port musí být mezi 1 a 65535.",
"invalidRemotePort": "Vzdálený port musí být mezi 1 a 65535.",
"invalidLocalTargetPort": "Místní cílový port musí být mezi 1 a 65535.",
"invalidEndpointPort": "Přístav koncových bodů musí být mezi 1 a 65535.",
"duplicateAutoStartBind": "Pouze jeden klientský tunel s automatickým startem může používat {{bind}}.",
"manualControlError": "Aktualizace stavu tunelu se nezdařila.",
"saveHostBeforeManualControl": "Uložit tohoto hostitele před spuštěním nebo zastavením jeho serverových tunelů.",
"status": "Stav",
"active": "Aktivní",
"inactive": "Neaktivní",
"start": "Začít",
"stop": "Zastavit",
"test": "Zkouška",
"restart": "Restartovat",
"connectionType": "Typ připojení",
"type": "Typ tunelu",
"typeLocal": "Místní (-L)",
"typeRemote": "Vzdálené (-R)",
"typeDynamic": "Dynamické (-D)",
"typeServerLocalDesc": "Aktuální hostitel ke koncovce.",
"typeServerRemoteDesc": "Koncový bod zpět do aktuálního hostitele.",
"typeClientLocalDesc": "Lokální počítač ke koncovce.",
"typeClientRemoteDesc": "Koncový bod zpět do lokálního počítače.",
"typeClientDynamicDesc": "SOCKS na místním počítači.",
"typeDynamicDesc": "Přesměrování SOCKS5 KONNECT provozu přes SSH",
"forwardDescriptionServerLocal": "Aktuální host {{sourcePort}} → koncový bod {{endpointPort}}.",
"forwardDescriptionServerRemote": "Koncový bod {{endpointPort}} → aktuální host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS na aktuálním hostiteli {{sourcePort}}.",
"forwardDescriptionClientLocal": "Místní {{sourcePort}} → vzdálené {{endpointPort}}.",
"forwardDescriptionClientRemote": "Vzdálená {{sourcePort}} → lokální {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS na místním portu {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS prostřednictvím {{endpoint}}",
"autoNameClientLocal": "Místní {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → místní {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} prostřednictvím {{endpoint}}",
"route": "Trasa:",
"lastStarted": "Naposledy spuštěno",
"lastTested": "Poslední test",
"lastError": "Poslední chyba",
"maxRetries": "Maximální počet opakování",
"maxRetriesDescription": "Maximální počet pokusů o opakování.",
"retryInterval": "Interval opakovat (sekundy)",
"retryIntervalDescription": "Čas čekat mezi pokusy opakovat.",
"local": "Místní",
"remote": "Vzdálené",
"dynamic": "Dynamický",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Zadejte prosím vaše SSH přihlašovací údaje pro připojení k tomuto serveru.",
"sshPasswordDescription": "Zadejte heslo pro toto SSH připojení.",
"sshKeyPasswordDescription": "Pokud je váš SSH klíč šifrován, zadejte heslo zde.",
"passphraseRequired": "Vyžadována přístupová fráze",
"passphraseRequiredDescription": "SSH klíč je šifrován. Zadejte prosím heslo pro odemknutí.",
"step1ScanQR": "Krok 1: Naskenujte QR kód pomocí vaší ověřovací aplikace",
"manualEntryCode": "Ruční vstupní kód",
"cannotScanQRText": "Pokud nemůžete naskenovat QR kód, zadejte tento kód ručně do vaší ověřovací aplikace",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Obnovení hesla se nezdařilo",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Nepodařilo se spustit OIDC přihlášení",
"failedUserInfo": "Nepodařilo se získat informace o uživateli po přihlášení OIDC",
"failedUserInfo": "Nepodařilo se získat informace o uživateli po přihlášení",
"oidcAuthFailed": "OIDC ověření se nezdařilo",
"noTokenReceived": "Od přihlášení nebyl obdržen žádný token",
"invalidAuthUrl": "Z backendu obdržena neplatná autorizační URL",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Zobrazit štítky pod každým hostitelem v postranním panelu. Zakažte pro skrytí všech štítků.",
"account": "Účet",
"appearance": "Vzhled",
"c2sTunnelConfigDesc": "Místní stolní tunely zaměřující nastavené SSH hostitele.",
"c2sTunnelPresets": "Předvolby klientského tunelu",
"c2sTunnelPresetsDesc": "Uložte místní seznam tunelů tohoto desktopového klienta jako pojmenovanou předvolbu serveru, nebo nahrajte předvolbu zpět do tohoto klienta.",
"c2sTunnelPresetsUnavailable": "Předvolby klientského tunelu jsou k dispozici pouze v desktopovém klientovi.",
"c2sPresetName": "Název předvolby",
"c2sPresetNamePlaceholder": "Název předvolby klienta",
"c2sPresetToLoad": "Předvolba k načtení",
"c2sNoPresetSelected": "Nebyla vybrána žádná předvolba",
"c2sNoPresets": "Žádná přednastavení nebyla uložena",
"c2sLoadPreset": "Načíst",
"c2sCurrentLocalConfig": "{{count}} lokální klientské tunely nakonfigurovány na tomto počítači.",
"c2sPresetSyncNote": "Předvolby jsou explicitní snímky; načítání jednoho nahradí lokální klientský seznam klientských tunelů.",
"c2sPresetSaved": "Předvolba klientského tunelu byla uložena",
"c2sPresetLoaded": "Předvolba klientského tunelu byla načtena lokálně",
"c2sPresetRenamed": "Předvolba klientského tunelu přejmenována",
"c2sPresetDeleted": "Předvolba klientského tunelu byla smazána",
"c2sPresetLoadError": "Nepodařilo se načíst přednastavení klientského tunelu",
"languageLocalization": "Jazyk & Lokalizace",
"fileManagerSettings": "Správce souborů",
"terminalSettings": "Terminál",
@@ -2370,6 +2472,10 @@
"description": "SSH - popis pověření",
"searchCredentials": "Hledat přihlašovací údaje podle jména, uživatelského jména nebo štítků...",
"sshConfig": "konfigurace koncového bodu ssh",
"bindLocalhost": "127.0.0.1 (vázat na localhost)",
"localListenerHost": "127.0.0.1 (poslouchejte lokálně)",
"localTargetHost": "127.0.0.1 (cíl na tomto počítači)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/domů",
"clientId": "vaše-klient-id",
"clientSecret": "Vašich klient-tajných služeb",
@@ -2549,6 +2655,7 @@
"version": "Verze",
"upToDate": "Aktuální",
"updateAvailable": "Dostupná aktualizace",
"beta": "Beta",
"uptime": "Doba provozu",
"database": "Databáze",
"healthy": "Zdravé",
@@ -2955,6 +3062,20 @@
"error": "Sbírka metriky selhala: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Navazování připojení tunelu...",
"sourceConnecting": "Připojování ke zdrojovému hostiteli {{host}}...",
"sourceConnected": "Připojeno ke zdrojovému hostiteli",
"endpointResolving": "Vyhledávání koncového bodu hostitele {{host}}...",
"endpointResolved": "Hostitel koncového bodu vyřešen",
"forwarding": "Nastavení portu přesměrování {{localPort}} → {{remotePort}}...",
"connected": "Tunel byl úspěšně vytvořen",
"retrying": "Připojení se nezdařilo, opakuji v {{countdown}}s (pokus {{attempt}}/{{max}})...",
"retryExhausted": "Dosažen maximální počet pokusů o opakování",
"error": "Připojení k tunelu selhalo: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Připojování k SFTP serveru...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Du kører version {{version}}",
"updateAvailable": "Opdatering Tilgængelig",
"newVersionAvailable": "En ny version er tilgængelig! Du kører {{current}}, men {{latest}} er tilgængelig.",
"betaVersion": "Beta Version",
"betaVersionDesc": "Du kører {{current}}, som er nyere end den seneste stabile udgave {{latest}}.",
"releasedOn": "Udgivet til {{date}}",
"downloadUpdate": "Download Opdatering",
"dismiss": "Afvis",
@@ -426,6 +428,8 @@
"warning": "Advarsel",
"info": "Info",
"success": "Succes",
"unsavedChanges": "Ikke-gemte ændringer",
"dismiss": "Afvis",
"loading": "Indlæser...",
"required": "Påkrævet",
"optional": "Valgfri",
@@ -471,6 +475,7 @@
"chinese": "Kinesisk",
"german": "Tysk",
"cancel": "Annuller",
"done": "Udført",
"username": "Brugernavn",
"name": "Navn",
"login": "Login",
@@ -486,6 +491,7 @@
"save": "Gem",
"saving": "Gemmer...",
"delete": "Slet",
"rename": "Omdøb",
"edit": "Rediger",
"add": "Tilføj",
"search": "Søg",
@@ -559,7 +565,8 @@
"passwordCopied": "Adgangskode kopieret til udklipsholder",
"sudoPasswordCopied": "Sudo adgangskode kopieret til udklipsholder",
"noPasswordAvailable": "Ingen adgangskode tilgængelig",
"failedToCopyPassword": "Kunne ikke kopiere adgangskode"
"failedToCopyPassword": "Kunne ikke kopiere adgangskode",
"openFileManager": "Åbn Filhåndtering"
},
"admin": {
"title": "Admin Indstillinger",
@@ -868,7 +875,42 @@
"passwordMinLength": "Adgangskoden skal være på mindst 6 tegn",
"currentRoles": "Aktuelle Roller",
"noRolesAssigned": "Ingen roller tildelt",
"assignNewRole": "Tildel Ny Rolle"
"assignNewRole": "Tildel Ny Rolle",
"apiKeys": {
"tabLabel": "Api Nøgler",
"title": "Api Nøgler",
"createApiKey": "Opret API-nøgle",
"createApiKeyDescription": "Opret en ny API-nøgle afgrænset til en bestemt bruger. Token vises kun én gang.",
"keyCreated": "Api Nøgle Oprettet",
"keyCreatedDescription": "Kopier denne nøgle nu - den vil ikke blive vist igen.",
"keyName": "Nøgle Navn",
"keyNamePlaceholder": "f.eks. CI/CD Rørledning",
"scopedUser": "Scoped Bruger",
"selectUser": "Vælg en bruger...",
"searchUsers": "Søg efter brugere...",
"noUsersFound": "Ingen brugere fundet.",
"expiresAt": "Udløber Ved",
"optional": "valgfri",
"expiresAtHelp": "Lad være tom for en nøgle, der aldrig udløber.",
"copyWarningTitle": "Gem din API-nøgle",
"copyWarningDescription": "Denne nøgle vil kun blive vist én gang. Opbevar den på et sikkert sted.",
"apiKey": "API Nøgle",
"tokenCopied": "Token kopieret til udklipsholder",
"creating": "Opretter...",
"nameRequired": "Nøgle navn er påkrævet",
"userRequired": "Vælg venligst en bruger",
"failedToCreate": "Kunne ikke oprette API-nøgle",
"noKeys": "Ingen API-nøgler fundet.",
"name": "Navn",
"prefix": "Præfiks",
"lastUsed": "Sidst Brugt",
"never": "Aldrig",
"revokeKey": "Tilbagekald nøgle",
"confirmRevoke": "Er du sikker på, at du vil tilbagekalde API-nøglen\"{{name}}\"? Dette kan ikke fortrydes.",
"revokedSuccessfully": "API-nøgle tilbagekaldt succesfuldt",
"failedToRevoke": "Mislykkedes at tilbagekalde API-nøgle",
"failedToFetch": "Kunne ikke hente API-nøgler"
}
},
"hosts": {
"title": "Vært Manager",
@@ -962,28 +1004,8 @@
"enableDocker": "Aktiver Docker",
"defaultPath": "Standard Sti",
"defaultPathDesc": "Standard mappe ved åbning af filhåndtering for denne vært",
"tunnelConnections": "Tunnelforbindelser",
"connection": "Forbindelse",
"remove": "Fjern",
"sourcePort": "Kilde Port",
"sourcePortDesc": " (Ressource refererer til de aktuelle tilslutningsdetaljer i fanen Generelt)",
"endpointPort": "Endepunkt Port",
"endpointSshConfig": "Indstilling Af Endepunkt SSH",
"tunnelForwardDescription": "Denne tunnel vil videresende trafikken fra havnen {{sourcePort}} på kildemaskinen (aktuelle forbindelsesdetaljer i den generelle fane) til port {{endpointPort}} på endepunktsmaskinen.",
"maxRetries": "Maks. Forsøg",
"maxRetriesDescription": "Maksimalt antal forsøg på at prøve igen for tunnelforbindelse.",
"retryInterval": "Forsøg Interval (sekunder)",
"retryIntervalDescription": "Tid til at vente mellem forsøg igen.",
"autoStartContainer": "Automatisk start ved Container Start",
"autoStartDesc": "Start automatisk denne tunnel når beholderen starter",
"addConnection": "Tilføj Tunnelforbindelse",
"tunnelType": "Tunnel Type",
"tunnelTypeLocal": "Lokal (-L)",
"tunnelTypeRemote": "Fjernbetjening (-R)",
"tunnelTypeLocalDesc": "Videresend lokal port til fjern-endepunkt",
"tunnelTypeRemoteDesc": "Videresend fjernport til lokal maskine",
"tunnelForwardDescriptionLocal": "Denne tunnel vil videresende trafikken fra den lokale havn {{sourcePort}} til port {{endpointPort}} på slutmaskinen.",
"tunnelForwardDescriptionRemote": "Denne tunnel vil videresende trafikken fra havnen {{sourcePort}} på kildemaskinen (aktuelle forbindelsesdetaljer i den generelle fane) til port {{endpointPort}} på endepunktsmaskinen.",
"sshpassRequired": "Sshpass Kræves For Adgangskodegodkendelse",
"sshpassRequiredDesc": "For adgangskodegodkendelse i tunneler skal sshpass installeres på systemet.",
"otherInstallMethods": "Andre installationsmetoder:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Opdel Lodret",
"closePanel": "Luk Panel",
"reconnect": "Genopret",
"sessionEnded": "Session Afsluttet",
"sessionEnded": "Session afsluttet.",
"connectionLost": "Forbindelse tabt",
"error": "FEJL: {{message}}",
"disconnected": "Afbrudt",
@@ -1933,22 +1955,10 @@
"ascending": "Stigende",
"descending": "Faldende"
},
"tunnel": {
"log": {
"connecting": "Etablerer tunnelforbindelse...",
"sourceConnecting": "Forbinder til kildevært {{host}}...",
"sourceConnected": "Forbundet til kildevært",
"endpointResolving": "Fortolker værten {{host}}...",
"endpointResolved": "Endepunkt vært løst",
"forwarding": "Opsætning af videresendelse af port {{localPort}} → {{remotePort}}...",
"connected": "Tunnel oprettet med succes",
"retrying": "Forbindelse mislykkedes, prøver igen i {{countdown}}s (forsøg {{attempt}}/{{max}})...",
"retryExhausted": "Maks. gentagelsesforsøg nået",
"error": "Tunnelforbindelse mislykkedes: {{error}}"
}
},
"tunnels": {
"title": "SSH-tunneler",
"noTunnelsConfigured": "Ingen Tunneler Konfigureret",
"configureTunnelsInHostSettings": "Konfigurer tunnelforbindelser i værtshåndteringen for at komme i gang",
"noSshTunnels": "Ingen SSH-tunneler",
"createFirstTunnelMessage": "Du har endnu ikke oprettet nogen SSH-tunneler. Konfigurer tunnelforbindelser i værtshåndteringen for at komme i gang.",
"connected": "Forbundet",
@@ -1969,27 +1979,100 @@
"disconnect": "Afbryd",
"cancel": "Annuller",
"port": "Port",
"localPort": "Lokal Port",
"remotePort": "Ekstern Port",
"currentHostPort": "Nuværende Værts Port",
"endpointPort": "Endepunkt Port",
"bindIp": "Lokal IP",
"currentHostIp": "Nuværende Vært Ip",
"endpointSshConfig": "Indstilling Af Endepunkt SSH",
"endpointSshConfigRequired": "Endpoint SSH konfiguration er påkrævet",
"endpointSshHost": "Endepunkt SSH Host",
"endpointSshHostPlaceholder": "Vælg en konfigureret vært",
"endpointSshHostRequired": "Vælg en SSH-vært for hver klienttunnel.",
"attempt": "Forsøg {{current}} af {{max}}",
"nextRetryIn": "Næste forsøg igen i {{seconds}} sekunder",
"checkDockerLogs": "Tjek dine Docker logs for fejlårsagen, deltag i",
"orCreate": "eller opret en ",
"noTunnelConnections": "Ingen tunnelforbindelser konfigureret",
"tunnelConnections": "Tunnelforbindelser",
"serverTunnels": "Servertunneler",
"serverTunnelsDesc": "Backend-administrerede tunneler gemt med denne vært.",
"clientTunnels": "Klient Tunneler",
"clientTunnelsUnavailable": "Klienttunneler kræver en desktop klient.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Klient Tunnel",
"addServerTunnel": "Tilføj Servertunnel",
"addClientTunnel": "Tilføj Klienttunnel",
"noServerTunnels": "Ingen servertunneler konfigureret.",
"noClientTunnels": "Ingen klienttunneler konfigureret på dette skrivebord.",
"manageClientTunnels": "Administrer Klienttunneler",
"addTunnel": "Tilføj Tunnel",
"editTunnel": "Rediger Tunnel",
"deleteTunnel": "Slet Tunnel",
"tunnelName": "Tunnel Navn",
"localPort": "Lokal Port",
"remoteHost": "Ekstern Vært",
"remotePort": "Ekstern Port",
"autoStart": "Automatisk Start",
"autoStartContainer": "Start automatisk ved start",
"autoStartContainerDesc": "Start automatisk denne tunnel når din Termix-server starter.",
"autoStartEnableFailed": "Vært gemt, men kunne ikke starte auto-start tunneler for {{name}}.",
"clientAutoStartDesc": "Starter når denne desktop klient åbner og forbliver tilsluttet.",
"clientManualStartDesc": "Brug Start og Stop fra denne række. Termix vil ikke åbne den automatisk.",
"clientRemoteServerNote": "Fjernvideresendelse kan kræve AllowTcpForwarding og GatewayPorts på SSH-slutserveren. Fjernporten lukker, når skrivebordet afbryder.",
"clientTunnelStarted": "Klienttunnel startet",
"clientTunnelStopped": "Klient tunnel stoppet",
"tunnelTestSucceeded": "Tunneltesten lykkedes",
"tunnelTestFailed": "Tunneltest mislykkedes",
"localSaved": "Klienttunneler gemt",
"localSaveError": "Kunne ikke gemme lokale klienttunneler",
"invalidBindIp": "Lokal IP skal være en gyldig IPv4-adresse.",
"invalidLocalTargetIp": "Lokalt mål IP skal være en gyldig IPv4-adresse.",
"invalidCurrentHostIp": "Nuværende værts IP skal være en gyldig IPv4-adresse.",
"invalidLocalPort": "Lokal port skal være mellem 1 og 65535.",
"invalidRemotePort": "Fjernport skal være mellem 1 og 65535.",
"invalidLocalTargetPort": "Lokal målport skal være mellem 1 og 65535.",
"invalidEndpointPort": "Endepunkthavn skal være mellem 1 og 65535.",
"duplicateAutoStartBind": "Kun én auto-start klienttunnel kan bruge {{bind}}.",
"manualControlError": "Mislykkedes at opdatere tunneltilstand.",
"saveHostBeforeManualControl": "Gem denne vært, før du starter eller stopper dens servertunneller.",
"status": "Status",
"active": "Aktiv",
"inactive": "Inaktiv",
"start": "Start",
"stop": "Stop",
"test": "Test",
"restart": "Genstart",
"connectionType": "Forbindelsestype",
"type": "Tunnel Type",
"typeLocal": "Lokal (-L)",
"typeRemote": "Fjernbetjening (-R)",
"typeDynamic": "Dynamisk (-D)",
"typeServerLocalDesc": "Nuværende vært for slutpunktet.",
"typeServerRemoteDesc": "Endepunkt tilbage til nuværende vært.",
"typeClientLocalDesc": "Lokal computer til slutpunkt.",
"typeClientRemoteDesc": "Slutpunkt tilbage til lokal computer.",
"typeClientDynamicDesc": "SOCKS på lokal computer.",
"typeDynamicDesc": "Videresend SOCKS5 CONNECT trafik gennem SSH",
"forwardDescriptionServerLocal": "Nuværende vært {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS på nuværende vært {{sourcePort}}.",
"forwardDescriptionClientLocal": "Lokal {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS på lokal port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Lokal {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → Lokal {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Rute:",
"lastStarted": "Sidst startet",
"lastTested": "Sidst testet",
"lastError": "Seneste fejl",
"maxRetries": "Maks. Forsøg",
"maxRetriesDescription": "Maksimal mængde af forsøg igen.",
"retryInterval": "Forsøg Interval (sekunder)",
"retryIntervalDescription": "Tid til at vente mellem forsøg igen.",
"local": "Lokal",
"remote": "Fjernbetjening",
"dynamic": "Dynamisk",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Angiv venligst dine SSH-legitimationsoplysninger for at oprette forbindelse til denne server.",
"sshPasswordDescription": "Indtast adgangskoden til denne SSH-forbindelse.",
"sshKeyPasswordDescription": "Hvis din SSH-nøgle er krypteret, så skriv adgangskoden her.",
"passphraseRequired": "Kodeord Kræves",
"passphraseRequiredDescription": "SSH-tasten er krypteret. Indtast kodeord for at låse den op.",
"step1ScanQR": "Trin 1: Scan QR-koden med din autentificerings-app",
"manualEntryCode": "Manuel Indgangskode",
"cannotScanQRText": "Hvis du ikke kan scanne QR-koden, skal du indtaste denne kode manuelt i din autentificeringsapp",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Kunne ikke fuldføre nulstilling af adgangskode",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Mislykkedes at starte OIDC login",
"failedUserInfo": "Kunne ikke hente brugerinfo efter OIDC login",
"failedUserInfo": "Kunne ikke hente brugerinfo efter login",
"oidcAuthFailed": "OIDC godkendelse mislykkedes",
"noTokenReceived": "Ingen token modtaget fra login",
"invalidAuthUrl": "Ugyldig autorisation URL modtaget fra backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Vis tags under hver vært i sidepanelet. Deaktivér for at skjule alle tags.",
"account": "Konto",
"appearance": "Udseende",
"c2sTunnelConfigDesc": "Lokale stationære tunneler rettet mod konfigurerede SSH-værter.",
"c2sTunnelPresets": "Klient Tunnel Forudindstillinger",
"c2sTunnelPresetsDesc": "Gem denne desktop klients lokale tunnelliste som en navngiven server forudindstilling, eller indlæs en forudindstillet tilbage i denne klient.",
"c2sTunnelPresetsUnavailable": "Klienttunnelforudindstillinger er kun tilgængelige i skrivebordsklienten.",
"c2sPresetName": "Forudindstillet Navn",
"c2sPresetNamePlaceholder": "Klient forudindstillede navn",
"c2sPresetToLoad": "Forudindstilling For At Indlæse",
"c2sNoPresetSelected": "Ingen forudindstilling valgt",
"c2sNoPresets": "Ingen forudindstillinger gemt",
"c2sLoadPreset": "Indlæs",
"c2sCurrentLocalConfig": "{{count}} lokal klienttunnel(er) konfigureret på dette skrivebord.",
"c2sPresetSyncNote": "Forudindstillinger er eksplicitte snapshots; indlæsning af en erstatter denne desktop klients lokale klienttunnelliste.",
"c2sPresetSaved": "Klient tunnel forudindstilling gemt",
"c2sPresetLoaded": "Forudindstillede klienttunnel indlæst lokalt",
"c2sPresetRenamed": "Klient tunnel forudindstillet omdøbt",
"c2sPresetDeleted": "Klient tunnel forudindstilling slettet",
"c2sPresetLoadError": "Kunne ikke indlæse klienttunnelforudindstillinger",
"languageLocalization": "Sprog & Lokalisering",
"fileManagerSettings": "Filhåndtering",
"terminalSettings": "Terminal",
@@ -2370,6 +2472,10 @@
"description": "Beskrivelse af SSH legitimationsoplysninger",
"searchCredentials": "Søg efter legitimationsoplysninger efter navn, brugernavn eller tags...",
"sshConfig": "endpoint ssh konfiguration",
"bindLocalhost": "127.0.0.1 (bind til localhost)",
"localListenerHost": "127.0.0.1 (lyt lokalt)",
"localTargetHost": "127.0.0.1 (mål på denne computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/hjem",
"clientId": "din-client-id",
"clientSecret": "din-klient-hemmelighed",
@@ -2549,6 +2655,7 @@
"version": "Version",
"upToDate": "Op til dato",
"updateAvailable": "Opdatering Tilgængelig",
"beta": "Beta",
"uptime": "Oppetid",
"database": "Database",
"healthy": "Sunde",
@@ -2955,6 +3062,20 @@
"error": "Målesamling mislykkedes: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Etablerer tunnelforbindelse...",
"sourceConnecting": "Forbinder til kildevært {{host}}...",
"sourceConnected": "Forbundet til kildevært",
"endpointResolving": "Fortolker værten {{host}}...",
"endpointResolved": "Endepunkt vært løst",
"forwarding": "Opsætning af videresendelse af port {{localPort}} → {{remotePort}}...",
"connected": "Tunnel oprettet med succes",
"retrying": "Forbindelse mislykkedes, prøver igen i {{countdown}}s (forsøg {{attempt}}/{{max}})...",
"retryExhausted": "Maks. gentagelsesforsøg nået",
"error": "Tunnelforbindelse mislykkedes: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Forbinder til SFTP-server...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Sie verwenden Version {{version}}",
"updateAvailable": "Update verfügbar",
"newVersionAvailable": "Eine neue Version ist verfügbar! Sie verwenden {{current}}, aber {{latest}} ist verfügbar.",
"betaVersion": "Beta-Version",
"betaVersionDesc": "Sie verwenden {{current}}, was neuer ist als die neueste stabile Version {{latest}}.",
"releasedOn": "Veröffentlicht am {{date}}",
"downloadUpdate": "Update herunterladen",
"dismiss": "Verwerfen",
@@ -426,6 +428,8 @@
"warning": "Warnung",
"info": "Info",
"success": "Erfolg",
"unsavedChanges": "Ungespeicherte Änderungen",
"dismiss": "Verwerfen",
"loading": "Wird geladen...",
"required": "Benötigt",
"optional": "Optional",
@@ -471,6 +475,7 @@
"chinese": "Chinesisch",
"german": "Deutsch",
"cancel": "Abbrechen",
"done": "Fertig",
"username": "Benutzername",
"name": "Name",
"login": "Anmelden",
@@ -486,6 +491,7 @@
"save": "Speichern",
"saving": "Speichern...",
"delete": "Löschen",
"rename": "Umbenennen",
"edit": "Bearbeiten",
"add": "Neu",
"search": "Suchen",
@@ -559,7 +565,8 @@
"passwordCopied": "Passwort in Zwischenablage kopiert",
"sudoPasswordCopied": "Sudo-Passwort in Zwischenablage kopiert",
"noPasswordAvailable": "Kein Passwort verfügbar",
"failedToCopyPassword": "Fehler beim Kopieren des Passworts"
"failedToCopyPassword": "Fehler beim Kopieren des Passworts",
"openFileManager": "Dateimanager öffnen"
},
"admin": {
"title": "Admin-Einstellungen",
@@ -868,7 +875,42 @@
"passwordMinLength": "Passwort muss mindestens 6 Zeichen lang sein",
"currentRoles": "Aktuelle Rollen",
"noRolesAssigned": "Keine Rollen zugewiesen",
"assignNewRole": "Neue Rolle zuweisen"
"assignNewRole": "Neue Rolle zuweisen",
"apiKeys": {
"tabLabel": "API-Schlüssel",
"title": "API-Schlüssel",
"createApiKey": "API-Schlüssel erstellen",
"createApiKeyDescription": "Erstellen Sie einen neuen API-Schlüssel für einen bestimmten Benutzer. Der Token wird nur einmal angezeigt.",
"keyCreated": "API-Schlüssel erstellt",
"keyCreatedDescription": "Diesen Schlüssel jetzt kopieren — er wird nicht wieder angezeigt.",
"keyName": "Schlüsselname",
"keyNamePlaceholder": "z.B. CI/CD Pipeline",
"scopedUser": "Gespeicherter Benutzer",
"selectUser": "Benutzer auswählen...",
"searchUsers": "Benutzer suchen...",
"noUsersFound": "Keine Benutzer gefunden.",
"expiresAt": "Gültig bis",
"optional": "optional",
"expiresAtHelp": "Leer lassen für einen Schlüssel, der nie ablaufen kann.",
"copyWarningTitle": "API-Schlüssel speichern",
"copyWarningDescription": "Dieser Schlüssel wird nur einmal angezeigt. Speichern Sie ihn an einem sicheren Ort.",
"apiKey": "API-Schlüssel",
"tokenCopied": "Token in Zwischenablage kopiert",
"creating": "Erstellen...",
"nameRequired": "Schlüsselname ist erforderlich",
"userRequired": "Bitte wählen Sie einen Benutzer",
"failedToCreate": "API-Schlüssel konnte nicht erstellt werden",
"noKeys": "Keine API-Schlüssel gefunden.",
"name": "Name",
"prefix": "Präfix",
"lastUsed": "Zuletzt verwendet",
"never": "Nie",
"revokeKey": "Schlüssel widerrufen",
"confirmRevoke": "Sind Sie sicher, dass Sie den API-Schlüssel \"{{name}}\" widerrufen möchten? Dies kann nicht rückgängig gemacht werden.",
"revokedSuccessfully": "API-Schlüssel erfolgreich widerrufen",
"failedToRevoke": "API-Schlüssel konnte nicht widerrufen",
"failedToFetch": "Fehler beim Abrufen der API-Schlüssel"
}
},
"hosts": {
"title": "Host-Manager",
@@ -962,28 +1004,8 @@
"enableDocker": "Docker aktivieren",
"defaultPath": "Standardpfad",
"defaultPathDesc": "Standardverzeichnis beim Öffnen des Dateimanagers für diesen Host",
"tunnelConnections": "Tunnelverbindungen",
"connection": "Verbindung",
"remove": "Entfernen",
"sourcePort": "Quellport",
"sourcePortDesc": " (Quelle bezieht sich auf die aktuellen Verbindungsdetails in der Registerkarte Allgemein)",
"endpointPort": "Endpunkt-Port",
"endpointSshConfig": "Endpunkt SSH Konfiguration",
"tunnelForwardDescription": "Dieser Tunnel leitet den Verkehr von Port {{sourcePort}} auf der Quellmaschine (aktuelle Verbindungsdetails im allgemeinen Tab) an Port {{endpointPort}} auf dem Endpunkt-Rechner weiter.",
"maxRetries": "Max. Wiederholungen",
"maxRetriesDescription": "Maximale Anzahl der Wiederholungsversuche für die Tunnelverbindung.",
"retryInterval": "Wiederholungsintervall (Sekunden)",
"retryIntervalDescription": "Wartezeit zwischen wiederholten Versuchen.",
"autoStartContainer": "Beim Start des Containers automatisch starten",
"autoStartDesc": "Diesen Tunnel automatisch starten, wenn der Container startet",
"addConnection": "Tunnelverbindung hinzufügen",
"tunnelType": "Tunneltyp",
"tunnelTypeLocal": "Lokal (-L)",
"tunnelTypeRemote": "Remote (-R)",
"tunnelTypeLocalDesc": "Lokalen Port zum entfernten Endpunkt weiterleiten",
"tunnelTypeRemoteDesc": "Remote-Port an lokalen Rechner weiterleiten",
"tunnelForwardDescriptionLocal": "Dieser Tunnel leitet den Verkehr vom lokalen Port {{sourcePort}} an Port {{endpointPort}} auf der Endpunktmaschine weiter.",
"tunnelForwardDescriptionRemote": "Dieser Tunnel leitet den Verkehr von Port {{sourcePort}} auf der Quellmaschine (aktuelle Verbindungsdetails im allgemeinen Tab) an Port {{endpointPort}} auf dem Endpunkt-Rechner weiter.",
"sshpassRequired": "Sshpass erforderlich für Passwort-Authentifizierung",
"sshpassRequiredDesc": "Für die Passwort-Authentifizierung in Tunnels muss sshpass auf dem System installiert sein.",
"otherInstallMethods": "Andere Installationsmethoden:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Vertikal teilen",
"closePanel": "Panel schließen",
"reconnect": "Neu verbinden",
"sessionEnded": "Sitzung beendet",
"sessionEnded": "Sitzung beendet.",
"connectionLost": "Verbindung verloren",
"error": "FEHLER: {{message}}",
"disconnected": "Verbindung getrennt",
@@ -1933,22 +1955,10 @@
"ascending": "Aufsteigend",
"descending": "Absteigend"
},
"tunnel": {
"log": {
"connecting": "Tunnelverbindung wird eingerichtet...",
"sourceConnecting": "Verbindung zum Quell-Host {{host}}...",
"sourceConnected": "Mit Quell-Host verbunden",
"endpointResolving": "Löse Endpunkt-Host {{host}}...",
"endpointResolved": "Endpunkt-Host gelöst",
"forwarding": "Port-Weiterleitung von {{localPort}} → {{remotePort}} einrichten ...",
"connected": "Tunnel erfolgreich erstellt",
"retrying": "Verbindung fehlgeschlagen, Wiederversuchen in {{countdown}}s (Versuch {{attempt}}/{{max}})...",
"retryExhausted": "Maximale Wiederholungsversuche erreicht",
"error": "Tunnelverbindung fehlgeschlagen: {{error}}"
}
},
"tunnels": {
"title": "SSH-Tunnel",
"noTunnelsConfigured": "Keine Tunnel konfiguriert",
"configureTunnelsInHostSettings": "Konfiguriere Tunnelverbindungen im Host-Manager um loszulegen",
"noSshTunnels": "Keine SSH-Tunnel",
"createFirstTunnelMessage": "Sie haben noch keine SSH-Tunnel erstellt. Konfigurieren Sie Tunnelverbindungen im Host Manager, um loszulegen.",
"connected": "Verbunden",
@@ -1969,27 +1979,100 @@
"disconnect": "Verbindung trennen",
"cancel": "Abbrechen",
"port": "Port",
"localPort": "Lokaler Port",
"remotePort": "Entfernter Port",
"currentHostPort": "Aktueller Host-Port",
"endpointPort": "Endpunkt-Port",
"bindIp": "Lokale IP",
"currentHostIp": "Aktuelle Host-IP",
"endpointSshConfig": "Endpunkt SSH Konfiguration",
"endpointSshConfigRequired": "Endpunkt SSH Konfiguration ist erforderlich",
"endpointSshHost": "Endpunkt SSH-Host",
"endpointSshHostPlaceholder": "Wähle einen konfigurierten Host",
"endpointSshHostRequired": "Wählen Sie einen Endpunkt-SSH-Host für jeden Client-Tunnel.",
"attempt": "{{current}} von {{max}} versuchen",
"nextRetryIn": "Nächste Wiederholung in {{seconds}} Sekunden",
"checkDockerLogs": "Überprüfen Sie Ihre Docker Logs aus der Fehlerbegründung, melden Sie sich an",
"orCreate": "oder erstellen Sie ein ",
"noTunnelConnections": "Keine Tunnelverbindungen konfiguriert",
"tunnelConnections": "Tunnelverbindungen",
"serverTunnels": "Servertunnel",
"serverTunnelsDesc": "Backend-verwaltete Tunnel mit diesem Host gespeichert.",
"clientTunnels": "Kliententunnel",
"clientTunnelsUnavailable": "Kliententunnel benötigen einen Desktop-Client.",
"serverTunnel": "Servertunnel",
"clientTunnel": "Kliententunnel",
"addServerTunnel": "Servertunnel hinzufügen",
"addClientTunnel": "Kliententunnel hinzufügen",
"noServerTunnels": "Keine Servertunnel konfiguriert.",
"noClientTunnels": "Keine Client-Tunnel auf diesem Desktop konfiguriert.",
"manageClientTunnels": "Kliententunnel verwalten",
"addTunnel": "Tunnel hinzufügen",
"editTunnel": "Tunnel bearbeiten",
"deleteTunnel": "Tunnel löschen",
"tunnelName": "Tunnelname",
"localPort": "Lokaler Port",
"remoteHost": "Entfernter Host",
"remotePort": "Entfernter Port",
"autoStart": "Autostart",
"autoStartContainer": "Auto-Start beim Start",
"autoStartContainerDesc": "Diesen Tunnel automatisch starten, wenn Ihr Termix Server gestartet wird.",
"autoStartEnableFailed": "Host gespeichert, aber Fehler beim Autostart von Tunnels für {{name}}.",
"clientAutoStartDesc": "Startet, wenn sich dieser Desktop-Client öffnet und verbunden bleibt.",
"clientManualStartDesc": "Verwenden Sie Start und Stop aus dieser Zeile. Termix öffnet sie nicht automatisch.",
"clientRemoteServerNote": "Remote-Weiterleitung kann AllowTcpForwarding und GatewayPorts auf dem SSH-Server erfordern. Der entfernte Port wird geschlossen, wenn dieser Desktop sich trennt.",
"clientTunnelStarted": "Kliententunnel gestartet",
"clientTunnelStopped": "Kliententunnel gestoppt",
"tunnelTestSucceeded": "Tunneltest erfolgreich",
"tunnelTestFailed": "Tunneltest fehlgeschlagen",
"localSaved": "Kliententunnel gespeichert",
"localSaveError": "Fehler beim Speichern lokaler Client-Tunnel",
"invalidBindIp": "Lokale IP muss eine gültige IPv4-Adresse sein.",
"invalidLocalTargetIp": "Lokale Ziel-IP muss eine gültige IPv4-Adresse sein.",
"invalidCurrentHostIp": "Aktuelle Host-IP muss eine gültige IPv4-Adresse sein.",
"invalidLocalPort": "Der lokale Port muss zwischen 1 und 65535 liegen.",
"invalidRemotePort": "Der Remote Port muss zwischen 1 und 65535 liegen.",
"invalidLocalTargetPort": "Lokaler Port muss zwischen 1 und 65535 liegen.",
"invalidEndpointPort": "Endpunkt-Port muss zwischen 1 und 65535 liegen.",
"duplicateAutoStartBind": "Nur ein Kliententunnel kann {{bind}} verwenden.",
"manualControlError": "Tunnel Status konnte nicht aktualisiert werden.",
"saveHostBeforeManualControl": "Speichere diesen Host, bevor du seine Servertunnel startest oder stoppst.",
"status": "Status",
"active": "Aktiv",
"inactive": "Inaktiv",
"start": "Start",
"stop": "Stoppen",
"test": "Testen",
"restart": "Neustart",
"connectionType": "Verbindungstyp",
"type": "Tunneltyp",
"typeLocal": "Lokal (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamisch (-D)",
"typeServerLocalDesc": "Aktueller Host bis Endpunkt.",
"typeServerRemoteDesc": "Endpunkt zurück zum aktuellen Host.",
"typeClientLocalDesc": "Lokaler Computer bis Endpunkt.",
"typeClientRemoteDesc": "Endpunkt zurück zum lokalen Computer.",
"typeClientDynamicDesc": "SOCKS auf lokalem Computer.",
"typeDynamicDesc": "SOCKS5 CONNECT Verkehr durch SSH weiterleiten",
"forwardDescriptionServerLocal": "Aktueller Host {{sourcePort}} → Endpunkt {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpunkt {{endpointPort}} → aktueller Host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS auf dem aktuellen Host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Lokale {{sourcePort}} → Remote- {{endpointPort}}.",
"forwardDescriptionClientRemote": "Entfernte {{sourcePort}} → Lokale {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS auf lokalen Port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Lokale {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → lokale {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Zuletzt gestartet",
"lastTested": "Zuletzt geprüft",
"lastError": "Letzter Fehler",
"maxRetries": "Max. Wiederholungen",
"maxRetriesDescription": "Maximale Anzahl an Wiederholungsversuchen.",
"retryInterval": "Wiederholungsintervall (Sekunden)",
"retryIntervalDescription": "Wartezeit zwischen wiederholten Versuchen.",
"local": "Lokal",
"remote": "Entfernt",
"dynamic": "Dynamisch",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Bitte geben Sie Ihre SSH-Zugangsdaten an, um sich mit diesem Server zu verbinden.",
"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.",
"passphraseRequired": "Passphrase erforderlich",
"passphraseRequiredDescription": "Der SSH-Schlüssel ist verschlüsselt. Bitte geben Sie die Passphrase ein, um ihn zu entsperren.",
"step1ScanQR": "Schritt 1: Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App",
"manualEntryCode": "Manueller Eintrag-Code",
"cannotScanQRText": "Wenn Sie den QR-Code nicht scannen können, geben Sie diesen Code manuell in Ihre Authentifizierungs-App ein",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Fehler beim Zurücksetzen des Passworts",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Fehler beim Starten des OIDC-Logins",
"failedUserInfo": "Fehler beim Abrufen der Benutzerinformationen nach dem OIDC-Login",
"failedUserInfo": "Fehler beim Abrufen der Benutzerinformationen nach dem Login",
"oidcAuthFailed": "OIDC Authentifizierung fehlgeschlagen",
"noTokenReceived": "Kein Token vom Login erhalten",
"invalidAuthUrl": "Ungültige Autorisierungs-URL vom Backend erhalten",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Tags unter jedem Host in der Seitenleiste anzeigen. Deaktivieren, um alle Tags auszublenden.",
"account": "Konto",
"appearance": "Erscheinung",
"c2sTunnelConfigDesc": "Lokale Desktop-Tunnel, die auf konfigurierte SSH-Hosts abzielen.",
"c2sTunnelPresets": "Client-Tunnel Voreinstellungen",
"c2sTunnelPresetsDesc": "Speichere die lokale Tunnelliste dieses Desktop-Clients als benannte Server-Voreinstellung oder lade eine Voreinstellung zurück in diesen Client.",
"c2sTunnelPresetsUnavailable": "Voreinstellungen für den Client Tunnel sind nur im Desktop-Client verfügbar.",
"c2sPresetName": "Preset-Name",
"c2sPresetNamePlaceholder": "Name des Presets",
"c2sPresetToLoad": "Voreinstellung zum Laden",
"c2sNoPresetSelected": "Keine Vorlage ausgewählt",
"c2sNoPresets": "Keine Voreinstellungen gespeichert",
"c2sLoadPreset": "Laden",
"c2sCurrentLocalConfig": "{{count}} lokale Clienttunnel(s) auf dieser Arbeitsfläche konfiguriert.",
"c2sPresetSyncNote": "Voreinstellung sind explizite Schnappschüsse; das Laden eines Gerätes ersetzt die lokale Liste des Client-Tunnels.",
"c2sPresetSaved": "Voreinstellung für Kliententunnel gespeichert",
"c2sPresetLoaded": "Voreinstellung für Client-Tunnel lokal geladen",
"c2sPresetRenamed": "Voreinstellung für Kliententunnel umbenannt",
"c2sPresetDeleted": "Voreinstellung für Kliententunnel gelöscht",
"c2sPresetLoadError": "Fehler beim Laden der Client-Tunnel Voreinstellungen",
"languageLocalization": "Sprache & Lokalisierung",
"fileManagerSettings": "Datei-Manager",
"terminalSettings": "Terminal",
@@ -2370,6 +2472,10 @@
"description": "SSH-Anmeldeinformationen Beschreibung",
"searchCredentials": "Anmeldedaten nach Namen, Benutzernamen oder Tags suchen...",
"sshConfig": "endpoint ssh Konfiguration",
"bindLocalhost": "127.0.0.1 (an localhost binden)",
"localListenerHost": "127.0.0.1 (lokal lauschen)",
"localTargetHost": "127.0.0.1 (Ziel auf diesem Computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/home",
"clientId": "eigene Client-Id",
"clientSecret": "dein-client-Secret",
@@ -2549,6 +2655,7 @@
"version": "Version",
"upToDate": "Aktuell",
"updateAvailable": "Update verfügbar",
"beta": "Beta",
"uptime": "Laufzeit",
"database": "Datenbank",
"healthy": "Gesund",
@@ -2955,6 +3062,20 @@
"error": "Metrik-Sammlung fehlgeschlagen: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Tunnelverbindung wird eingerichtet...",
"sourceConnecting": "Verbindung zum Quell-Host {{host}}...",
"sourceConnected": "Mit Quell-Host verbunden",
"endpointResolving": "Löse Endpunkt-Host {{host}}...",
"endpointResolved": "Endpunkt-Host gelöst",
"forwarding": "Port-Weiterleitung von {{localPort}} → {{remotePort}} einrichten ...",
"connected": "Tunnel erfolgreich erstellt",
"retrying": "Verbindung fehlgeschlagen, Wiederversuchen in {{countdown}}s (Versuch {{attempt}}/{{max}})...",
"retryExhausted": "Maximale Wiederholungsversuche erreicht",
"error": "Tunnelverbindung fehlgeschlagen: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Verbindung zum SFTP-Server...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Εκτελείτε την έκδοση {{version}}",
"updateAvailable": "Διαθέσιμη Ενημέρωση",
"newVersionAvailable": "Μια νέα έκδοση είναι διαθέσιμη! Εκτελείτε {{current}}αλλά το {{latest}} είναι διαθέσιμο.",
"betaVersion": "Έκδοση Beta",
"betaVersionDesc": "Τρέχετε {{current}}, το οποίο είναι νεότερο από την τελευταία σταθερή κυκλοφορία {{latest}}.",
"releasedOn": "Κυκλοφόρησε στο {{date}}",
"downloadUpdate": "Λήψη Ενημέρωσης",
"dismiss": "Απόρριψη",
@@ -426,6 +428,8 @@
"warning": "Προειδοποίηση",
"info": "Πληροφορίες",
"success": "Επιτυχία",
"unsavedChanges": "Μη αποθηκευμένες αλλαγές",
"dismiss": "Απόρριψη",
"loading": "Φόρτωση...",
"required": "Απαιτείται",
"optional": "Προαιρετικό",
@@ -471,6 +475,7 @@
"chinese": "Κινέζικα",
"german": "Γερμανικά",
"cancel": "Ακύρωση",
"done": "Ολοκληρώθηκε",
"username": "Όνομα Χρήστη",
"name": "Όνομα",
"login": "Είσοδος",
@@ -486,6 +491,7 @@
"save": "Αποθήκευση",
"saving": "Αποθηκεύεται...",
"delete": "Διαγραφή",
"rename": "Μετονομασία",
"edit": "Επεξεργασία",
"add": "Προσθήκη",
"search": "Αναζήτηση",
@@ -559,7 +565,8 @@
"passwordCopied": "Ο κωδικός αντιγράφηκε στο πρόχειρο",
"sudoPasswordCopied": "Ο κωδικός Sudo αντιγράφηκε στο πρόχειρο",
"noPasswordAvailable": "Δεν υπάρχει διαθέσιμος κωδικός πρόσβασης",
"failedToCopyPassword": "Αποτυχία αντιγραφής κωδικού πρόσβασης"
"failedToCopyPassword": "Αποτυχία αντιγραφής κωδικού πρόσβασης",
"openFileManager": "Άνοιγμα Διαχειριστή Αρχείων"
},
"admin": {
"title": "Ρυθμίσεις Διαχειριστή",
@@ -868,7 +875,42 @@
"passwordMinLength": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 6 χαρακτήρες",
"currentRoles": "Τρέχοντες Ρόλοι",
"noRolesAssigned": "Δεν έχουν ανατεθεί ρόλοι",
"assignNewRole": "Εκχώρηση Νέου Ρόλου"
"assignNewRole": "Εκχώρηση Νέου Ρόλου",
"apiKeys": {
"tabLabel": "Κλειδιά API",
"title": "Κλειδιά API",
"createApiKey": "Δημιουργία Κλειδιού Api",
"createApiKeyDescription": "Δημιουργία ενός νέου κλειδιού API για συγκεκριμένο χρήστη. Το διακριτικό εμφανίζεται μόνο μία φορά.",
"keyCreated": "Το Κλειδί Api Δημιουργήθηκε",
"keyCreatedDescription": "Αντιγραφή αυτού του κλειδιού τώρα - δεν θα εμφανιστεί ξανά.",
"keyName": "Όνομα Κλειδιού",
"keyNamePlaceholder": "π.χ. Αγωγός CI/CD",
"scopedUser": "Εμβέλεια Χρήστη",
"selectUser": "Επιλέξτε έναν χρήστη...",
"searchUsers": "Αναζήτηση χρηστη...",
"noUsersFound": "Δεν βρέθηκαν χρήστες.",
"expiresAt": "Λήγει Στις",
"optional": "προαιρετικό",
"expiresAtHelp": "Αφήστε κενό για ένα κλειδί που δεν λήγει ποτέ.",
"copyWarningTitle": "Αποθηκεύστε το κλειδί API σας",
"copyWarningDescription": "Αυτό το κλειδί θα εμφανίζεται μόνο μία φορά. Αποθηκεύστε το σε ασφαλές μέρος.",
"apiKey": "Κλειδί API",
"tokenCopied": "Το διακριτικό αντιγράφηκε στο πρόχειρο",
"creating": "Δημιουργία...",
"nameRequired": "Απαιτείται όνομα κλειδιού",
"userRequired": "Παρακαλώ επιλέξτε ένα χρήστη",
"failedToCreate": "Αποτυχία δημιουργίας κλειδιού API",
"noKeys": "Δεν βρέθηκαν κλειδιά API.",
"name": "Όνομα",
"prefix": "Πρόθεμα",
"lastUsed": "Τελευταία Χρήση",
"never": "Ποτέ",
"revokeKey": "Ανάκληση κλειδιού",
"confirmRevoke": "Είστε βέβαιοι ότι θέλετε να ανακαλέσετε το κλειδί API \"{{name}}\"? Αυτό δεν μπορεί να αναιρεθεί.",
"revokedSuccessfully": "Το κλειδί API ανακλήθηκε επιτυχώς",
"failedToRevoke": "Αποτυχία ανάκλησης κλειδιού API",
"failedToFetch": "Αποτυχία ανάκτησης κλειδιών API"
}
},
"hosts": {
"title": "Διαχειριστής Υπολογιστών",
@@ -962,28 +1004,8 @@
"enableDocker": "Ενεργοποίηση Docker",
"defaultPath": "Προεπιλεγμένη Διαδρομή",
"defaultPathDesc": "Προεπιλεγμένος κατάλογος κατά το άνοιγμα του διαχειριστή αρχείων για αυτόν τον κεντρικό υπολογιστή",
"tunnelConnections": "Συνδέσεις Tunnel",
"connection": "Σύνδεση",
"remove": "Αφαίρεση",
"sourcePort": "Θύρα Πηγής",
"sourcePortDesc": " (Η πηγή αναφέρεται στις τρέχουσες λεπτομέρειες σύνδεσης στην καρτέλα Γενικά)",
"endpointPort": "Θύρα Τελικού Σημείου",
"endpointSshConfig": "Ρύθμιση Τελικού Σημείου SSH",
"tunnelForwardDescription": "Αυτή η σήραγγα θα προωθήσει την κυκλοφορία από το λιμάνι {{sourcePort}} στο πηγαίο μηχάνημα (τρέχουσες λεπτομέρειες σύνδεσης γενικά στην καρτέλα) στη θύρα {{endpointPort}} στη μηχανή τελικού σημείου.",
"maxRetries": "Μέγιστες Επαναλήψεις",
"maxRetriesDescription": "Μέγιστος αριθμός προσπαθειών επανάληψης για σύνδεση με σήραγγα.",
"retryInterval": "Διάστημα Επανάληψη (δευτερόλεπτα)",
"retryIntervalDescription": "Χρόνος αναμονής μεταξύ προσπαθειών επανάληψης.",
"autoStartContainer": "Αυτόματη εκκίνηση στο Container",
"autoStartDesc": "Αυτόματη εκκίνηση αυτής της σήραγγας όταν εκκινήσει ο περιέκτης",
"addConnection": "Προσθήκη Σύνδεσης Tunnel",
"tunnelType": "Τύπος Tunnel",
"tunnelTypeLocal": "Τοπικό (-L)",
"tunnelTypeRemote": "Απομακρυσμένο (-R)",
"tunnelTypeLocalDesc": "Προώθηση τοπικής θύρας για απομακρυσμένο τελικό σημείο",
"tunnelTypeRemoteDesc": "Προώθηση απομακρυσμένης θύρας στον τοπικό υπολογιστή",
"tunnelForwardDescriptionLocal": "Αυτή η σήραγγα θα προωθήσει την κυκλοφορία από το τοπικό λιμάνι {{sourcePort}} στο λιμάνι {{endpointPort}} στο μηχάνημα τελικού σημείου.",
"tunnelForwardDescriptionRemote": "Αυτή η σήραγγα θα προωθήσει την κυκλοφορία από το λιμάνι {{sourcePort}} στο πηγαίο μηχάνημα (τρέχουσες λεπτομέρειες σύνδεσης γενικά στην καρτέλα) στη θύρα {{endpointPort}} στη μηχανή τελικού σημείου.",
"sshpassRequired": "Απαιτείται Sshpass Για Πιστοποίηση Κωδικού Πρόσβασης",
"sshpassRequiredDesc": "Για τον έλεγχο ταυτότητας κωδικού πρόσβασης σε σήραγγες, το sshpass πρέπει να εγκατασταθεί στο σύστημα.",
"otherInstallMethods": "Άλλες μέθοδοι εγκατάστασης:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Κάθετος Διαχωρισμός",
"closePanel": "Κλείσιμο Πίνακα",
"reconnect": "Επανασύνδεση",
"sessionEnded": "Η Συνεδρία Έληξε",
"sessionEnded": "Η συνεδρία έληξε.",
"connectionLost": "Η σύνδεση χάθηκε",
"error": "ΣΦΑΛΜΑ: {{message}}",
"disconnected": "Αποσυνδέθηκε",
@@ -1933,22 +1955,10 @@
"ascending": "Αύξουσα",
"descending": "Φθίνουσα"
},
"tunnel": {
"log": {
"connecting": "Δημιουργία σύνδεσης σήραγγας...",
"sourceConnecting": "Σύνδεση στον πηγαίο υπολογιστή {{host}}...",
"sourceConnected": "Συνδεδεμένο στον πηγαίο υπολογιστή",
"endpointResolving": "Επίλυση του κεντρικού υπολογιστή τελικού σημείου {{host}}...",
"endpointResolved": "Κεντρικός υπολογιστής τελικού σημείου επιλύθηκε",
"forwarding": "Ρύθμιση προώθησης θύρας {{localPort}} → {{remotePort}}...",
"connected": "Η σήραγγα δημιουργήθηκε επιτυχώς",
"retrying": "Η σύνδεση απέτυχε, προσπάθεια ξανά στο {{countdown}}s (προσπάθεια {{attempt}}/{{max}})...",
"retryExhausted": "Επιτεύχθηκαν μέγιστες προσπάθειες επανάληψης",
"error": "Η σύνδεση της σήραγγας απέτυχε: {{error}}"
}
},
"tunnels": {
"title": "SSH Σήραγγες",
"noTunnelsConfigured": "Δεν Ρυθμίστηκαν Σήραγγες",
"configureTunnelsInHostSettings": "Ρυθμίστε τις συνδέσεις δρομολόγησης στο Host Manager για να ξεκινήσετε",
"noSshTunnels": "Χωρίς SSH Tunnels",
"createFirstTunnelMessage": "Δεν έχετε δημιουργήσει ακόμα SSH tunnel. Ρυθμίστε τις συνδέσεις tunnel στο Host Manager για να ξεκινήσετε.",
"connected": "Συνδεδεμένο",
@@ -1969,27 +1979,100 @@
"disconnect": "Αποσύνδεση",
"cancel": "Ακύρωση",
"port": "Θύρα",
"localPort": "Τοπική Θύρα",
"remotePort": "Απομακρυσμένη Θύρα",
"currentHostPort": "Τρέχουσα Θύρα Διακομιστή",
"endpointPort": "Θύρα Τελικού Σημείου",
"bindIp": "Τοπική IP",
"currentHostIp": "Τρέχουσα IP Διακομιστή",
"endpointSshConfig": "Ρύθμιση Τελικού Σημείου SSH",
"endpointSshConfigRequired": "Απαιτείται ρύθμιση παραμέτρων SSH τελικού σημείου",
"endpointSshHost": "Τελικός Διακομιστής SSH",
"endpointSshHostPlaceholder": "Επιλέξτε έναν ρυθμισμένο διακομιστή",
"endpointSshHostRequired": "Επιλέξτε έναν υπολογιστή SSH τελικού σημείου για κάθε δρομολόγηση υπολογιστή-πελάτη.",
"attempt": "Προσπάθεια {{current}} του {{max}}",
"nextRetryIn": "Επόμενη προσπάθεια σε {{seconds}} δευτερόλεπτα",
"checkDockerLogs": "Ελέγξτε τα αρχεία καταγραφής Docker για το λόγο σφάλματος, μπείτε στο",
"orCreate": "ή δημιουργήστε ένα ",
"noTunnelConnections": "Δεν έχουν ρυθμιστεί συνδέσεις διοχέτευσης",
"tunnelConnections": "Συνδέσεις Tunnel",
"serverTunnels": "Σήραγγες Διακομιστή",
"serverTunnelsDesc": "Οπισθοδιαχειριζόμενες σήραγγες αποθηκευμένες με αυτόν τον υπολογιστή.",
"clientTunnels": "Σήραγγες Πελάτη",
"clientTunnelsUnavailable": "Οι σήραγγες πελατών απαιτούν έναν υπολογιστή-πελάτη.",
"serverTunnel": "Tunnel Διακομιστή",
"clientTunnel": "Σήραγγα Πελάτη",
"addServerTunnel": "Προσθήκη Tunnel Διακομιστή",
"addClientTunnel": "Προσθήκη Σήραγγας Πελάτη",
"noServerTunnels": "Δεν έχουν ρυθμιστεί διοχέτευση διακομιστή.",
"noClientTunnels": "Δεν έχουν ρυθμιστεί σήραγγες πελάτη σε αυτήν την επιφάνεια εργασίας.",
"manageClientTunnels": "Διαχείριση Tunnels Πελάτη",
"addTunnel": "Προσθήκη Tunnel",
"editTunnel": "Επεξεργασία Σήραγγας",
"deleteTunnel": "Διαγραφή Tunnel",
"tunnelName": "Όνομα Tunnel",
"localPort": "Τοπική Θύρα",
"remoteHost": "Απομακρυσμένος Διακομιστής",
"remotePort": "Απομακρυσμένη Θύρα",
"autoStart": "Αυτόματη Εκκίνηση",
"autoStartContainer": "Αυτόματη εκκίνηση κατά την εκκίνηση",
"autoStartContainerDesc": "Αυτόματη εκκίνηση αυτής της διοχέτευσης όταν ο διακομιστής Termix ξεκινάει.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Ξεκινά όταν ανοίγει αυτός ο υπολογιστής-πελάτης και παραμένει συνδεδεμένος.",
"clientManualStartDesc": "Χρησιμοποιήστε Έναρξη και Διακοπή από αυτή τη σειρά. Το Termix δεν θα το ανοίξει αυτόματα.",
"clientRemoteServerNote": "Η απομακρυσμένη προώθηση μπορεί να απαιτεί AllowTcpForwarding και GatewayPorts στον εξυπηρετητή τελικού σημείου SSH. Η απομακρυσμένη θύρα κλείνει όταν αποσυνδέεται αυτή η επιφάνεια εργασίας.",
"clientTunnelStarted": "Η σήραγγα πελάτη ξεκίνησε",
"clientTunnelStopped": "Η σήραγγα πελάτη σταμάτησε",
"tunnelTestSucceeded": "Επιτυχής δοκιμή σήραγγας",
"tunnelTestFailed": "Η δοκιμή σήραγγας απέτυχε",
"localSaved": "Οι σήραγγες πελατών αποθηκεύτηκαν",
"localSaveError": "Αποτυχία αποθήκευσης τοπικών tunnels πελάτη",
"invalidBindIp": "Η τοπική IP πρέπει να είναι μια έγκυρη διεύθυνση IPv4.",
"invalidLocalTargetIp": "Το τοπικό IP προορισμού πρέπει να είναι μια έγκυρη διεύθυνση IPv4.",
"invalidCurrentHostIp": "Το IP κεντρικού υπολογιστή πρέπει να είναι μια έγκυρη διεύθυνση IPv4.",
"invalidLocalPort": "Η τοπική θύρα πρέπει να είναι μεταξύ 1 και 65535.",
"invalidRemotePort": "Η απομακρυσμένη θύρα πρέπει να είναι μεταξύ 1 και 65535.",
"invalidLocalTargetPort": "Η τοπική θύρα προορισμού πρέπει να είναι μεταξύ 1 και 65535.",
"invalidEndpointPort": "Ο λιμένας τελικού σημείου πρέπει να είναι μεταξύ 1 και 65535.",
"duplicateAutoStartBind": "Μόνο μία αυτόματη εκκίνηση tunnel πελάτη μπορεί να χρησιμοποιήσει το {{bind}}.",
"manualControlError": "Αποτυχία ενημέρωσης κατάστασης διοχέτευσης.",
"saveHostBeforeManualControl": "Αποθηκεύστε αυτόν τον κεντρικό υπολογιστή πριν από την εκκίνηση ή τη διακοπή των σηράγγων του διακομιστή.",
"status": "Κατάσταση",
"active": "Ενεργό",
"inactive": "Ανενεργό",
"start": "Έναρξη",
"stop": "Διακοπή",
"test": "Δοκιμή",
"restart": "Επανεκκίνηση",
"connectionType": "Τύπος Σύνδεσης",
"type": "Τύπος Tunnel",
"typeLocal": "Τοπικό (-L)",
"typeRemote": "Απομακρυσμένο (-R)",
"typeDynamic": "Δυναμικό (-D)",
"typeServerLocalDesc": "Τρέχων υπολογιστής στο τελικό σημείο.",
"typeServerRemoteDesc": "Τελικό σημείο πίσω στον τρέχοντα υπολογιστή.",
"typeClientLocalDesc": "Τοπικός υπολογιστής στο τελικό σημείο.",
"typeClientRemoteDesc": "Τελικό σημείο πίσω στον τοπικό υπολογιστή.",
"typeClientDynamicDesc": "SOCKS στον τοπικό υπολογιστή.",
"typeDynamicDesc": "Προώθηση κίνησης SOCKS5 ΣΥΝΔΕΣΗΣ μέσω SSH",
"forwardDescriptionServerLocal": "Τρέχων κεντρικός υπολογιστής {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Τοπικό {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS στο τοπικό λιμάνι {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS μέσω {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} μέσω {{endpoint}}",
"route": "Διαδρομή:",
"lastStarted": "Τελευταία εκκίνηση",
"lastTested": "Τελευταία δοκιμή",
"lastError": "Τελευταίο σφάλμα",
"maxRetries": "Μέγιστες Επαναλήψεις",
"maxRetriesDescription": "Μέγιστος αριθμός προσπαθειών επανάληψης.",
"retryInterval": "Διάστημα Επανάληψη (δευτερόλεπτα)",
"retryIntervalDescription": "Χρόνος αναμονής μεταξύ προσπαθειών επανάληψης.",
"local": "Τοπικό",
"remote": "Απομακρυσμένο",
"dynamic": "Δυναμικό",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Παρακαλώ δώστε τα SSH διαπιστευτήρια σας για να συνδεθείτε σε αυτόν το διακομιστή.",
"sshPasswordDescription": "Εισάγετε τον κωδικό πρόσβασης για αυτή τη σύνδεση SSH.",
"sshKeyPasswordDescription": "Αν το SSH κλειδί σας είναι κρυπτογραφημένο, εισάγετε εδώ τη φράση πρόσβασης.",
"passphraseRequired": "Απαιτείται Συνθηματική φράση",
"passphraseRequiredDescription": "Το SSH κλειδί είναι κρυπτογραφημένο. Παρακαλώ εισάγετε τη φράση πρόσβασης για να το ξεκλειδώσετε.",
"step1ScanQR": "Βήμα 1: Σαρώστε τον κωδικό QR με την εφαρμογή ελέγχου ταυτότητας",
"manualEntryCode": "Χειροκίνητος Κώδικας Εισόδου",
"cannotScanQRText": "Αν δεν μπορείτε να σαρώσετε τον κωδικό QR, πληκτρολογήστε αυτόν τον κωδικό χειροκίνητα στην εφαρμογή ελέγχου ταυτότητας",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Αποτυχία ολοκλήρωσης της επαναφοράς κωδικού πρόσβασης",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Αποτυχία έναρξης σύνδεσης OIDC",
"failedUserInfo": "Αποτυχία λήψης πληροφοριών χρήστη μετά την σύνδεση OIDC",
"failedUserInfo": "Αποτυχία λήψης πληροφοριών χρήστη μετά τη σύνδεση",
"oidcAuthFailed": "Αποτυχία ταυτοποίησης OIDC",
"noTokenReceived": "Δεν ελήφθη κανένα διακριτικό από τη σύνδεση",
"invalidAuthUrl": "Μη έγκυρη έγκριση URL λήφθηκε από το σύστημα υποστήριξης",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Εμφάνιση ετικετών σε κάθε κεντρικό υπολογιστή στην πλαϊνή μπάρα. Απενεργοποιήστε για να αποκρύψετε όλες τις ετικέτες.",
"account": "Λογαριασμός",
"appearance": "Εμφάνιση",
"c2sTunnelConfigDesc": "Τοπικές σήραγγες επιφάνειας εργασίας με στόχο ρυθμισμένους SSH υπολογιστές.",
"c2sTunnelPresets": "Προεπιλογές Tunnel Πελάτη",
"c2sTunnelPresetsDesc": "Αποθήκευση της τοπικής λίστας διοχέτευσης αυτού του υπολογιστή-πελάτη γραφείου ως μια καθορισμένη προεπιλογή διακομιστή ή φόρτωση μιας προκαθορισμένης ρύθμισης σε αυτόν τον υπολογιστή-πελάτη.",
"c2sTunnelPresetsUnavailable": "Οι προεπιλογές διοχέτευσης πελάτη είναι διαθέσιμες μόνο στην επιφάνεια εργασίας προγράμματος-πελάτη.",
"c2sPresetName": "Προκαθορισμένο Όνομα",
"c2sPresetNamePlaceholder": "Προκαθορισμένο όνομα πελάτη",
"c2sPresetToLoad": "Προκαθορισμένο Για Φόρτωση",
"c2sNoPresetSelected": "Δεν επιλέχθηκε προεπιλογή",
"c2sNoPresets": "Δεν αποθηκεύτηκαν προεπιλογές",
"c2sLoadPreset": "Φόρτωση",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Οι προεπιλογές είναι σαφείς στιγμιότυπα. Η φόρτωση μιας αντικαθιστά την τοπική λίστα tunnel πελάτη της επιφάνειας εργασίας.",
"c2sPresetSaved": "Η προκαθορισμένη ρύθμιση δρομολόγησης πελάτη αποθηκεύτηκε",
"c2sPresetLoaded": "Τοπικά φορτισμένη σήραγγα πελάτη",
"c2sPresetRenamed": "Η προκαθορισμένη ρύθμιση δρομολόγησης πελάτη μετονομάστηκε",
"c2sPresetDeleted": "Η προκαθορισμένη ρύθμιση δρομολόγησης πελάτη διαγράφηκε",
"c2sPresetLoadError": "Αποτυχία φόρτωσης προεπιλογών tunnel πελάτη",
"languageLocalization": "Γλώσσα & Τοπικοποίηση",
"fileManagerSettings": "Διαχειριστής Αρχείων",
"terminalSettings": "Τερματικό",
@@ -2370,6 +2472,10 @@
"description": "Περιγραφή διαπιστευτηρίων SSH",
"searchCredentials": "Αναζήτηση διαπιστευτηρίων με βάση όνομα, όνομα χρήστη ή ετικέτες...",
"sshConfig": "ρύθμιση τελικού σημείου ssh",
"bindLocalhost": "127.0.0.1 (δέσμευση σε localhost)",
"localListenerHost": "127.0.0.1 (ακούστε τοπικά)",
"localTargetHost": "127.0.0.1 (στόχος σε αυτόν τον υπολογιστή)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/οικία",
"clientId": "το δικό σας-πελάτη",
"clientSecret": "σας-πελάτης-μυστικό",
@@ -2549,6 +2655,7 @@
"version": "Έκδοση",
"upToDate": "Έως την ημερομηνία",
"updateAvailable": "Διαθέσιμη Ενημέρωση",
"beta": "Βήτα",
"uptime": "Χρόνος",
"database": "Βάση Δεδομένων",
"healthy": "Υγιείς",
@@ -2955,6 +3062,20 @@
"error": "Η συλλογή μετρήσεων απέτυχε: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Δημιουργία σύνδεσης σήραγγας...",
"sourceConnecting": "Σύνδεση στον πηγαίο υπολογιστή {{host}}...",
"sourceConnected": "Συνδεδεμένο στον πηγαίο υπολογιστή",
"endpointResolving": "Επίλυση του κεντρικού υπολογιστή τελικού σημείου {{host}}...",
"endpointResolved": "Κεντρικός υπολογιστής τελικού σημείου επιλύθηκε",
"forwarding": "Ρύθμιση προώθησης θύρας {{localPort}} → {{remotePort}}...",
"connected": "Η σήραγγα δημιουργήθηκε επιτυχώς",
"retrying": "Η σύνδεση απέτυχε, προσπάθεια ξανά στο {{countdown}}s (προσπάθεια {{attempt}}/{{max}})...",
"retryExhausted": "Επιτεύχθηκαν μέγιστες προσπάθειες επανάληψης",
"error": "Η σύνδεση της σήραγγας απέτυχε: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Σύνδεση σε διακομιστή SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Estás ejecutando la versión {{version}}",
"updateAvailable": "Actualización disponible",
"newVersionAvailable": "¡Una nueva versión está disponible! Estás usando {{current}}, pero {{latest}} está disponible.",
"betaVersion": "Versión beta",
"betaVersionDesc": "Está ejecutando {{current}}, que es más reciente que la última versión estable {{latest}}.",
"releasedOn": "Liberado en {{date}}",
"downloadUpdate": "Descargar actualización",
"dismiss": "Descartar",
@@ -426,6 +428,8 @@
"warning": "Advertencia",
"info": "Info",
"success": "Éxito",
"unsavedChanges": "Cambios sin guardar",
"dismiss": "Descartar",
"loading": "Cargando...",
"required": "Requerido",
"optional": "Opcional",
@@ -471,6 +475,7 @@
"chinese": "Chino",
"german": "Alemán",
"cancel": "Cancelar",
"done": "Hecho",
"username": "Usuario",
"name": "Nombre",
"login": "Ingresar",
@@ -486,6 +491,7 @@
"save": "Guardar",
"saving": "Guardando...",
"delete": "Eliminar",
"rename": "Renombrar",
"edit": "Editar",
"add": "Añadir",
"search": "Buscar",
@@ -559,7 +565,8 @@
"passwordCopied": "Contraseña copiada al portapapeles",
"sudoPasswordCopied": "Contraseña de Sudo copiada al portapapeles",
"noPasswordAvailable": "Contraseña no disponible",
"failedToCopyPassword": "Error al copiar la contraseña"
"failedToCopyPassword": "Error al copiar la contraseña",
"openFileManager": "Abrir gestor de archivos"
},
"admin": {
"title": "Configuración de Admin",
@@ -868,7 +875,42 @@
"passwordMinLength": "La contraseña debe tener al menos 6 caracteres",
"currentRoles": "Roles actuales",
"noRolesAssigned": "No hay roles asignados",
"assignNewRole": "Asignar nuevo rol"
"assignNewRole": "Asignar nuevo rol",
"apiKeys": {
"tabLabel": "Claves API",
"title": "Claves API",
"createApiKey": "Crear Clave API",
"createApiKeyDescription": "Crear una nueva clave API con alcance para un usuario específico. El token se muestra una sola vez.",
"keyCreated": "Clave API creada",
"keyCreatedDescription": "Copie esta clave ahora — no se mostrará de nuevo.",
"keyName": "Nombre clave",
"keyNamePlaceholder": "ej. Pipeline CI/CD",
"scopedUser": "Usuario de ámbito",
"selectUser": "Seleccione un usuario...",
"searchUsers": "Buscar usuarios...",
"noUsersFound": "No se encontraron usuarios.",
"expiresAt": "Caduca el",
"optional": "opcional",
"expiresAtHelp": "Dejar vacío para una clave que nunca expire.",
"copyWarningTitle": "Guarda tu clave API",
"copyWarningDescription": "Esta clave sólo se mostrará una vez. Guárdelo en un lugar seguro.",
"apiKey": "Clave API",
"tokenCopied": "Token copiado al portapapeles",
"creating": "Creando...",
"nameRequired": "Se requiere nombre de clave",
"userRequired": "Por favor, seleccione un usuario",
"failedToCreate": "Error al crear la clave API",
"noKeys": "No se encontraron claves API.",
"name": "Nombre",
"prefix": "Prefijo",
"lastUsed": "Último uso",
"never": "Nunca",
"revokeKey": "Revocar clave",
"confirmRevoke": "¿Está seguro de que desea revocar la clave de API \"{{name}}\"? Esto no se puede deshacer.",
"revokedSuccessfully": "Clave API revocada con éxito",
"failedToRevoke": "Error al revocar la clave API",
"failedToFetch": "No se pudo obtener las claves API"
}
},
"hosts": {
"title": "Gestor de Anfitriones",
@@ -962,28 +1004,8 @@
"enableDocker": "Activar Docker",
"defaultPath": "Ruta por defecto",
"defaultPathDesc": "Directorio predeterminado al abrir el gestor de archivos para este host",
"tunnelConnections": "Conexiones de túnel",
"connection": "Conexión",
"remove": "Eliminar",
"sourcePort": "Puerto de origen",
"sourcePortDesc": " (El canal se refiere a los detalles de la conexión actual en la pestaña General)",
"endpointPort": "Puerto final",
"endpointSshConfig": "Configuración SSSH de punto final",
"tunnelForwardDescription": "Este túnel redirigirá el tráfico desde el puerto {{sourcePort}} en la máquina fuente (los detalles de conexión actuales en la pestaña general) al puerto {{endpointPort}} en la máquina de endpoint.",
"maxRetries": "Reintentos máximos",
"maxRetriesDescription": "Número máximo de intentos de reintento para la conexión del túnel.",
"retryInterval": "Intervalo de reintento (segundos)",
"retryIntervalDescription": "Tiempo de espera entre intentos de reintento.",
"autoStartContainer": "Inicio automático en el lanzamiento del contenedor",
"autoStartDesc": "Iniciar automáticamente este túnel cuando el contenedor se ejecuta",
"addConnection": "Añadir conexión de túnel",
"tunnelType": "Tipo de túnel",
"tunnelTypeLocal": "Local (-L)",
"tunnelTypeRemote": "Remoto (-R)",
"tunnelTypeLocalDesc": "Reenviar puerto local al extremo remoto",
"tunnelTypeRemoteDesc": "Reenviar puerto remoto a máquina local",
"tunnelForwardDescriptionLocal": "Este túnel redirigirá el tráfico desde el puerto local {{sourcePort}} al puerto {{endpointPort}} en la máquina endpoint.",
"tunnelForwardDescriptionRemote": "Este túnel redirigirá el tráfico desde el puerto {{sourcePort}} en la máquina fuente (los detalles de conexión actuales en la pestaña general) al puerto {{endpointPort}} en la máquina de endpoint.",
"sshpassRequired": "Sshpass requerido para la autenticación de contraseña",
"sshpassRequiredDesc": "Para la autenticación de contraseña en túneles, sshpass debe estar instalado en el sistema.",
"otherInstallMethods": "Otros métodos de instalación:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Dividir vertical",
"closePanel": "Cerrar panel",
"reconnect": "Volver a conectar",
"sessionEnded": "Sesión finalizada",
"sessionEnded": "Sesión finalizada.",
"connectionLost": "Conexión perdida",
"error": "ERROR: {{message}}",
"disconnected": "Desconectado",
@@ -1933,22 +1955,10 @@
"ascending": "Ascendente",
"descending": "Descendente"
},
"tunnel": {
"log": {
"connecting": "Estableciendo conexión de túnel...",
"sourceConnecting": "Conectando al host fuente {{host}}...",
"sourceConnected": "Conectado al host fuente",
"endpointResolving": "Resolviendo host de endpoint {{host}}...",
"endpointResolved": "Host de punto final resuelto",
"forwarding": "Configurando el reenvío de puertos {{localPort}} → {{remotePort}}...",
"connected": "Túnel establecido con éxito",
"retrying": "Conexión fallida, reintentando en {{countdown}}s (intento {{attempt}}/{{max}})...",
"retryExhausted": "Máximo de intentos de reintento alcanzado",
"error": "Conexión de túnel fallida: {{error}}"
}
},
"tunnels": {
"title": "Túneles SSH",
"noTunnelsConfigured": "No hay túneles configurados",
"configureTunnelsInHostSettings": "Configurar conexiones de túnel en el Administrador de Equipos para comenzar",
"noSshTunnels": "No hay túneles SSH",
"createFirstTunnelMessage": "No has creado ningún túnel SSH todavía. Configura conexiones de túnel en el Administrador de Equipos para empezar.",
"connected": "Conectado",
@@ -1969,27 +1979,100 @@
"disconnect": "Desconectar",
"cancel": "Cancelar",
"port": "Puerto",
"localPort": "Puerto local",
"remotePort": "Puerto remoto",
"currentHostPort": "Puerto actual de host",
"endpointPort": "Puerto final",
"bindIp": "IP local",
"currentHostIp": "IP actual del Host",
"endpointSshConfig": "Configuración SSSH de punto final",
"endpointSshConfigRequired": "Se requiere configuración SSH de punto final",
"endpointSshHost": "Host SSH de punto final",
"endpointSshHostPlaceholder": "Seleccione un host configurado",
"endpointSshHostRequired": "Seleccione un endpoint SSH host para cada túnel de cliente.",
"attempt": "Intento {{current}} de {{max}}",
"nextRetryIn": "Siguiente reintento en {{seconds}} segundos",
"checkDockerLogs": "Comprueba tus registros de Docker por la razón del error, únete a",
"orCreate": "o crear un ",
"noTunnelConnections": "No hay conexiones de túnel configuradas",
"tunnelConnections": "Conexiones de túnel",
"serverTunnels": "Túneles de servidor",
"serverTunnelsDesc": "Túneles administrados en backend almacenados con este host.",
"clientTunnels": "Túneles de cliente",
"clientTunnelsUnavailable": "Túneles de clientes requieren un cliente de escritorio.",
"serverTunnel": "Túnel de Servidor",
"clientTunnel": "Túnel de cliente",
"addServerTunnel": "Añadir túnel de servidor",
"addClientTunnel": "Añadir túnel de cliente",
"noServerTunnels": "No hay túneles de servidor configurados.",
"noClientTunnels": "No hay túneles de cliente configurados en este ordenador.",
"manageClientTunnels": "Administrar túneles de cliente",
"addTunnel": "Añadir túnel",
"editTunnel": "Editar túnel",
"deleteTunnel": "Eliminar túnel",
"tunnelName": "Nombre del túnel",
"localPort": "Puerto local",
"remoteHost": "Host remoto",
"remotePort": "Puerto remoto",
"autoStart": "Auto inicio",
"autoStartContainer": "Inicio automático en el lanzamiento",
"autoStartContainerDesc": "Iniciar automáticamente este túnel cuando se inicia el servidor de Termix.",
"autoStartEnableFailed": "Host guardado, pero no se pudo iniciar túneles de autoarranque para {{name}}.",
"clientAutoStartDesc": "Se inicia cuando este cliente de escritorio se abre y permanece conectado.",
"clientManualStartDesc": "Usar Inicio y Detener de esta fila. Termix no lo abrirá automáticamente.",
"clientRemoteServerNote": "El reenvío remoto puede requerir AllowTcpForwarding y GatewayPorts en el servidor SSH. El puerto remoto se cierra cuando este escritorio se desconecta.",
"clientTunnelStarted": "Túnel de cliente iniciado",
"clientTunnelStopped": "Túnel de cliente detenido",
"tunnelTestSucceeded": "Prueba de túnel exitosa",
"tunnelTestFailed": "Prueba de túnel fallida",
"localSaved": "Túneles de cliente guardados",
"localSaveError": "Error al guardar túneles de cliente locales",
"invalidBindIp": "IP local debe ser una dirección IPv4 válida.",
"invalidLocalTargetIp": "IP de destino local debe ser una dirección IPv4 válida.",
"invalidCurrentHostIp": "La IP actual del host debe ser una dirección IPv4 válida.",
"invalidLocalPort": "El puerto local debe estar entre 1 y 65535.",
"invalidRemotePort": "Puerto remoto debe estar entre 1 y 65535.",
"invalidLocalTargetPort": "El puerto local destino debe estar entre 1 y 65535.",
"invalidEndpointPort": "El puerto de punto final debe estar entre 1 y 65535.",
"duplicateAutoStartBind": "Sólo un túnel de cliente de autoarranque puede usar {{bind}}.",
"manualControlError": "Error al actualizar el estado del túnel.",
"saveHostBeforeManualControl": "Guardar este host antes de iniciar o detener sus túneles de servidor.",
"status": "Estado",
"active": "Activo",
"inactive": "Inactivo",
"start": "Comenzar",
"stop": "Parar",
"test": "Prueba",
"restart": "Reiniciar",
"connectionType": "Tipo de conexión",
"type": "Tipo de túnel",
"typeLocal": "Local (-L)",
"typeRemote": "Remoto (-R)",
"typeDynamic": "Dinámica (-D)",
"typeServerLocalDesc": "Anfitrión actual al final.",
"typeServerRemoteDesc": "Volver al punto final al host actual.",
"typeClientLocalDesc": "Ordenador local al final.",
"typeClientRemoteDesc": "Vuelve al punto final al ordenador local.",
"typeClientDynamicDesc": "SOCKS en el ordenador local.",
"typeDynamicDesc": "Reenviar tráfico CONNECT SOCKS5 a través de SSH",
"forwardDescriptionServerLocal": "Hosting actual {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → host actual {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS en el host actual {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remoto {{endpointPort}}.",
"forwardDescriptionClientRemote": "{{sourcePort}} remoto → {{endpointPort}} local .",
"forwardDescriptionClientDynamic": "SOCKS en el puerto local {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS vía {{endpoint}}",
"autoNameClientLocal": "{{localPort}} local → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → {{localPort}} local",
"autoNameClientDynamic": "SOCKS {{localPort}} vía {{endpoint}}",
"route": "Ruta:",
"lastStarted": "Último inicio",
"lastTested": "Última prueba",
"lastError": "Último error",
"maxRetries": "Reintentos máximos",
"maxRetriesDescription": "Cantidad máxima de intentos de reintento.",
"retryInterval": "Intervalo de reintento (segundos)",
"retryIntervalDescription": "Tiempo de espera entre intentos de reintento.",
"local": "Local",
"remote": "Remoto",
"dynamic": "Dinámica",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Por favor, proporcione sus credenciales SSH para conectarse a este servidor.",
"sshPasswordDescription": "Introduzca la contraseña para esta conexión SSH.",
"sshKeyPasswordDescription": "Si su clave SSH está cifrada, introduzca la contraseña aquí.",
"passphraseRequired": "Contraseña requerida",
"passphraseRequiredDescription": "La clave SSH está cifrada. Por favor, introduzca la contraseña para desbloquearla.",
"step1ScanQR": "Paso 1: Escanea el código QR con tu aplicación de autenticación",
"manualEntryCode": "Código de entrada manual",
"cannotScanQRText": "Si no puedes escanear el código QR, introduce este código manualmente en tu aplicación de autenticación",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Error al restablecer la contraseña",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Error al iniciar inicio de sesión OIDC",
"failedUserInfo": "Error al obtener la información del usuario después del inicio de sesión OIDC",
"failedUserInfo": "Error al obtener la información del usuario después de iniciar sesión",
"oidcAuthFailed": "Autenticación OIDC fallida",
"noTokenReceived": "No se ha recibido ningún token de inicio de sesión",
"invalidAuthUrl": "URL de autorización no válida recibida del backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Mostrar etiquetas bajo cada host en la barra lateral. Desactivar para ocultar todas las etiquetas.",
"account": "Cuenta",
"appearance": "Apariencia",
"c2sTunnelConfigDesc": "Túneles de escritorio locales dirigidos a hosts SSH configurados.",
"c2sTunnelPresets": "Ajustes de túnel de cliente",
"c2sTunnelPresetsDesc": "Guarde la lista de túneles locales de este cliente de escritorio como un preset de servidor nombrado, o cargue un preset de nuevo en este cliente.",
"c2sTunnelPresetsUnavailable": "Los ajustes predeterminados del túnel de cliente sólo están disponibles en el cliente de escritorio.",
"c2sPresetName": "Nombre predefinido",
"c2sPresetNamePlaceholder": "Nombre predefinido del cliente",
"c2sPresetToLoad": "Preseleccionar para cargar",
"c2sNoPresetSelected": "Ningún preset seleccionado",
"c2sNoPresets": "No hay preajustes guardados",
"c2sLoadPreset": "Cargar",
"c2sCurrentLocalConfig": "{{count}} túnel(s) de cliente local configurado en este escritorio.",
"c2sPresetSyncNote": "Los ajustes predeterminados son instantáneas explícitas; la carga de uno reemplaza la lista de túneles de clientes locales de este cliente de escritorio.",
"c2sPresetSaved": "Preajuste de túnel de cliente guardado",
"c2sPresetLoaded": "Túnel de cliente preestablecido cargado localmente",
"c2sPresetRenamed": "Preajuste de túnel de cliente renombrado",
"c2sPresetDeleted": "Preajuste de túnel de cliente eliminado",
"c2sPresetLoadError": "Error al cargar los ajustes predeterminados del túnel del cliente",
"languageLocalization": "Idioma y localización",
"fileManagerSettings": "Gestor de archivos",
"terminalSettings": "Terminal",
@@ -2370,6 +2472,10 @@
"description": "Descripción de credencial SSH",
"searchCredentials": "Buscar credenciales por nombre, nombre de usuario o etiquetas...",
"sshConfig": "configuración de endpoint ssh",
"bindLocalhost": "127.0.0.1 (enlazar a localhost)",
"localListenerHost": "127.0.0.1 (escuchar localmente)",
"localTargetHost": "127.0.0.1 (objetivo en este ordenador)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/home",
"clientId": "su-cliente-id",
"clientSecret": "su-cliente-secreto-",
@@ -2549,6 +2655,7 @@
"version": "Versión",
"upToDate": "Hasta la fecha",
"updateAvailable": "Actualización disponible",
"beta": "Beta",
"uptime": "Actualización",
"database": "Base de datos",
"healthy": "Saludable",
@@ -2955,6 +3062,20 @@
"error": "Error en la recolección de métricas: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Estableciendo conexión de túnel...",
"sourceConnecting": "Conectando al host fuente {{host}}...",
"sourceConnected": "Conectado al host fuente",
"endpointResolving": "Resolviendo host de endpoint {{host}}...",
"endpointResolved": "Host de punto final resuelto",
"forwarding": "Configurando el reenvío de puertos {{localPort}} → {{remotePort}}...",
"connected": "Túnel establecido con éxito",
"retrying": "Conexión fallida, reintentando en {{countdown}}s (intento {{attempt}}/{{max}})...",
"retryExhausted": "Máximo de intentos de reintento alcanzado",
"error": "Conexión de túnel fallida: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Conectando al servidor SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Käytössäsi on versio {{version}}",
"updateAvailable": "Päivitys Saatavilla",
"newVersionAvailable": "Uusi versio on saatavilla! Käytät {{current}}, mutta {{latest}} on saatavilla.",
"betaVersion": "Beta Versio",
"betaVersionDesc": "Käytössäsi on {{current}}, joka on uudempi kuin uusin vakaa julkaisu {{latest}}.",
"releasedOn": "Julkaistu {{date}}",
"downloadUpdate": "Lataa Päivitys",
"dismiss": "Hylkää",
@@ -426,6 +428,8 @@
"warning": "Varoitus",
"info": "Tiedot",
"success": "Onnistui",
"unsavedChanges": "Tallentamattomat muutokset",
"dismiss": "Hylkää",
"loading": "Ladataan...",
"required": "Pakollinen",
"optional": "Valinnainen",
@@ -471,6 +475,7 @@
"chinese": "Kiina",
"german": "Saksa",
"cancel": "Peruuta",
"done": "Valmis",
"username": "Käyttäjätunnus",
"name": "Nimi",
"login": "Kirjaudu",
@@ -486,6 +491,7 @@
"save": "Tallenna",
"saving": "Tallennetaan...",
"delete": "Poista",
"rename": "Uudelleennimeä",
"edit": "Muokkaa",
"add": "Lisää",
"search": "Etsi",
@@ -559,7 +565,8 @@
"passwordCopied": "Salasana kopioitu leikepöydälle",
"sudoPasswordCopied": "Sudo salasana kopioitu leikepöydälle",
"noPasswordAvailable": "Salasanaa ei ole saatavilla",
"failedToCopyPassword": "Salasanan kopiointi epäonnistui"
"failedToCopyPassword": "Salasanan kopiointi epäonnistui",
"openFileManager": "Avaa Tiedostonhallinta"
},
"admin": {
"title": "Ylläpitäjän Asetukset",
@@ -868,7 +875,42 @@
"passwordMinLength": "Salasanan on oltava vähintään 6 merkkiä",
"currentRoles": "Nykyiset Roolit",
"noRolesAssigned": "Rooleja ei määritetty",
"assignNewRole": "Määritä Uusi Rooli"
"assignNewRole": "Määritä Uusi Rooli",
"apiKeys": {
"tabLabel": "Api Avaimet",
"title": "Api Avaimet",
"createApiKey": "Luo API-avain",
"createApiKeyDescription": "Luo uusi API-avain, joka on määritelty tietylle käyttäjälle. Tunnus näytetään vain kerran.",
"keyCreated": "Api Avain Luotu",
"keyCreatedDescription": "Kopioi tämä avain nyt — sitä ei näytetä uudelleen.",
"keyName": "Avaimen Nimi",
"keyNamePlaceholder": "esim. CI/CD Pipeline",
"scopedUser": "Poistettu Käyttäjä",
"selectUser": "Valitse käyttäjä...",
"searchUsers": "Etsi käyttäjiä...",
"noUsersFound": "Käyttäjiä ei löytynyt.",
"expiresAt": "Vanhenee",
"optional": "valinnainen",
"expiresAtHelp": "Jätä tyhjäksi avaimelle, joka ei ikinä vanhene.",
"copyWarningTitle": "Tallenna API-avaimesi",
"copyWarningDescription": "Tämä avain näytetään vain kerran. Säilytä se turvallisessa paikassa.",
"apiKey": "Api Avain",
"tokenCopied": "Token kopioitu leikepöydälle",
"creating": "Luodaan...",
"nameRequired": "Avaimen nimi vaaditaan",
"userRequired": "Ole hyvä ja valitse käyttäjä",
"failedToCreate": "API-avaimen luonti epäonnistui",
"noKeys": "API-avaimia ei löytynyt.",
"name": "Nimi",
"prefix": "Etuliite",
"lastUsed": "Viimeksi Käytetty",
"never": "Ei Koskaan",
"revokeKey": "Peruuta avain",
"confirmRevoke": "Oletko varma, että haluat peruuttaa API-avaimen \"{{name}}\"? Tätä ei voi perua.",
"revokedSuccessfully": "API-avain kumottu onnistuneesti",
"failedToRevoke": "API-avaimen peruuttaminen epäonnistui",
"failedToFetch": "API-avainten noutaminen epäonnistui"
}
},
"hosts": {
"title": "Isäntälaitteen Hallinta",
@@ -962,28 +1004,8 @@
"enableDocker": "Ota Docker Käyttöön",
"defaultPath": "Oletus Polku",
"defaultPathDesc": "Oletushakemisto, kun avataan tiedostonhallintaohjelmaa tälle palvelimelle",
"tunnelConnections": "Tunneliyhteydet",
"connection": "Yhteys",
"remove": "Poista",
"sourcePort": "Lähde Portti",
"sourcePortDesc": " (Lähdöllä tarkoitetaan yleisen välilehden nykyisiä yhteystietoja.)",
"endpointPort": "Päätepisteen Portti",
"endpointSshConfig": "Päätepisteen Ssh Asetukset",
"tunnelForwardDescription": "Tämä tunneli siirtää liikennettä satamasta {{sourcePort}} lähdekoneella (nykyisen yhteyden tiedot yleensä välilehdessä) porttiin {{endpointPort}} päätepistekoneella.",
"maxRetries": "Maksimi Uudelleen",
"maxRetriesDescription": "Enimmäismäärä uudelleen kokeilevia yrityksiä tunneliyhteyttä varten.",
"retryInterval": "Uudelleen Aikaväli (Sekuntia)",
"retryIntervalDescription": "Aika odottaa yritysten uudelleenkokeilun välillä.",
"autoStartContainer": "Käynnistä automaattisesti kontin käynnistyksessä",
"autoStartDesc": "Käynnistä tämä tunneli automaattisesti kontin käynnistyessä",
"addConnection": "Lisää Tunneliyhteys",
"tunnelType": "Tunnelin Tyyppi",
"tunnelTypeLocal": "Paikallinen (-L)",
"tunnelTypeRemote": "Etäkäyttö (-R)",
"tunnelTypeLocalDesc": "Välitä paikallinen portti etäpäätepisteeseen",
"tunnelTypeRemoteDesc": "Välitä etäportti paikalliseen koneeseen",
"tunnelForwardDescriptionLocal": "Tämä tunneli siirtää liikennettä paikallisesta portista {{sourcePort}} porttiin {{endpointPort}} päätepistekoneella.",
"tunnelForwardDescriptionRemote": "Tämä tunneli siirtää liikennettä satamasta {{sourcePort}} lähdekoneella (nykyisen yhteyden tiedot yleensä välilehdessä) porttiin {{endpointPort}} päätepistekoneella.",
"sshpassRequired": "Sshpassi Vaaditaan Salasanan Todentamiseksi",
"sshpassRequiredDesc": "Jotta salasanan todennus tunneleissa, sshpass on asennettava järjestelmään.",
"otherInstallMethods": "Muut asennusmenetelmät:",
@@ -1529,7 +1551,7 @@
"splitVertical": "Jaa pystysuunnassa",
"closePanel": "Sulje paneeli",
"reconnect": "Yhdistä uudelleen",
"sessionEnded": "Istunto päättyi",
"sessionEnded": "Istunto päättyi.",
"connectionLost": "Yhteys katkesi",
"error": "VIRHE: {{message}}",
"disconnected": "Yhteys Katkaistu",
@@ -1933,22 +1955,10 @@
"ascending": "Nouseva",
"descending": "Laskeva"
},
"tunnel": {
"log": {
"connecting": "Luodaan tunneliyhteyttä...",
"sourceConnecting": "Yhdistetään lähteen isäntään {{host}}...",
"sourceConnected": "Yhdistetty lähteen palvelimeen",
"endpointResolving": "Selvitetään päätepisteen isäntää {{host}}...",
"endpointResolved": "Päätepisteen isäntä ratkaistu",
"forwarding": "Portinsiirron määrittäminen {{localPort}} → {{remotePort}}...",
"connected": "Tunneli muodostettu onnistuneesti",
"retrying": "Yhteys epäonnistui, yritetään uudelleen {{countdown}}s kuluttua (yritys {{attempt}}/{{max}})...",
"retryExhausted": "Enimmäismäärä uudelleenyrityksiä saavutettu",
"error": "Tunneliyhteys epäonnistui: {{error}}"
}
},
"tunnels": {
"title": "SSH-tunnelit",
"noTunnelsConfigured": "Tunneleita Ei Määritetty",
"configureTunnelsInHostSettings": "Määritä tunneliyhteydet isäntäpäällikössä aloittaaksesi sen",
"noSshTunnels": "Ei SSH-tunneleita",
"createFirstTunnelMessage": "Et ole vielä luonut SSH-tunneleita. Aloita määrittämällä tunneliyhteydet Host Managerissa.",
"connected": "Yhdistetty",
@@ -1969,27 +1979,100 @@
"disconnect": "Katkaise yhteys",
"cancel": "Peruuta",
"port": "Portti",
"localPort": "Paikallinen satama",
"remotePort": "Etäportti",
"currentHostPort": "Nykyinen Isäntäportti",
"endpointPort": "Päätepisteen Portti",
"bindIp": "Paikallinen IP-osoite",
"currentHostIp": "Nykyinen Isäntä Ip",
"endpointSshConfig": "Päätepisteen Ssh Asetukset",
"endpointSshConfigRequired": "Päätepisteen SSH asetukset vaaditaan",
"endpointSshHost": "Päätepiste SSH Isäntä",
"endpointSshHostPlaceholder": "Valitse määritetty isäntä",
"endpointSshHostRequired": "Valitse päätepiste SSH isäntä jokaiselle asiakkaan tunneli.",
"attempt": "Yritys {{current}} / {{max}}",
"nextRetryIn": "Seuraava retry {{seconds}} sekunnissa",
"checkDockerLogs": "Tarkista virheen syy Docker-lokeistasi ja liity",
"orCreate": "tai luo ",
"noTunnelConnections": "Tunneliyhteyksiä ei ole määritetty",
"tunnelConnections": "Tunneliyhteydet",
"serverTunnels": "Palvelimen Tunnelit",
"serverTunnelsDesc": "Backend-hallinnoidut tunnelit tallennettu tällä isäntä.",
"clientTunnels": "Asiakkaan Tunnelit",
"clientTunnelsUnavailable": "Asiakastunnelit vaativat työpöytäsovelluksen.",
"serverTunnel": "Palvelimen Tunneli",
"clientTunnel": "Asiakkaan Tunneli",
"addServerTunnel": "Lisää Palvelimen Tunneli",
"addClientTunnel": "Lisää Asiakastunneli",
"noServerTunnels": "Palvelimen tunneleita ei ole määritetty.",
"noClientTunnels": "Tällä työpöydällä ei ole määritettyjä asiakastunneleita.",
"manageClientTunnels": "Hallitse Asiakastunneleita",
"addTunnel": "Lisää tunneli",
"editTunnel": "Muokkaa tunnelia",
"deleteTunnel": "Poista tunneli",
"tunnelName": "Tunnelin nimi",
"localPort": "Paikallinen satama",
"remoteHost": "Etäisäntä",
"remotePort": "Etäportti",
"autoStart": "Automaattinen käynnistys",
"autoStartContainer": "Aloita automaattisesti käynnistyksessä",
"autoStartContainerDesc": "Käynnistä tämä tunneli automaattisesti, kun Termix-palvelin käynnistää.",
"autoStartEnableFailed": "Isäntä tallennettu, mutta ei käynnistänyt automaattista käynnistystä tunneleille {{name}}.",
"clientAutoStartDesc": "Aloittaa kun työpöytäohjelma avautuu ja pysyy kytkettynä.",
"clientManualStartDesc": "Käytä Käynnistä ja lopeta tämä rivi. Termix ei avaa sitä automaattisesti.",
"clientRemoteServerNote": "Etäsiirto saattaa vaatia AllowTcpForwarding ja GatewayPorts päätepisteessä SSH-palvelimella. Etäsiirros sulkeutuu kun työpöytä irrotetaan.",
"clientTunnelStarted": "Asiakkaan tunneli aloitettu",
"clientTunnelStopped": "Asiakkaan tunneli pysäytetty",
"tunnelTestSucceeded": "Tunnelin testi onnistui",
"tunnelTestFailed": "Tunnelitesti epäonnistui",
"localSaved": "Asiakastunnelit tallennettu",
"localSaveError": "Paikallisten asiakastunneleiden tallennus epäonnistui",
"invalidBindIp": "Paikallisen IP-osoitteen on oltava kelvollinen IPv4-osoite.",
"invalidLocalTargetIp": "Paikallisen kohteen IP-osoitteen on oltava kelvollinen IPv4-osoite.",
"invalidCurrentHostIp": "Nykyisen isäntäosoitteen on oltava kelvollinen IPv4-osoite.",
"invalidLocalPort": "Paikallinen satama on oltava välillä 1 ja 65535.",
"invalidRemotePort": "Etäportin on oltava välillä 1 ja 65535.",
"invalidLocalTargetPort": "Paikallisen tavoiteportin on oltava välillä 165535.",
"invalidEndpointPort": "Päätesataman on oltava välillä 165535.",
"duplicateAutoStartBind": "Vain yksi automaattinen käynnistys -asiakastunneli voi käyttää {{bind}} -tunnusta.",
"manualControlError": "Tunnelin tilan päivitys epäonnistui.",
"saveHostBeforeManualControl": "Tallenna tämä isäntä ennen kuin aloitat tai pysäytät sen serveritunnelit.",
"status": "Status",
"active": "Aktiivinen",
"inactive": "Passiivinen",
"start": "Aloita",
"stop": "Stop",
"test": "Testi",
"restart": "Käynnistä uudelleen",
"connectionType": "Yhteystyyppi",
"type": "Tunnelin Tyyppi",
"typeLocal": "Paikallinen (-L)",
"typeRemote": "Etäkäyttö (-R)",
"typeDynamic": "Dynaaminen (-D)",
"typeServerLocalDesc": "Nykyinen isäntä päätepisteeseen.",
"typeServerRemoteDesc": "Päätä piste takaisin nykyiseen isäntään.",
"typeClientLocalDesc": "Paikallinen tietokone tutkittavaksi.",
"typeClientRemoteDesc": "Endpoint takaisin paikalliseen tietokoneeseen.",
"typeClientDynamicDesc": "SOCKS paikallisella tietokoneella.",
"typeDynamicDesc": "Siirrä SOCKS5 CONNECT liikennettä SSH:n kautta",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS paikallisessa portissa {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Reitti:",
"lastStarted": "Viimeksi aloitettu",
"lastTested": "Viimeksi testattu",
"lastError": "Viimeisin virhe",
"maxRetries": "Maksimi Uudelleen",
"maxRetriesDescription": "Enimmäismäärä uudelleen kokeilevia yrityksiä.",
"retryInterval": "Uudelleen Aikaväli (Sekuntia)",
"retryIntervalDescription": "Aika odottaa yritysten uudelleenkokeilun välillä.",
"local": "Paikallinen",
"remote": "Kauko",
"dynamic": "Dynaaminen",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Anna SSH-tunnuksesi muodostaaksesi yhteyden tähän palvelimeen.",
"sshPasswordDescription": "Anna tämän SSH-yhteyden salasana.",
"sshKeyPasswordDescription": "Jos SSH-avaimesi on salattu, anna salasana tähän.",
"passphraseRequired": "Tunnuslause Vaaditaan",
"passphraseRequiredDescription": "SSH avain on salattu. Ole hyvä ja kirjoita salasana avataksesi sen.",
"step1ScanQR": "Vaihe 1: Skannaa QR-koodi todennussovelluksellasi",
"manualEntryCode": "Manuaalisen syöttökoodin",
"cannotScanQRText": "Jos et pysty skannaamaan QR-koodia, syötä tämä koodi manuaalisesti todennussovellukseesi.",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Salasanan nollaus epäonnistui",
"invalidTotpCode": "Virheellinen TOTP-koodi",
"failedOidcLogin": "OIDC-kirjautumisen aloittaminen epäonnistui",
"failedUserInfo": "Käyttäjätietojen hakeminen epäonnistui OIDC-kirjautumisen jälkeen",
"failedUserInfo": "Käyttäjätietojen saaminen epäonnistui kirjautumisen jälkeen",
"oidcAuthFailed": "OIDC-todennus epäonnistui",
"noTokenReceived": "Kirjautumisesta ei vastaanotettu tunnusta",
"invalidAuthUrl": "Virheellinen valtuutus-URL-osoite vastaanotettu taustajärjestelmästä",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Näytä tunnisteet jokaisen isännän alla sivupalkissa. Poista tämä käytöstä, jos haluat piilottaa kaikki tunnisteet.",
"account": "Tili",
"appearance": "Ulkonäkö",
"c2sTunnelConfigDesc": "Paikalliset työpöydän tunnelit kohdennettu määritetty SSH isännät.",
"c2sTunnelPresets": "Asiakkaan Tunnelin Esiasetukset",
"c2sTunnelPresetsDesc": "Tallenna tämä työpöytäasiakkaan paikallinen tunneliluettelo nimenä palvelimen esiasetuksena tai lataa esiasetus takaisin tähän ohjelmaan.",
"c2sTunnelPresetsUnavailable": "Asiakkaan tunnelin esiasetukset ovat käytettävissä vain työpöytäsovelluksessa.",
"c2sPresetName": "Esiasetuksen Nimi",
"c2sPresetNamePlaceholder": "Asiakkaan esiasetus nimi",
"c2sPresetToLoad": "Esiasetus Ladattavaksi",
"c2sNoPresetSelected": "Ei esiasetusta valittuna",
"c2sNoPresets": "Ei tallennettuja esiasetuksia",
"c2sLoadPreset": "Lataa",
"c2sCurrentLocalConfig": "{{count}} paikallista asiakastunnelia määritetty tällä työpöydällä.",
"c2sPresetSyncNote": "Esiasetukset ovat nimenomaisia kuvakaappauksia; lataus korvaa tämän työpöytäasiakkaan paikallisen tunneliluettelon.",
"c2sPresetSaved": "Asiakkaan tunnelin esiasetus tallennettu",
"c2sPresetLoaded": "Asiakastunnelin esiasetus ladattu paikallisesti",
"c2sPresetRenamed": "Asiakkaan tunnelin esiasetus uudelleen nimetty",
"c2sPresetDeleted": "Asiakkaan tunnelin esiasetus poistettu",
"c2sPresetLoadError": "Asiakastunnelin esiasetusten lataaminen epäonnistui",
"languageLocalization": "Kieli ja lokalisointi",
"fileManagerSettings": "Tiedostojen Hallinta",
"terminalSettings": "Pääte",
@@ -2370,6 +2472,10 @@
"description": "SSH-tunnistetietojen kuvaus",
"searchCredentials": "Hae tunnistetietoja nimen, käyttäjätunnuksen tai tunnisteiden perusteella...",
"sshConfig": "päätepisteen ssh-konfiguraatio",
"bindLocalhost": "127.0.0.1 (sitoudu paikalliseen)",
"localListenerHost": "127.0.0.1 (kuuntele paikallisesti)",
"localTargetHost": "127.0.0.1 (tämän tietokoneen tavoite)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/kotiin",
"clientId": "asiakastunnuksesi",
"clientSecret": "asiakkaasi-salaisuus",
@@ -2549,6 +2655,7 @@
"version": "Versio",
"upToDate": "Ajan tasalla",
"updateAvailable": "Päivitys Saatavilla",
"beta": "Beeta",
"uptime": "Käyttöaika",
"database": "Tietokanta",
"healthy": "Terveellinen",
@@ -2955,6 +3062,20 @@
"error": "Metriikan kokoelma epäonnistui: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Luodaan tunneliyhteyttä...",
"sourceConnecting": "Yhdistetään lähteen isäntään {{host}}...",
"sourceConnected": "Yhdistetty lähteen palvelimeen",
"endpointResolving": "Selvitetään päätepisteen isäntää {{host}}...",
"endpointResolved": "Päätepisteen isäntä ratkaistu",
"forwarding": "Portinsiirron määrittäminen {{localPort}} → {{remotePort}}...",
"connected": "Tunneli muodostettu onnistuneesti",
"retrying": "Yhteys epäonnistui, yritetään uudelleen {{countdown}}s kuluttua (yritys {{attempt}}/{{max}})...",
"retryExhausted": "Enimmäismäärä uudelleenyrityksiä saavutettu",
"error": "Tunneliyhteys epäonnistui: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Yhdistetään SFTP-palvelimeen...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "Vous utilisez la version {{version}}",
"updateAvailable": "Mise à jour disponible",
"newVersionAvailable": "Une nouvelle version est disponible ! Vous utilisez {{current}}, mais {{latest}} est disponible.",
"betaVersion": "Version bêta",
"betaVersionDesc": "Vous utilisez {{current}}, qui est plus récent que la dernière version stable {{latest}}.",
"releasedOn": "Publié sur {{date}}",
"downloadUpdate": "Télécharger la mise à jour",
"dismiss": "Refuser",
@@ -426,6 +428,8 @@
"warning": "Avertissement",
"info": "Infos",
"success": "Succès",
"unsavedChanges": "Modifications non enregistrées",
"dismiss": "Refuser",
"loading": "Chargement en cours...",
"required": "Requis",
"optional": "Optionnel",
@@ -471,6 +475,7 @@
"chinese": "Chinois",
"german": "Allemand",
"cancel": "Abandonner",
"done": "Fait",
"username": "Nom d'utilisateur",
"name": "Nom",
"login": "Se connecter",
@@ -486,6 +491,7 @@
"save": "Enregistrer",
"saving": "Sauvegarde en cours...",
"delete": "Supprimez",
"rename": "Renommer",
"edit": "Editer",
"add": "Ajouter",
"search": "Chercher",
@@ -559,7 +565,8 @@
"passwordCopied": "Mot de passe copié dans le presse-papiers",
"sudoPasswordCopied": "Mot de passe Sudo copié dans le presse-papiers",
"noPasswordAvailable": "Aucun mot de passe disponible",
"failedToCopyPassword": "Impossible de copier le mot de passe"
"failedToCopyPassword": "Impossible de copier le mot de passe",
"openFileManager": "Ouvrir le gestionnaire de fichiers"
},
"admin": {
"title": "Paramètres de l'administrateur",
@@ -868,7 +875,42 @@
"passwordMinLength": "Le mot de passe doit comporter au moins 6 caractères",
"currentRoles": "Rôles actuels",
"noRolesAssigned": "Aucun rôle assigné",
"assignNewRole": "Assigner un nouveau rôle"
"assignNewRole": "Assigner un nouveau rôle",
"apiKeys": {
"tabLabel": "Clés API",
"title": "Clés API",
"createApiKey": "Créer une clé API",
"createApiKeyDescription": "Créer une nouvelle clé API portée à un utilisateur spécifique. Le jeton n'est affiché qu'une seule fois.",
"keyCreated": "Clé API créée",
"keyCreatedDescription": "Copiez cette clé maintenant — elle ne sera plus affichée.",
"keyName": "Nom de la clé",
"keyNamePlaceholder": "par exemple Pipeline CI/CD",
"scopedUser": "Utilisateur Scoped",
"selectUser": "Sélectionnez un utilisateur...",
"searchUsers": "Rechercher des utilisateurs...",
"noUsersFound": "Aucun utilisateur trouvé.",
"expiresAt": "Expire à",
"optional": "optionnel",
"expiresAtHelp": "Laisser vide pour une clé qui n'expire jamais.",
"copyWarningTitle": "Enregistrez votre clé API",
"copyWarningDescription": "Cette clé ne sera affichée qu'une seule fois. Stockez-la dans un endroit sûr.",
"apiKey": "Clé API",
"tokenCopied": "Jeton copié dans le presse-papiers",
"creating": "Création en cours...",
"nameRequired": "Le nom de la clé est requis",
"userRequired": "Veuillez sélectionner un utilisateur",
"failedToCreate": "Impossible de créer la clé API",
"noKeys": "Aucune clé API trouvée.",
"name": "Nom",
"prefix": "Préfixe",
"lastUsed": "Dernière utilisation",
"never": "Jamais",
"revokeKey": "Révoquer la clé",
"confirmRevoke": "Êtes-vous sûr de vouloir révoquer la clé API \"{{name}}\" ? Cette action est irréversible.",
"revokedSuccessfully": "Clé API révoquée avec succès",
"failedToRevoke": "Impossible de révoquer la clé API",
"failedToFetch": "Impossible de récupérer les clés API"
}
},
"hosts": {
"title": "Responsable d'hôte",
@@ -962,28 +1004,8 @@
"enableDocker": "Activer Docker",
"defaultPath": "Chemin par défaut",
"defaultPathDesc": "Répertoire par défaut lors de l'ouverture du gestionnaire de fichiers pour cet hôte",
"tunnelConnections": "Connexions Tunnel",
"connection": "Raccordement",
"remove": "Retirer",
"sourcePort": "Port source",
"sourcePortDesc": " (Source se réfère aux détails de connexion en cours dans l'onglet Général)",
"endpointPort": "Port de terminaison",
"endpointSshConfig": "Configuration SSH du point d'extrémité",
"tunnelForwardDescription": "Ce tunnel acheminera le trafic depuis le port {{sourcePort}} sur la machine source (détails de connexion actuels dans l'onglet général) vers le port {{endpointPort}} sur la machine d'extrémité.",
"maxRetries": "Nombre maximum de tentatives",
"maxRetriesDescription": "Nombre maximum de tentatives de nouvelle tentative de connexion au tunnel.",
"retryInterval": "Intervalle de réessai (secondes)",
"retryIntervalDescription": "Temps d'attente entre les deux tentatives.",
"autoStartContainer": "Démarrage automatique au lancement du conteneur",
"autoStartDesc": "Démarrer automatiquement ce tunnel lorsque le conteneur démarre",
"addConnection": "Ajouter une connexion Tunnel",
"tunnelType": "Type de tunnel",
"tunnelTypeLocal": "Local (-L)",
"tunnelTypeRemote": "Télécommande (-R)",
"tunnelTypeLocalDesc": "Transférer le port local vers le point de terminaison distant",
"tunnelTypeRemoteDesc": "Transférer le port distant vers la machine locale",
"tunnelForwardDescriptionLocal": "Ce tunnel redirigera le trafic du port local {{sourcePort}} vers le port {{endpointPort}} sur la machine d'extrémité.",
"tunnelForwardDescriptionRemote": "Ce tunnel acheminera le trafic depuis le port {{sourcePort}} sur la machine source (détails de connexion actuels dans l'onglet général) vers le port {{endpointPort}} sur la machine d'extrémité.",
"sshpassRequired": "Sshpass requis pour l'authentification par mot de passe",
"sshpassRequiredDesc": "Pour l'authentification par mot de passe dans les tunnels, sshpass doit être installé sur le système.",
"otherInstallMethods": "Autres méthodes d'installation :",
@@ -1529,7 +1551,7 @@
"splitVertical": "Diviser verticalement",
"closePanel": "Fermer le panneau",
"reconnect": "Reconnecter",
"sessionEnded": "Session terminée",
"sessionEnded": "Session terminée.",
"connectionLost": "Connexion perdue",
"error": "ERREUR : {{message}}",
"disconnected": "Déconnecté",
@@ -1933,22 +1955,10 @@
"ascending": "Ascendant",
"descending": "Descendant"
},
"tunnel": {
"log": {
"connecting": "Établissement de la connexion au tunnel...",
"sourceConnecting": "Connexion à l'hôte source {{host}}...",
"sourceConnected": "Connecté à l'hôte source",
"endpointResolving": "Résolution de l'hôte de terminaison {{host}}...",
"endpointResolved": "Hôte de point d'extrémité résolu",
"forwarding": "Mise en place de la redirection de port {{localPort}} → {{remotePort}}...",
"connected": "Tunnel établi avec succès",
"retrying": "La connexion a échoué, nouvelle tentative dans {{countdown}}s (tentative {{attempt}}/{{max}})...",
"retryExhausted": "Nombre maximum de tentatives de réessai atteint",
"error": "Échec de la connexion au tunnel: {{error}}"
}
},
"tunnels": {
"title": "Tunnels SSH",
"noTunnelsConfigured": "Pas de tunnels configurés",
"configureTunnelsInHostSettings": "Configurer les connexions de tunnels dans le Gestionnaire d'Hôtes pour commencer",
"noSshTunnels": "Pas de tunnels SSH",
"createFirstTunnelMessage": "Vous n'avez pas encore créé de tunnels SSH. Configurez les connexions de tunnels dans le Gestionnaire d'Hôtes pour commencer.",
"connected": "Connecté",
@@ -1969,27 +1979,100 @@
"disconnect": "Déconnecter",
"cancel": "Abandonner",
"port": "Port",
"localPort": "Port local",
"remotePort": "Port distant",
"currentHostPort": "Port actuel de l'hôte",
"endpointPort": "Port de terminaison",
"bindIp": "IP locale",
"currentHostIp": "IP de l'hôte actuel",
"endpointSshConfig": "Configuration SSH du point d'extrémité",
"endpointSshConfigRequired": "La configuration SSH du point d'extrémité est requise",
"endpointSshHost": "Hôte SSH du point d'extrémité",
"endpointSshHostPlaceholder": "Sélectionnez un hôte configuré",
"endpointSshHostRequired": "Sélectionnez un serveur SSH de point de terminaison pour chaque tunnel client.",
"attempt": "Tentative {{current}} de {{max}}",
"nextRetryIn": "Prochaine tentative dans {{seconds}} secondes",
"checkDockerLogs": "Vérifiez vos logs Docker pour la raison de l'erreur, rejoignez le",
"orCreate": "ou créer un ",
"noTunnelConnections": "Aucune connexion tunnel configurée",
"tunnelConnections": "Connexions Tunnel",
"serverTunnels": "Tunnels serveur",
"serverTunnelsDesc": "Tunels gérés par le backend stockés avec cet hôte.",
"clientTunnels": "Tunnels client",
"clientTunnelsUnavailable": "Les tunnels du client nécessitent un client de bureau.",
"serverTunnel": "Tunnel serveur",
"clientTunnel": "Tunnel client",
"addServerTunnel": "Ajouter un tunnel serveur",
"addClientTunnel": "Ajouter un tunnel client",
"noServerTunnels": "Aucun tunnel serveur configuré.",
"noClientTunnels": "Aucun tunnel client configuré sur ce bureau.",
"manageClientTunnels": "Gérer les tunnels clients",
"addTunnel": "Ajouter un tunnel",
"editTunnel": "Modifier le tunnel",
"deleteTunnel": "Supprimer le tunnel",
"tunnelName": "Nom du tunnel",
"localPort": "Port local",
"remoteHost": "Hôte distant",
"remotePort": "Port distant",
"autoStart": "Démarrage automatique",
"autoStartContainer": "Démarrage automatique au lancement",
"autoStartContainerDesc": "Démarrer automatiquement ce tunnel au démarrage de votre serveur Termix.",
"autoStartEnableFailed": "Hôte enregistré, mais impossible de démarrer les tunnels de démarrage automatique pour {{name}}.",
"clientAutoStartDesc": "Démarre lorsque ce client de bureau s'ouvre et reste connecté.",
"clientManualStartDesc": "Utiliser Démarrer et Arrêter de cette ligne. Termix ne l'ouvrira pas automatiquement.",
"clientRemoteServerNote": "La redirection à distance peut nécessiter AllowTcpForwarding et GatewayPorts sur le serveur SSH de point d'extrémité. Le port distant se ferme lorsque ce bureau se déconnecte.",
"clientTunnelStarted": "Tunnel client démarré",
"clientTunnelStopped": "Tunnel client arrêté",
"tunnelTestSucceeded": "Test du tunnel réussi",
"tunnelTestFailed": "Le test du tunnel a échoué",
"localSaved": "Tunnel client sauvegardé",
"localSaveError": "Impossible d'enregistrer les tunnels du client local",
"invalidBindIp": "L'adresse IP locale doit être une adresse IPv4 valide.",
"invalidLocalTargetIp": "L'IP locale cible doit être une adresse IPv4 valide.",
"invalidCurrentHostIp": "L'IP actuelle de l'hôte doit être une adresse IPv4 valide.",
"invalidLocalPort": "Le port local doit être compris entre 1 et 65535.",
"invalidRemotePort": "Le port distant doit être compris entre 1 et 65535.",
"invalidLocalTargetPort": "Le port local cible doit être compris entre 1 et 65535.",
"invalidEndpointPort": "Le port du point d'entrée doit être compris entre 1 et 65535.",
"duplicateAutoStartBind": "Un seul tunnel client de démarrage automatique peut utiliser {{bind}}.",
"manualControlError": "Impossible de mettre à jour l'état du tunnel.",
"saveHostBeforeManualControl": "Enregistrez cet hôte avant de démarrer ou d'arrêter ses tunnels de serveur.",
"status": "Statut",
"active": "Actif",
"inactive": "Inactif",
"start": "Début",
"stop": "Arrêter",
"test": "Tester",
"restart": "Redémarrer",
"connectionType": "Type de connexion",
"type": "Type de tunnel",
"typeLocal": "Local (-L)",
"typeRemote": "Télécommande (-R)",
"typeDynamic": "Dynamique (-D)",
"typeServerLocalDesc": "Hôte actuel vers point de terminaison.",
"typeServerRemoteDesc": "Point de fin de retour à l'hôte actuel.",
"typeClientLocalDesc": "Ordinateur local à terminal.",
"typeClientRemoteDesc": "Point de fin de retour à l'ordinateur local.",
"typeClientDynamicDesc": "SOCKS sur ordinateur local.",
"typeDynamicDesc": "Transférer le trafic SOCKS5 vers SSH",
"forwardDescriptionServerLocal": "Hôte actuel {{sourcePort}} → point de terminaison {{endpointPort}}.",
"forwardDescriptionServerRemote": "Point de terminaison {{endpointPort}} → hôte actuel {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS sur l'hôte actuel {{sourcePort}}.",
"forwardDescriptionClientLocal": "{{sourcePort}} local → {{endpointPort}} distant.",
"forwardDescriptionClientRemote": "{{sourcePort}} → {{endpointPort}} local.",
"forwardDescriptionClientDynamic": "SOCKS sur le port local {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "{{localPort}} local → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Voie :",
"lastStarted": "Dernier démarrage",
"lastTested": "Dernier test",
"lastError": "Dernière erreur",
"maxRetries": "Nombre maximum de tentatives",
"maxRetriesDescription": "Nombre maximum de tentatives de réessai.",
"retryInterval": "Intervalle de réessai (secondes)",
"retryIntervalDescription": "Temps d'attente entre les deux tentatives.",
"local": "Locale",
"remote": "Distante",
"dynamic": "Dynamique",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "Veuillez fournir vos identifiants SSH pour vous connecter à ce serveur.",
"sshPasswordDescription": "Entrez le mot de passe de cette connexion SSH.",
"sshKeyPasswordDescription": "Si votre clé SSH est chiffrée, entrez le mot de passe ici.",
"passphraseRequired": "Phrase de passe requise",
"passphraseRequiredDescription": "La clé SSH est chiffrée. Veuillez entrer le mot de passe pour la déverrouiller.",
"step1ScanQR": "Étape 1 : Scannez le code QR avec votre application d'authentification",
"manualEntryCode": "Code d'entrée manuel",
"cannotScanQRText": "Si vous ne pouvez pas scanner le code QR, saisissez ce code manuellement dans votre application d'authentification",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "Échec de la réinitialisation du mot de passe",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Impossible de démarrer la connexion OIDC",
"failedUserInfo": "Impossible d'obtenir les informations de l'utilisateur après la connexion OIDC",
"failedUserInfo": "Impossible d'obtenir les informations de l'utilisateur après la connexion",
"oidcAuthFailed": "Échec de l'authentification OIDC",
"noTokenReceived": "Aucun jeton reçu de la connexion",
"invalidAuthUrl": "URL d'autorisation invalide reçue du backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "Afficher les tags sous chaque hôte dans la barre latérale. Désactiver pour masquer tous les tags.",
"account": "Compte client",
"appearance": "Apparence",
"c2sTunnelConfigDesc": "Tunels locaux de bureau ciblant les hôtes SSH configurés.",
"c2sTunnelPresets": "Préréglages du tunnel client",
"c2sTunnelPresetsDesc": "Enregistrez la liste des tunnels locaux de ce client de bureau sous la forme d'un préréglage nommé, ou rechargez un préréglage dans ce client.",
"c2sTunnelPresetsUnavailable": "Les préréglages du tunnel client ne sont disponibles que dans le client de bureau.",
"c2sPresetName": "Nom du préréglage",
"c2sPresetNamePlaceholder": "Nom du préréglage client",
"c2sPresetToLoad": "Préréglage à charger",
"c2sNoPresetSelected": "Aucun préréglage sélectionné",
"c2sNoPresets": "Aucun préréglage enregistré",
"c2sLoadPreset": "Charger",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Les préréglages sont des instantanés explicites ; le chargement remplace la liste des tunnels clients locaux du client de bureau.",
"c2sPresetSaved": "Préréglage du tunnel client sauvegardé",
"c2sPresetLoaded": "Préréglage du tunnel client chargé localement",
"c2sPresetRenamed": "Préréglage du tunnel client renommé",
"c2sPresetDeleted": "Préréglage du tunnel client supprimé",
"c2sPresetLoadError": "Impossible de charger les préréglages du tunnel client",
"languageLocalization": "Langue et localisation",
"fileManagerSettings": "Gestionnaire de fichiers",
"terminalSettings": "Terminal",
@@ -2370,6 +2472,10 @@
"description": "Description des identifiants SSH",
"searchCredentials": "Rechercher les identifiants par nom, nom d'utilisateur ou tags...",
"sshConfig": "configuration ssh du point de terminaison",
"bindLocalhost": "127.0.0.1 (lier à localhost)",
"localListenerHost": "127.0.0.1 (écouter localement)",
"localTargetHost": "127.0.0.1 (cible sur cet ordinateur)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/home",
"clientId": "votre-client-id",
"clientSecret": "votre-client-secret",
@@ -2549,6 +2655,7 @@
"version": "Version",
"upToDate": "À jour",
"updateAvailable": "Mise à jour disponible",
"beta": "Bêta",
"uptime": "Délai de disponibilité",
"database": "Base de données",
"healthy": "Sain",
@@ -2955,6 +3062,20 @@
"error": "La collection de métriques a échoué : {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "Établissement de la connexion au tunnel...",
"sourceConnecting": "Connexion à l'hôte source {{host}}...",
"sourceConnected": "Connecté à l'hôte source",
"endpointResolving": "Résolution de l'hôte de terminaison {{host}}...",
"endpointResolved": "Hôte de point d'extrémité résolu",
"forwarding": "Mise en place de la redirection de port {{localPort}} → {{remotePort}}...",
"connected": "Tunnel établi avec succès",
"retrying": "La connexion a échoué, nouvelle tentative dans {{countdown}}s (tentative {{attempt}}/{{max}})...",
"retryExhausted": "Nombre maximum de tentatives de réessai atteint",
"error": "Échec de la connexion au tunnel: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "Connexion au serveur SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "אתה משתמש בגרסה {{version}}",
"updateAvailable": "עדכון זמין",
"newVersionAvailable": "גרסה חדשה זמינה! אתה מפעיל {{current}}, אבל {{latest}} זמין.",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "יצא לאור בתאריך {{date}}",
"downloadUpdate": "הורד עדכון",
"dismiss": "לְפַטֵר",
@@ -426,6 +428,8 @@
"warning": "אַזהָרָה",
"info": "מידע",
"success": "הַצלָחָה",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "טְעִינָה...",
"required": "דָרוּשׁ",
"optional": "אופציונלי",
@@ -471,6 +475,7 @@
"chinese": "סִינִית",
"german": "גֶרמָנִיָת",
"cancel": "לְבַטֵל",
"done": "Done",
"username": "שם משתמש",
"name": "שֵׁם",
"login": "כְּנִיסָה לַמַעֲרֶכֶת",
@@ -486,6 +491,7 @@
"save": "לְהַצִיל",
"saving": "חִסָכוֹן...",
"delete": "לִמְחוֹק",
"rename": "Rename",
"edit": "לַעֲרוֹך",
"add": "לְהוֹסִיף",
"search": "לְחַפֵּשׂ",
@@ -559,7 +565,8 @@
"passwordCopied": "הסיסמה הועתקה ללוח",
"sudoPasswordCopied": "סיסמת הסודו הועתקה ללוח",
"noPasswordAvailable": "אין סיסמה זמינה",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "הגדרות מנהל מערכת",
@@ -868,7 +875,42 @@
"passwordMinLength": "הסיסמה חייבת להיות באורך של לפחות 6 תווים",
"currentRoles": "תפקידים נוכחיים",
"noRolesAssigned": "לא הוקצו תפקידים",
"assignNewRole": "הקצאת תפקיד חדש"
"assignNewRole": "הקצאת תפקיד חדש",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "מנהל מארח",
@@ -962,28 +1004,8 @@
"enableDocker": "הפעל את Docker",
"defaultPath": "נתיב ברירת מחדל",
"defaultPathDesc": "ספריית ברירת מחדל בעת פתיחת מנהל הקבצים עבור מחשב מארח זה",
"tunnelConnections": "חיבורי מנהרה",
"connection": "קֶשֶׁר",
"remove": "לְהַסִיר",
"sourcePort": "יציאת מקור",
"sourcePortDesc": " (המקור מתייחס לפרטי החיבור הנוכחיים בלשונית כללי)",
"endpointPort": "יציאת נקודת קצה",
"endpointSshConfig": "תצורת SSH של נקודת קצה",
"tunnelForwardDescription": "מנהרה זו תעביר תעבורה מפורט {{sourcePort}} במחשב המקור (פרטי החיבור הנוכחיים בכרטיסייה הכללית) לפורט {{endpointPort}} במחשב נקודת הקצה.",
"maxRetries": "מקסימום ניסיונות חוזרים",
"maxRetriesDescription": "מספר מרבי של ניסיונות חוזרים עבור חיבור מנהרה.",
"retryInterval": "מרווח זמן לניסיון חוזר (שניות)",
"retryIntervalDescription": "זמן המתנה בין ניסיונות חוזרים.",
"autoStartContainer": "הפעלה אוטומטית בעת הפעלת מכולה",
"autoStartDesc": "הפעל אוטומטית את המנהרה הזו בעת הפעלת המכולה",
"addConnection": "הוסף חיבור מנהרה",
"tunnelType": "סוג מנהרה",
"tunnelTypeLocal": "מקומי (-L)",
"tunnelTypeRemote": "מרחוק (-R)",
"tunnelTypeLocalDesc": "העברת פורט מקומי לנקודת קצה מרוחקת",
"tunnelTypeRemoteDesc": "העברת פורט מרוחק למחשב מקומי",
"tunnelForwardDescriptionLocal": "מנהרה זו תעביר תעבורה מפורט מקומי {{sourcePort}} לפורט {{endpointPort}} במחשב נקודת הקצה.",
"tunnelForwardDescriptionRemote": "מנהרה זו תעביר תעבורה מפורט {{sourcePort}} במחשב המקור (פרטי החיבור הנוכחיים בכרטיסייה הכללית) לפורט {{endpointPort}} במחשב נקודת הקצה.",
"sshpassRequired": "נדרש Sshpass לאימות סיסמה",
"sshpassRequiredDesc": "לאימות סיסמה במנהרות, יש להתקין sshpass במערכת.",
"otherInstallMethods": "שיטות התקנה נוספות:",
@@ -1529,7 +1551,7 @@
"splitVertical": "פיצול אנכי",
"closePanel": "סגור את הפאנל",
"reconnect": "התחבר מחדש",
"sessionEnded": "הסשן הסתיים",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "שגיאה: {{message}}",
"disconnected": "מְנוּתָק",
@@ -1933,22 +1955,10 @@
"ascending": "עוֹלֶה",
"descending": "יורד"
},
"tunnel": {
"log": {
"connecting": "יצירת חיבור למנהרה...",
"sourceConnecting": "מתחבר למארח המקור {{host}}...",
"sourceConnected": "מחובר למארח המקור",
"endpointResolving": "פתרון מארח נקודת קצה {{host}}...",
"endpointResolved": "מארח נקודת הקצה נפתר",
"forwarding": "הגדרת העברת פורטים {{localPort}} → {{remotePort}}...",
"connected": "המנהרה הוקמה בהצלחה",
"retrying": "החיבור נכשל, ניסיון חוזר בעוד {{countdown}}שניות (ניסיון {{attempt}}/{{max}})...",
"retryExhausted": "הגעת למספר מקסימלי של ניסיונות ניסיון חוזר",
"error": "חיבור המנהרה נכשל: {{error}}"
}
},
"tunnels": {
"title": "מנהרות SSH",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "אין מנהרות SSH",
"createFirstTunnelMessage": "עדיין לא יצרת מנהרות SSH. הגדר חיבורי מנהרות במנהל המארח כדי להתחיל.",
"connected": "מְחוּבָּר",
@@ -1969,27 +1979,100 @@
"disconnect": "לְנַתֵק",
"cancel": "לְבַטֵל",
"port": "נָמָל",
"localPort": "נמל מקומי",
"remotePort": "יציאה מרוחקת",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "ניסיון {{current}} מתוך {{max}}",
"nextRetryIn": "ניסיון חוזר הבא בעוד {{seconds}} שניות",
"checkDockerLogs": "בדוק את יומני ה-Docker שלך כדי למצוא את סיבת השגיאה, הצטרף ל-",
"orCreate": "או ליצור ",
"noTunnelConnections": "לא הוגדרו חיבורי מנהרה",
"tunnelConnections": "חיבורי מנהרה",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "הוסף מנהרה",
"editTunnel": "עריכת מנהרה",
"deleteTunnel": "מחיקת מנהרה",
"tunnelName": "שם המנהרה",
"localPort": "נמל מקומי",
"remoteHost": "מארח מרוחק",
"remotePort": "יציאה מרוחקת",
"autoStart": "הפעלה אוטומטית",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "סטָטוּס",
"active": "פָּעִיל",
"inactive": "לֹא פָּעִיל",
"start": "הַתחָלָה",
"stop": "לְהַפְסִיק",
"test": "Test",
"restart": "הפעלה מחדש",
"connectionType": "סוג חיבור",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "מְקוֹמִי",
"remote": "מְרוּחָק",
"dynamic": "דִינָמִי",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "אנא ספק את פרטי ה-SSH שלך כדי להתחבר לשרת זה.",
"sshPasswordDescription": "הזן את הסיסמה עבור חיבור SSH זה.",
"sshKeyPasswordDescription": "אם מפתח ה-SSH שלך מוצפן, הזן את סיסמתך כאן.",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "שלב 1: סרוק את קוד ה-QR באמצעות אפליקציית האימות שלך",
"manualEntryCode": "קוד הזנה ידנית",
"cannotScanQRText": "אם אינך מצליח לסרוק את קוד ה-QR, הזן קוד זה ידנית באפליקציית האימות שלך.",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "נכשל השלמת איפוס הסיסמה",
"invalidTotpCode": "קוד TOTP לא חוקי",
"failedOidcLogin": "נכשל בהתחלת הכניסה ל-OIDC",
"failedUserInfo": "נכשלה קבלת פרטי המשתמש לאחר כניסה ל-OIDC",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "אימות OIDC נכשל",
"noTokenReceived": "לא התקבל אסימון מהכניסה",
"invalidAuthUrl": "כתובת URL לא חוקית להרשאה התקבלה מה-backend",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "הצג תגיות תחת כל מארח בסרגל הצד. השבת כדי להסתיר את כל התגיות.",
"account": "חֶשְׁבּוֹן",
"appearance": "הוֹפָעָה",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "שפה ולוקליזציה",
"fileManagerSettings": "מנהל הקבצים",
"terminalSettings": "מָסוֹף",
@@ -2370,6 +2472,10 @@
"description": "תיאור אישורי SSH",
"searchCredentials": "חפש פרטי כניסה לפי שם, שם משתמש או תגיות...",
"sshConfig": "תצורת SSH של נקודת הקצה",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/בַּיִת",
"clientId": "מזהה-הלקוח-שלך",
"clientSecret": "סוד הלקוח שלך",
@@ -2549,6 +2655,7 @@
"version": "גִרְסָה",
"upToDate": "מעודכן",
"updateAvailable": "עדכון זמין",
"beta": "Beta",
"uptime": "זמן פעולה",
"database": "מסד נתונים",
"healthy": "בָּרִיא",
@@ -2955,6 +3062,20 @@
"error": "איסוף המדדים נכשל: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "יצירת חיבור למנהרה...",
"sourceConnecting": "מתחבר למארח המקור {{host}}...",
"sourceConnected": "מחובר למארח המקור",
"endpointResolving": "פתרון מארח נקודת קצה {{host}}...",
"endpointResolved": "מארח נקודת הקצה נפתר",
"forwarding": "הגדרת העברת פורטים {{localPort}} → {{remotePort}}...",
"connected": "המנהרה הוקמה בהצלחה",
"retrying": "החיבור נכשל, ניסיון חוזר בעוד {{countdown}}שניות (ניסיון {{attempt}}/{{max}})...",
"retryExhausted": "הגעת למספר מקסימלי של ניסיונות ניסיון חוזר",
"error": "חיבור המנהרה נכשל: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "מתחבר לשרת SFTP...",
+161 -40
View File
@@ -402,6 +402,8 @@
"currentVersion": "आप संस्करण {{version}}चला रहे हैं",
"updateAvailable": "उपलब्ध अद्यतन",
"newVersionAvailable": "एक नया संस्करण उपलब्ध है! आप {{current}}चला रहे हैं, लेकिन {{latest}} भी उपलब्ध है।",
"betaVersion": "Beta Version",
"betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.",
"releasedOn": "{{date}}को जारी किया गया",
"downloadUpdate": "अपडेट डाउनलोड करें",
"dismiss": "नकार देना",
@@ -426,6 +428,8 @@
"warning": "चेतावनी",
"info": "जानकारी",
"success": "सफलता",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"loading": "लोड हो रहा है...",
"required": "आवश्यक",
"optional": "वैकल्पिक",
@@ -471,6 +475,7 @@
"chinese": "चीनी",
"german": "जर्मन",
"cancel": "रद्द करना",
"done": "Done",
"username": "उपयोगकर्ता नाम",
"name": "नाम",
"login": "लॉग इन करें",
@@ -486,6 +491,7 @@
"save": "बचाना",
"saving": "सहेजा जा रहा है...",
"delete": "मिटाना",
"rename": "Rename",
"edit": "संपादन करना",
"add": "जोड़ना",
"search": "खोज",
@@ -559,7 +565,8 @@
"passwordCopied": "पासवर्ड क्लिपबोर्ड पर कॉपी हो गया है",
"sudoPasswordCopied": "सूडो पासवर्ड क्लिपबोर्ड पर कॉपी हो गया है",
"noPasswordAvailable": "कोई पासवर्ड उपलब्ध नहीं है",
"failedToCopyPassword": "Failed to copy password"
"failedToCopyPassword": "Failed to copy password",
"openFileManager": "Open File Manager"
},
"admin": {
"title": "व्यवस्थापक सेटिंग्स",
@@ -868,7 +875,42 @@
"passwordMinLength": "पासवर्ड कम से कम 6 अंकों का होना चाहिए",
"currentRoles": "वर्तमान भूमिकाएँ",
"noRolesAssigned": "कोई भूमिका आवंटित नहीं की गई",
"assignNewRole": "नई भूमिका सौंपें"
"assignNewRole": "नई भूमिका सौंपें",
"apiKeys": {
"tabLabel": "API Keys",
"title": "API Keys",
"createApiKey": "Create API Key",
"createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.",
"keyCreated": "API Key Created",
"keyCreatedDescription": "Copy this key now — it will not be shown again.",
"keyName": "Key Name",
"keyNamePlaceholder": "e.g. CI/CD Pipeline",
"scopedUser": "Scoped User",
"selectUser": "Select a user...",
"searchUsers": "Search users...",
"noUsersFound": "No users found.",
"expiresAt": "Expires At",
"optional": "optional",
"expiresAtHelp": "Leave empty for a key that never expires.",
"copyWarningTitle": "Save your API key",
"copyWarningDescription": "This key will only be shown once. Store it in a safe place.",
"apiKey": "API Key",
"tokenCopied": "Token copied to clipboard",
"creating": "Creating...",
"nameRequired": "Key name is required",
"userRequired": "Please select a user",
"failedToCreate": "Failed to create API key",
"noKeys": "No API keys found.",
"name": "Name",
"prefix": "Prefix",
"lastUsed": "Last Used",
"never": "Never",
"revokeKey": "Revoke key",
"confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.",
"revokedSuccessfully": "API key revoked successfully",
"failedToRevoke": "Failed to revoke API key",
"failedToFetch": "Failed to fetch API keys"
}
},
"hosts": {
"title": "मेजबान प्रबंधक",
@@ -962,28 +1004,8 @@
"enableDocker": "डॉकर को सक्षम करें",
"defaultPath": "डिफ़ॉल्ट पथ",
"defaultPathDesc": "इस होस्ट के लिए फ़ाइल मैनेजर खोलते समय डिफ़ॉल्ट डायरेक्टरी",
"tunnelConnections": "सुरंग कनेक्शन",
"connection": "संबंध",
"remove": "निकालना",
"sourcePort": "स्रोत पोर्ट",
"sourcePortDesc": " (स्रोत से तात्पर्य सामान्य टैब में वर्तमान कनेक्शन विवरण से है)",
"endpointPort": "एंडपॉइंट पोर्ट",
"endpointSshConfig": "एंडपॉइंट एसएसएच कॉन्फ़िगरेशन",
"tunnelForwardDescription": "यह टनल स्रोत मशीन पर पोर्ट {{sourcePort}} (सामान्य टैब में वर्तमान कनेक्शन विवरण) से एंडपॉइंट मशीन पर पोर्ट {{endpointPort}} तक ट्रैफ़िक अग्रेषित करेगा।",
"maxRetries": "अधिकतम पुनः प्रयास",
"maxRetriesDescription": "टनल कनेक्शन के लिए अधिकतम पुनः प्रयास की संख्या।",
"retryInterval": "पुनः प्रयास अंतराल (सेकंड)",
"retryIntervalDescription": "पुनः प्रयास करने के बीच प्रतीक्षा करने का समय।",
"autoStartContainer": "कंटेनर लॉन्च होने पर ऑटो स्टार्ट",
"autoStartDesc": "कंटेनर लॉन्च होने पर यह टनल स्वचालित रूप से शुरू हो जाएगी",
"addConnection": "टनल कनेक्शन जोड़ें",
"tunnelType": "सुरंग प्रकार",
"tunnelTypeLocal": "स्थानीय (-एल)",
"tunnelTypeRemote": "रिमोट (-R)",
"tunnelTypeLocalDesc": "स्थानीय पोर्ट को रिमोट एंडपॉइंट पर अग्रेषित करें",
"tunnelTypeRemoteDesc": "रिमोट पोर्ट को लोकल मशीन पर फॉरवर्ड करें",
"tunnelForwardDescriptionLocal": "यह टनल स्थानीय पोर्ट {{sourcePort}} से एंडपॉइंट मशीन पर पोर्ट {{endpointPort}} तक ट्रैफ़िक को अग्रेषित करेगा।",
"tunnelForwardDescriptionRemote": "यह टनल स्रोत मशीन पर पोर्ट {{sourcePort}} (सामान्य टैब में वर्तमान कनेक्शन विवरण) से एंडपॉइंट मशीन पर पोर्ट {{endpointPort}} तक ट्रैफ़िक अग्रेषित करेगा।",
"sshpassRequired": "पासवर्ड प्रमाणीकरण के लिए एसएसएचपास आवश्यक है",
"sshpassRequiredDesc": "टनल में पासवर्ड प्रमाणीकरण के लिए, सिस्टम पर sshpass स्थापित होना आवश्यक है।",
"otherInstallMethods": "स्थापना के अन्य तरीके:",
@@ -1529,7 +1551,7 @@
"splitVertical": "ऊर्ध्वाधर विभाजन",
"closePanel": "पैनल बंद करें",
"reconnect": "रिकनेक्ट",
"sessionEnded": "सत्र समाप्त हुआ",
"sessionEnded": "Session ended.",
"connectionLost": "Connection lost",
"error": "त्रुटि: {{message}}",
"disconnected": "डिस्कनेक्ट किया गया",
@@ -1933,22 +1955,10 @@
"ascending": "आरोही",
"descending": "अवरोही"
},
"tunnel": {
"log": {
"connecting": "सुरंग कनेक्शन स्थापित किया जा रहा है...",
"sourceConnecting": "स्रोत होस्ट {{host}}से कनेक्ट हो रहा है...",
"sourceConnected": "स्रोत होस्ट से कनेक्टेड",
"endpointResolving": "एंडपॉइंट होस्ट {{host}}को हल किया जा रहा है...",
"endpointResolved": "एंडपॉइंट होस्ट हल हो गया",
"forwarding": "पोर्ट फ़ॉरवर्डिंग सेटअप करना {{localPort}} → {{remotePort}}...",
"connected": "सुरंग सफलतापूर्वक स्थापित हो गई",
"retrying": "कनेक्शन विफल हो गया, {{countdown}}सेकंड में पुनः प्रयास किया जा रहा है (प्रयास {{attempt}}/{{max}})...",
"retryExhausted": "अधिकतम पुनः प्रयास प्रयासों की सीमा पूरी हो गई है।",
"error": "टनल कनेक्शन विफल: {{error}}"
}
},
"tunnels": {
"title": "एसएसएच टनल",
"noTunnelsConfigured": "No Tunnels Configured",
"configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started",
"noSshTunnels": "कोई एसएसएच टनल नहीं",
"createFirstTunnelMessage": "आपने अभी तक कोई SSH टनल नहीं बनाई है। शुरुआत करने के लिए होस्ट मैनेजर में टनल कनेक्शन कॉन्फ़िगर करें।",
"connected": "जुड़े हुए",
@@ -1969,27 +1979,100 @@
"disconnect": "डिस्कनेक्ट",
"cancel": "रद्द करना",
"port": "पत्तन",
"localPort": "स्थानीय बंदरगाह",
"remotePort": "रिमोट पोर्ट",
"currentHostPort": "Current Host Port",
"endpointPort": "Endpoint Port",
"bindIp": "Local IP",
"currentHostIp": "Current Host IP",
"endpointSshConfig": "Endpoint SSH Configuration",
"endpointSshConfigRequired": "Endpoint SSH configuration is required",
"endpointSshHost": "Endpoint SSH Host",
"endpointSshHostPlaceholder": "Select a configured host",
"endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.",
"attempt": "प्रयास {{current}} का {{max}}",
"nextRetryIn": "अगला प्रयास {{seconds}} सेकंड में होगा",
"checkDockerLogs": "त्रुटि का कारण जानने के लिए अपने डॉकर लॉग की जाँच करें, और फिर शामिल हों।",
"orCreate": "या बनाएँ ",
"noTunnelConnections": "कोई टनल कनेक्शन कॉन्फ़िगर नहीं किया गया है",
"tunnelConnections": "सुरंग कनेक्शन",
"serverTunnels": "Server Tunnels",
"serverTunnelsDesc": "Backend-managed tunnels stored with this host.",
"clientTunnels": "Client Tunnels",
"clientTunnelsUnavailable": "Client tunnels require a desktop client.",
"serverTunnel": "Server Tunnel",
"clientTunnel": "Client Tunnel",
"addServerTunnel": "Add Server Tunnel",
"addClientTunnel": "Add Client Tunnel",
"noServerTunnels": "No server tunnels configured.",
"noClientTunnels": "No client tunnels configured on this desktop.",
"manageClientTunnels": "Manage Client Tunnels",
"addTunnel": "सुरंग जोड़ें",
"editTunnel": "सुरंग संपादित करें",
"deleteTunnel": "सुरंग हटाएं",
"tunnelName": "सुरंग का नाम",
"localPort": "स्थानीय बंदरगाह",
"remoteHost": "रिमोट होस्ट",
"remotePort": "रिमोट पोर्ट",
"autoStart": "ऑटो स्टार्ट",
"autoStartContainer": "Auto Start on Launch",
"autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.",
"autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.",
"clientAutoStartDesc": "Starts when this desktop client opens and stays connected.",
"clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.",
"clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.",
"clientTunnelStarted": "Client tunnel started",
"clientTunnelStopped": "Client tunnel stopped",
"tunnelTestSucceeded": "Tunnel test succeeded",
"tunnelTestFailed": "Tunnel test failed",
"localSaved": "Client tunnels saved",
"localSaveError": "Failed to save local client tunnels",
"invalidBindIp": "Local IP must be a valid IPv4 address.",
"invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.",
"invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.",
"invalidLocalPort": "Local port must be between 1 and 65535.",
"invalidRemotePort": "Remote port must be between 1 and 65535.",
"invalidLocalTargetPort": "Local target port must be between 1 and 65535.",
"invalidEndpointPort": "Endpoint port must be between 1 and 65535.",
"duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.",
"manualControlError": "Failed to update tunnel state.",
"saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.",
"status": "स्थिति",
"active": "सक्रिय",
"inactive": "निष्क्रिय",
"start": "शुरू",
"stop": "रुकना",
"test": "Test",
"restart": "पुनः आरंभ करें",
"connectionType": "रिश्ते का प्रकार",
"type": "Tunnel Type",
"typeLocal": "Local (-L)",
"typeRemote": "Remote (-R)",
"typeDynamic": "Dynamic (-D)",
"typeServerLocalDesc": "Current host to endpoint.",
"typeServerRemoteDesc": "Endpoint back to current host.",
"typeClientLocalDesc": "Local computer to endpoint.",
"typeClientRemoteDesc": "Endpoint back to local computer.",
"typeClientDynamicDesc": "SOCKS on local computer.",
"typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH",
"forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.",
"forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.",
"forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.",
"forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.",
"forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.",
"forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.",
"summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}",
"summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}",
"summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}",
"autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}",
"autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}",
"autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}",
"route": "Route:",
"lastStarted": "Last started",
"lastTested": "Last tested",
"lastError": "Last error",
"maxRetries": "Max Retries",
"maxRetriesDescription": "Maximum amount of retry attempts.",
"retryInterval": "Retry Interval (seconds)",
"retryIntervalDescription": "Time to wait between retry attempts.",
"local": "स्थानीय",
"remote": "दूर",
"dynamic": "गतिशील",
@@ -2172,6 +2255,8 @@
"sshProvideCredentialsDescription": "इस सर्वर से कनेक्ट करने के लिए कृपया अपने SSH क्रेडेंशियल प्रदान करें।",
"sshPasswordDescription": "इस एसएसएच कनेक्शन के लिए पासवर्ड दर्ज करें।",
"sshKeyPasswordDescription": "यदि आपकी SSH कुंजी एन्क्रिप्टेड है, तो यहां पासफ़्रेज़ दर्ज करें।",
"passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"step1ScanQR": "चरण 1: अपने प्रमाणीकरण ऐप से क्यूआर कोड को स्कैन करें",
"manualEntryCode": "मैनुअल एंट्री कोड",
"cannotScanQRText": "यदि आप क्यूआर कोड स्कैन नहीं कर पा रहे हैं, तो इस कोड को अपने प्रमाणीकरण ऐप में मैन्युअल रूप से दर्ज करें।",
@@ -2246,7 +2331,7 @@
"failedCompleteReset": "पासवर्ड रीसेट करने में विफलता",
"invalidTotpCode": "अमान्य टीओटीपी कोड",
"failedOidcLogin": "OIDC लॉगिन शुरू करने में विफल",
"failedUserInfo": "OIDC लॉगिन के बाद उपयोगकर्ता जानकारी प्राप्त करने में विफलता",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC प्रमाणीकरण विफल रहा",
"noTokenReceived": "लॉगिन से कोई टोकन प्राप्त नहीं हुआ",
"invalidAuthUrl": "बैकएंड से अमान्य प्राधिकरण यूआरएल प्राप्त हुआ।",
@@ -2322,6 +2407,23 @@
"showHostTagsDesc": "साइडबार में प्रत्येक होस्ट के नीचे टैग प्रदर्शित करें। सभी टैग छिपाने के लिए इसे अक्षम करें।",
"account": "खाता",
"appearance": "उपस्थिति",
"c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.",
"c2sTunnelPresets": "Client Tunnel Presets",
"c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.",
"c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.",
"c2sPresetName": "Preset Name",
"c2sPresetNamePlaceholder": "Client preset name",
"c2sPresetToLoad": "Preset To Load",
"c2sNoPresetSelected": "No preset selected",
"c2sNoPresets": "No presets saved",
"c2sLoadPreset": "Load",
"c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.",
"c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.",
"c2sPresetSaved": "Client tunnel preset saved",
"c2sPresetLoaded": "Client tunnel preset loaded locally",
"c2sPresetRenamed": "Client tunnel preset renamed",
"c2sPresetDeleted": "Client tunnel preset deleted",
"c2sPresetLoadError": "Failed to load client tunnel presets",
"languageLocalization": "भाषा और स्थानीयकरण",
"fileManagerSettings": "फ़ाइल मैनेजर",
"terminalSettings": "टर्मिनल",
@@ -2370,6 +2472,10 @@
"description": "एसएसएच क्रेडेंशियल विवरण",
"searchCredentials": "नाम, उपयोगकर्ता नाम या टैग के आधार पर क्रेडेंशियल खोजें...",
"sshConfig": "एंडपॉइंट एसएसएच कॉन्फ़िगरेशन",
"bindLocalhost": "127.0.0.1 (bind to localhost)",
"localListenerHost": "127.0.0.1 (listen locally)",
"localTargetHost": "127.0.0.1 (target on this computer)",
"socksListenerHost": "127.0.0.1 (SOCKS listener)",
"homePath": "/घर",
"clientId": "आपका क्लाइंट आईडी",
"clientSecret": "आपका-ग्राहक-गुप्त",
@@ -2549,6 +2655,7 @@
"version": "संस्करण",
"upToDate": "अप टू डेट",
"updateAvailable": "उपलब्ध अद्यतन",
"beta": "Beta",
"uptime": "अपटाइम",
"database": "डेटाबेस",
"healthy": "स्वस्थ",
@@ -2955,6 +3062,20 @@
"error": "मैट्रिक्स संग्रह विफल: {{error}}"
}
},
"tunnel": {
"log": {
"connecting": "सुरंग कनेक्शन स्थापित किया जा रहा है...",
"sourceConnecting": "स्रोत होस्ट {{host}}से कनेक्ट हो रहा है...",
"sourceConnected": "स्रोत होस्ट से कनेक्टेड",
"endpointResolving": "एंडपॉइंट होस्ट {{host}}को हल किया जा रहा है...",
"endpointResolved": "एंडपॉइंट होस्ट हल हो गया",
"forwarding": "पोर्ट फ़ॉरवर्डिंग सेटअप करना {{localPort}} → {{remotePort}}...",
"connected": "सुरंग सफलतापूर्वक स्थापित हो गई",
"retrying": "कनेक्शन विफल हो गया, {{countdown}}सेकंड में पुनः प्रयास किया जा रहा है (प्रयास {{attempt}}/{{max}})...",
"retryExhausted": "अधिकतम पुनः प्रयास प्रयासों की सीमा पूरी हो गई है।",
"error": "टनल कनेक्शन विफल: {{error}}"
}
},
"sftp": {
"log": {
"connecting": "SFTP सर्वर से कनेक्ट हो रहा है...",

Some files were not shown because too many files have changed in this diff Show More