mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7450d5867 | |||
| e9e30cd318 | |||
| a255a08903 | |||
| ea2e59abd8 | |||
| 5caadf1d5d | |||
| 98d3c86cc7 | |||
| ee9824d47f | |||
| effe419d97 | |||
| bb5d104b52 | |||
| e4361b9bd1 | |||
| 1b8f6b54b4 | |||
| a0237dc155 | |||
| 55789db3c5 | |||
| dcf5468acc | |||
| 0ff03110c9 | |||
| 44049b8031 | |||
| ffec17201d |
@@ -31,7 +31,6 @@ Thumbs.db
|
||||
.gitignore
|
||||
|
||||
README.md
|
||||
README-CN.md
|
||||
CONTRIBUTING.md
|
||||
LICENSE
|
||||
|
||||
|
||||
@@ -319,7 +319,6 @@ jobs:
|
||||
cd flatpak-build
|
||||
flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml
|
||||
|
||||
# Determine the architecture
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
FLATPAK_ARCH="x86_64"
|
||||
@@ -329,7 +328,6 @@ jobs:
|
||||
FLATPAK_ARCH="$ARCH"
|
||||
fi
|
||||
|
||||
# Build bundle for the current architecture
|
||||
flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Create flatpakref file
|
||||
|
||||
@@ -8,6 +8,9 @@ jobs:
|
||||
lint-and-build:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
cask "termix" do
|
||||
version "1.11.0"
|
||||
sha256 "c62ea2c3097f466229edcbd65a5ea49d898028a716a3023704c5f644d973c70d"
|
||||
version "1.11.2"
|
||||
sha256 "92eb67e25a302474084aeee92071561b037aff9b2d5d6fdafe33f8aeb18cb05d"
|
||||
|
||||
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
|
||||
name "Termix"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
# Repo Stats
|
||||
|
||||
<p align="center">
|
||||
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English |
|
||||
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a>
|
||||
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English ·
|
||||
<a href="readme/README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
|
||||
<a href="readme/README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
|
||||
<a href="readme/README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
|
||||
<a href="readme/README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
|
||||
<a href="readme/README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
|
||||
<a href="readme/README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
|
||||
<a href="readme/README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
|
||||
<a href="readme/README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
|
||||
<a href="readme/README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
|
||||
<a href="readme/README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
|
||||
<a href="readme/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/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/README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
|
||||
</p>
|
||||
|
||||

|
||||
@@ -34,12 +46,13 @@ If you would like, you can support the project here!\
|
||||
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform
|
||||
solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal
|
||||
access, SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect
|
||||
access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote SSH file management, and many other tools. Termix is the perfect
|
||||
free and self-hosted alternative to Termius available for all platforms.
|
||||
|
||||
# Features
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **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.
|
||||
@@ -47,19 +60,20 @@ free and self-hosted alternative to Termius available for all platforms.
|
||||
- **Server Stats** - View CPU, memory, and disk usage along with network, uptime, system information, firewall, port monitor, on most Linux based servers
|
||||
- **Dashboard** - View server information at a glance on your dashboard
|
||||
- **RBAC** - Create roles and share hosts across users/roles
|
||||
- **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together.
|
||||
- **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.
|
||||
- **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 dark or light mode based UI. Use URL routes to open any connection in full-screen.
|
||||
- **Languages** - Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations))
|
||||
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS), PWA, and dedicated mobile/tablet app for iOS and Android.
|
||||
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS, can be run standalone without Termix backend), PWA, and dedicated mobile/tablet app for iOS and Android.
|
||||
- **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals.
|
||||
- **Command History** - Auto-complete and view previously ran SSH commands
|
||||
- **Quick Connect** - Connect to a server without having to save the connection data
|
||||
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
|
||||
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
|
||||
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
|
||||
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
|
||||
|
||||
# Planned Features
|
||||
|
||||
@@ -86,13 +100,13 @@ Supported Devices:
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- ISO
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix on all platforms. Otherwise, view
|
||||
a sample Docker Compose file here:
|
||||
a sample Docker Compose file here (you can omit guacd and the network if you don't plan on using remote desktop features):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -106,10 +120,27 @@ services:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Sponsors
|
||||
@@ -126,6 +157,10 @@ volumes:
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Support
|
||||
@@ -136,7 +171,7 @@ channel, however, response times may be longer.
|
||||
|
||||
# Screenshots
|
||||
|
||||
[](https://youtu.be/8OYCPwS7ciA)
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
|
||||
|
||||
+4
-5
@@ -6,8 +6,7 @@ RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN rm -rf node_modules package-lock.json && \
|
||||
npm install --ignore-scripts --force && \
|
||||
RUN npm ci --ignore-scripts --force && \
|
||||
npm cache clean --force
|
||||
|
||||
# Stage 2: Build frontend
|
||||
@@ -51,7 +50,7 @@ ENV DATA_DIR=/app/data \
|
||||
PORT=8080 \
|
||||
NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu && \
|
||||
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 && \
|
||||
@@ -75,8 +74,8 @@ VOLUME ["/app/data"]
|
||||
|
||||
EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:30001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://localhost:30001/health || exit 1
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
+17
-8
@@ -5,16 +5,21 @@ PUID=${PUID:-1000}
|
||||
PGID=${PGID:-1000}
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."
|
||||
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
|
||||
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
|
||||
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 /app/nginx 2>/dev/null || true
|
||||
|
||||
echo "User node is now UID: $PUID, GID: $PGID"
|
||||
echo "User node is now UID: $PUID, GID: $PGID"
|
||||
|
||||
exec gosu node:node "$0" "$@"
|
||||
exec gosu node:node "$0" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
export PORT=${PORT:-8080}
|
||||
@@ -52,8 +57,12 @@ else
|
||||
ls -ld /app/data/.opk
|
||||
fi
|
||||
|
||||
if [ ! -d "/app/opkssh" ]; then
|
||||
echo "WARNING: OPKSSH binary directory not found at /app/opkssh"
|
||||
OPKSSH_DIR="${DATA_DIR:-/app/data}/opkssh"
|
||||
if [ ! -d "$OPKSSH_DIR" ]; then
|
||||
echo "WARNING: OPKSSH binary directory not found at $OPKSSH_DIR"
|
||||
echo "OPKSSH will be downloaded automatically on first use."
|
||||
else
|
||||
echo "OPKSSH binary directory found at $OPKSSH_DIR"
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_SSL" = "true" ]; then
|
||||
|
||||
+144
-55
@@ -26,6 +26,21 @@ http {
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $proxy_x_forwarded_host {
|
||||
default $http_x_forwarded_host;
|
||||
'' $http_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_port $proxy_x_forwarded_port {
|
||||
default $http_x_forwarded_port;
|
||||
'' '';
|
||||
}
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
@@ -72,25 +87,27 @@ http {
|
||||
location ~ ^/users/sessions(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
}
|
||||
|
||||
location ~ ^/users(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
}
|
||||
|
||||
location ~ ^/version(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -99,7 +116,7 @@ http {
|
||||
location ~ ^/releases(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -108,7 +125,7 @@ http {
|
||||
location ~ ^/alerts(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -117,7 +134,7 @@ http {
|
||||
location ~ ^/rbac(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -126,7 +143,7 @@ http {
|
||||
location ~ ^/credentials(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -139,7 +156,7 @@ http {
|
||||
location ~ ^/snippets(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -148,7 +165,7 @@ http {
|
||||
location ~ ^/terminal(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -160,7 +177,7 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -179,7 +196,7 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -195,50 +212,58 @@ http {
|
||||
location ~ ^/encryption(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/quick-connect {
|
||||
location /host/quick-connect {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
|
||||
location ~ ^/host/opkssh-chooser(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_cache_bypass 1;
|
||||
proxy_no_cache 1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
}
|
||||
|
||||
location ~ ^/ssh/opkssh-callback(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
|
||||
location ~ ^/host/opkssh-callback(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_cache_bypass 1;
|
||||
proxy_no_cache 1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
}
|
||||
|
||||
location /ssh/ {
|
||||
location /host/ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -251,15 +276,13 @@ http {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
@@ -271,52 +294,86 @@ http {
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
}
|
||||
|
||||
location /ssh/tunnel/ {
|
||||
location ^~ /guacamole/websocket/ {
|
||||
proxy_pass http://127.0.0.1:30008/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
}
|
||||
|
||||
location ~ ^/guacamole(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /host/tunnel/ {
|
||||
proxy_pass http://127.0.0.1:30003;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/recent {
|
||||
location /host/file_manager/recent {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/pinned {
|
||||
location /host/file_manager/pinned {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/shortcuts {
|
||||
location /host/file_manager/shortcuts {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/sudo-password {
|
||||
location /host/file_manager/sudo-password {
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/ssh/ {
|
||||
location /ssh/file_manager/ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
@@ -324,7 +381,28 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /host/file_manager/ssh/ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
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;
|
||||
@@ -340,7 +418,7 @@ http {
|
||||
location ~ ^/network-topology(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -349,7 +427,7 @@ http {
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -358,7 +436,7 @@ http {
|
||||
location ~ ^/status(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30005;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -367,7 +445,7 @@ http {
|
||||
location ~ ^/metrics(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30005;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -377,10 +455,19 @@ http {
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location ~ ^/global-settings(/.*)?$ {
|
||||
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 ~ ^/uptime(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -389,7 +476,7 @@ http {
|
||||
location ~ ^/activity(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -398,24 +485,26 @@ http {
|
||||
location ~ ^/dashboard/preferences(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 ^~ /docker/console/ {
|
||||
proxy_pass http://127.0.0.1:30008/;
|
||||
proxy_pass http://127.0.0.1:30009/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
@@ -430,7 +519,7 @@ http {
|
||||
location ~ ^/docker(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30007;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
|
||||
+144
-54
@@ -26,6 +26,21 @@ http {
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $proxy_x_forwarded_host {
|
||||
default $http_x_forwarded_host;
|
||||
'' $http_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_port $proxy_x_forwarded_port {
|
||||
default $http_x_forwarded_port;
|
||||
'' '';
|
||||
}
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
@@ -61,25 +76,27 @@ http {
|
||||
location ~ ^/users/sessions(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
}
|
||||
|
||||
location ~ ^/users(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
}
|
||||
|
||||
location ~ ^/version(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -88,7 +105,7 @@ http {
|
||||
location ~ ^/releases(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -97,7 +114,7 @@ http {
|
||||
location ~ ^/alerts(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -106,7 +123,7 @@ http {
|
||||
location ~ ^/rbac(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -115,7 +132,7 @@ http {
|
||||
location ~ ^/credentials(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -128,7 +145,7 @@ http {
|
||||
location ~ ^/snippets(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -137,7 +154,7 @@ http {
|
||||
location ~ ^/terminal(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -149,7 +166,7 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -168,7 +185,7 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -184,50 +201,58 @@ http {
|
||||
location ~ ^/encryption(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/quick-connect {
|
||||
location /host/quick-connect {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
|
||||
location ~ ^/host/opkssh-chooser(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_cache_bypass 1;
|
||||
proxy_no_cache 1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
}
|
||||
|
||||
location ~ ^/ssh/opkssh-callback(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
|
||||
location ~ ^/host/opkssh-callback(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
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_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_cache_bypass 1;
|
||||
proxy_no_cache 1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
}
|
||||
|
||||
location /ssh/ {
|
||||
location /host/ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -240,14 +265,13 @@ http {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
@@ -259,52 +283,86 @@ http {
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
}
|
||||
|
||||
location /ssh/tunnel/ {
|
||||
location ^~ /guacamole/websocket/ {
|
||||
proxy_pass http://127.0.0.1:30008/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
}
|
||||
|
||||
location ~ ^/guacamole(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /host/tunnel/ {
|
||||
proxy_pass http://127.0.0.1:30003;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/recent {
|
||||
location /host/file_manager/recent {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/pinned {
|
||||
location /host/file_manager/pinned {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/shortcuts {
|
||||
location /host/file_manager/shortcuts {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/sudo-password {
|
||||
location /host/file_manager/sudo-password {
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 /ssh/file_manager/ssh/ {
|
||||
location /ssh/file_manager/ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
@@ -312,7 +370,28 @@ http {
|
||||
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /host/file_manager/ssh/ {
|
||||
client_max_body_size 5G;
|
||||
client_body_timeout 300s;
|
||||
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
|
||||
proxy_pass http://127.0.0.1:30004;
|
||||
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;
|
||||
@@ -328,7 +407,7 @@ http {
|
||||
location ~ ^/network-topology(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -337,7 +416,7 @@ http {
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:30001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -346,7 +425,7 @@ http {
|
||||
location ~ ^/status(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30005;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -355,7 +434,7 @@ http {
|
||||
location ~ ^/metrics(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30005;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -365,10 +444,19 @@ http {
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location ~ ^/global-settings(/.*)?$ {
|
||||
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 ~ ^/uptime(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -377,7 +465,7 @@ http {
|
||||
location ~ ^/activity(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
@@ -386,24 +474,26 @@ http {
|
||||
location ~ ^/dashboard/preferences(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 ^~ /docker/console/ {
|
||||
proxy_pass http://127.0.0.1:30008/;
|
||||
proxy_pass http://127.0.0.1:30009/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
@@ -418,7 +508,7 @@ http {
|
||||
location ~ ^/docker(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:30007;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
|
||||
+19
-5
@@ -5,18 +5,29 @@
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"asar": false,
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*",
|
||||
"public/**/*",
|
||||
"!**/node_modules/**/*",
|
||||
"!src/**/*",
|
||||
"!*.md",
|
||||
"!tsconfig*.json",
|
||||
"!vite.config.ts",
|
||||
"!eslint.config.js"
|
||||
"!eslint.config.js",
|
||||
"!node_modules/@napi-rs/canvas*/**/*",
|
||||
"!node_modules/@rollup/rollup-darwin-*/**/*",
|
||||
"!node_modules/@rollup/rollup-linux-*/**/*",
|
||||
"!node_modules/@rollup/rollup-win32-*/**/*",
|
||||
"!dist/icon-mac.png",
|
||||
"!public/icon-mac.png",
|
||||
"!dist/icon.ico",
|
||||
"!public/icon.ico",
|
||||
"!dist/icon.icns",
|
||||
"!public/icon.icns",
|
||||
"!dist/icons/**/*",
|
||||
"!public/icons/**/*"
|
||||
],
|
||||
"asarUnpack": ["node_modules/node-fetch/**/*"],
|
||||
"extraMetadata": {
|
||||
"main": "electron/main.cjs"
|
||||
},
|
||||
@@ -103,7 +114,10 @@
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
|
||||
"type": "distribution",
|
||||
"minimumSystemVersion": "10.15"
|
||||
"minimumSystemVersion": "10.15",
|
||||
"mergeASARs": false,
|
||||
"singleArchFiles": "**/*.node",
|
||||
"x64ArchFiles": "**/*.node"
|
||||
},
|
||||
"dmg": {
|
||||
"artifactName": "termix_macos_${arch}_dmg.${ext}",
|
||||
@@ -116,7 +130,7 @@
|
||||
"entitlementsInherit": "build/entitlements.mas.inherit.plist",
|
||||
"hardenedRuntime": false,
|
||||
"gatekeeperAssess": false,
|
||||
"asarUnpack": ["**/*.node"],
|
||||
"asar": false,
|
||||
"type": "distribution",
|
||||
"category": "public.app-category.developer-tools",
|
||||
"artifactName": "termix_macos_${arch}_mas.${ext}",
|
||||
|
||||
+340
-116
@@ -5,10 +5,76 @@ const {
|
||||
ipcMain,
|
||||
dialog,
|
||||
Menu,
|
||||
Tray,
|
||||
} = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const { URL } = require("url");
|
||||
const { fork } = require("child_process");
|
||||
|
||||
const logFile = path.join(app.getPath("userData"), "termix-main.log");
|
||||
function logToFile(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const msg = args
|
||||
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
|
||||
.join(" ");
|
||||
const line = `[${timestamp}] ${msg}\n`;
|
||||
try {
|
||||
fs.appendFileSync(logFile, line);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
function httpFetch(url, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 10000,
|
||||
};
|
||||
|
||||
if (isHttps) {
|
||||
requestOptions.rejectUnauthorized = false;
|
||||
requestOptions.agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
checkServerIdentity: () => undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const req = client.request(url, requestOptions, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("Request timeout"));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(options.body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
app.commandLine.appendSwitch("--ozone-platform-hint=auto");
|
||||
@@ -22,10 +88,148 @@ app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
|
||||
app.commandLine.appendSwitch("--enable-features=NetworkService");
|
||||
|
||||
let mainWindow = null;
|
||||
let backendProcess = null;
|
||||
let tray = null;
|
||||
let isQuitting = false;
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
|
||||
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
|
||||
|
||||
function getBackendEntryPath() {
|
||||
if (isDev) {
|
||||
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
|
||||
}
|
||||
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
|
||||
}
|
||||
|
||||
function getBackendDataDir() {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const dataDir = path.join(userDataPath, "server-data");
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
function startBackendServer() {
|
||||
return new Promise((resolve) => {
|
||||
const entryPath = getBackendEntryPath();
|
||||
|
||||
logToFile("isDev:", isDev, "appRoot:", appRoot);
|
||||
logToFile("app.isPackaged:", app.isPackaged);
|
||||
logToFile("process.env.NODE_ENV:", process.env.NODE_ENV);
|
||||
|
||||
if (!fs.existsSync(entryPath)) {
|
||||
logToFile("Backend entry not found:", entryPath);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataDir = getBackendDataDir();
|
||||
logToFile("Starting embedded backend server...");
|
||||
logToFile("Backend entry:", entryPath);
|
||||
logToFile("Data directory:", dataDir);
|
||||
logToFile("Backend cwd:", appRoot);
|
||||
|
||||
logToFile("Checking paths...");
|
||||
logToFile(" entryPath exists:", fs.existsSync(entryPath));
|
||||
logToFile(" dataDir exists:", fs.existsSync(dataDir));
|
||||
logToFile(" appRoot exists:", fs.existsSync(appRoot));
|
||||
|
||||
const distPath = path.join(appRoot, "dist");
|
||||
if (fs.existsSync(distPath)) {
|
||||
logToFile(" dist directory contents:", fs.readdirSync(distPath));
|
||||
const backendPath = path.join(distPath, "backend");
|
||||
if (fs.existsSync(backendPath)) {
|
||||
logToFile(" dist/backend contents:", fs.readdirSync(backendPath));
|
||||
}
|
||||
}
|
||||
|
||||
backendProcess = fork(entryPath, [], {
|
||||
cwd: appRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
DATA_DIR: dataDir,
|
||||
NODE_ENV: "production",
|
||||
ELECTRON_EMBEDDED: "true",
|
||||
PORT: "30001",
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||
});
|
||||
|
||||
logToFile("Backend process spawned, pid:", backendProcess.pid);
|
||||
|
||||
let resolved = false;
|
||||
const readyTimeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
logToFile("Backend ready timeout (15s), proceeding anyway...");
|
||||
resolve(true);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
backendProcess.stdout.on("data", (data) => {
|
||||
const msg = data.toString().trim();
|
||||
logToFile("[backend]", msg);
|
||||
if (!resolved && msg.includes("started successfully")) {
|
||||
resolved = true;
|
||||
clearTimeout(readyTimeout);
|
||||
logToFile("Backend ready signal received");
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
backendProcess.stderr.on("data", (data) => {
|
||||
logToFile("[backend:stderr]", data.toString().trim());
|
||||
});
|
||||
|
||||
backendProcess.on("exit", (code, signal) => {
|
||||
logToFile(`Backend process exited with code ${code}, signal ${signal}`);
|
||||
backendProcess = null;
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(readyTimeout);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
backendProcess.on("error", (err) => {
|
||||
logToFile("Failed to start backend process:", err.message);
|
||||
backendProcess = null;
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(readyTimeout);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopBackendServer() {
|
||||
if (!backendProcess) return;
|
||||
|
||||
console.log("Stopping embedded backend server...");
|
||||
|
||||
try {
|
||||
backendProcess.send({ type: "shutdown" });
|
||||
} catch {
|
||||
// IPC channel may already be closed
|
||||
}
|
||||
|
||||
const forceKillTimeout = setTimeout(() => {
|
||||
if (backendProcess) {
|
||||
console.log("Force killing backend process...");
|
||||
backendProcess.kill("SIGKILL");
|
||||
backendProcess = null;
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
backendProcess.on("exit", () => {
|
||||
clearTimeout(forceKillTimeout);
|
||||
backendProcess = null;
|
||||
});
|
||||
}
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
console.log("Another instance is already running, quitting...");
|
||||
@@ -41,6 +245,62 @@ if (!gotTheLock) {
|
||||
});
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
try {
|
||||
const { nativeImage } = require("electron");
|
||||
|
||||
let trayIcon;
|
||||
if (process.platform === "darwin") {
|
||||
const iconPath = path.join(appRoot, "public", "icons", "16x16.png");
|
||||
trayIcon = nativeImage.createFromPath(iconPath);
|
||||
trayIcon.setTemplateImage(true);
|
||||
} else if (process.platform === "win32") {
|
||||
trayIcon = path.join(appRoot, "public", "icon.ico");
|
||||
} else {
|
||||
trayIcon = path.join(appRoot, "public", "icons", "32x32.png");
|
||||
}
|
||||
|
||||
tray = new Tray(trayIcon);
|
||||
tray.setToolTip("Termix");
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show Window",
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on("click", () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("System tray created successfully");
|
||||
} catch (err) {
|
||||
console.error("Failed to create system tray:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const appVersion = app.getVersion();
|
||||
const electronVersion = process.versions.electron;
|
||||
@@ -71,6 +331,19 @@ function createWindow() {
|
||||
show: true,
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.setPermissionRequestHandler(
|
||||
(webContents, permission, callback) => {
|
||||
if (
|
||||
permission === "clipboard-read" ||
|
||||
permission === "clipboard-sanitized-write"
|
||||
) {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
callback(true);
|
||||
},
|
||||
);
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
@@ -183,6 +456,13 @@ function createWindow() {
|
||||
console.log("Frontend loaded successfully");
|
||||
});
|
||||
|
||||
mainWindow.on("close", (event) => {
|
||||
if (!isQuitting && tray && !tray.isDestroyed()) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
@@ -215,65 +495,7 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
|
||||
}
|
||||
|
||||
try {
|
||||
let fetch;
|
||||
try {
|
||||
fetch = globalThis.fetch || require("node-fetch");
|
||||
} catch (e) {
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const { URL } = require("url");
|
||||
|
||||
fetch = (url, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 10000,
|
||||
};
|
||||
|
||||
if (isHttps) {
|
||||
requestOptions.rejectUnauthorized = false;
|
||||
requestOptions.agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
secureProtocol: "TLSv1_2_method",
|
||||
checkServerIdentity: () => undefined,
|
||||
ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
|
||||
honorCipherOrder: true,
|
||||
});
|
||||
}
|
||||
|
||||
const req = client.request(url, requestOptions, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("Request timeout"));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(options.body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
|
||||
const response = await httpFetch(`${GITHUB_API_BASE}${endpoint}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "TermixElectronUpdateChecker/1.0",
|
||||
@@ -358,6 +580,14 @@ ipcMain.handle("get-platform", () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
ipcMain.handle("get-embedded-server-status", () => {
|
||||
return {
|
||||
running: backendProcess !== null && !backendProcess.killed,
|
||||
embedded: !isDev,
|
||||
dataDir: isDev ? null : getBackendDataDir(),
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle("get-server-config", () => {
|
||||
try {
|
||||
const userDataPath = app.getPath("userData");
|
||||
@@ -433,67 +663,33 @@ ipcMain.handle("set-setting", (event, key, value) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("clear-session-cookies", async () => {
|
||||
try {
|
||||
const ses = mainWindow?.webContents?.session;
|
||||
if (ses) {
|
||||
const cookies = await ses.cookies.get({});
|
||||
for (const cookie of cookies) {
|
||||
const scheme = cookie.secure ? "https" : "http";
|
||||
const domain = cookie.domain?.startsWith(".")
|
||||
? cookie.domain.slice(1)
|
||||
: cookie.domain || "localhost";
|
||||
const url = `${scheme}://${domain}${cookie.path || "/"}`;
|
||||
await ses.cookies.remove(url, cookie.name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to clear session cookies:", error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
||||
try {
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const { URL } = require("url");
|
||||
|
||||
const fetch = (url, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 10000,
|
||||
};
|
||||
|
||||
if (isHttps) {
|
||||
requestOptions.rejectUnauthorized = false;
|
||||
requestOptions.agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
secureProtocol: "TLSv1_2_method",
|
||||
checkServerIdentity: () => undefined,
|
||||
ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
|
||||
honorCipherOrder: true,
|
||||
});
|
||||
}
|
||||
|
||||
const req = client.request(url, requestOptions, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("Request timeout"));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(options.body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
const normalizedServerUrl = serverUrl.replace(/\/$/, "");
|
||||
|
||||
const healthUrl = `${normalizedServerUrl}/health`;
|
||||
|
||||
try {
|
||||
const response = await fetch(healthUrl, {
|
||||
const response = await httpFetch(healthUrl, {
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
});
|
||||
@@ -539,7 +735,7 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
||||
|
||||
try {
|
||||
const versionUrl = `${normalizedServerUrl}/version`;
|
||||
const response = await fetch(versionUrl, {
|
||||
const response = await httpFetch(versionUrl, {
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
});
|
||||
@@ -656,13 +852,36 @@ function createMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
logToFile("=== App ready ===");
|
||||
logToFile(
|
||||
"isDev:",
|
||||
isDev,
|
||||
"platform:",
|
||||
process.platform,
|
||||
"arch:",
|
||||
process.arch,
|
||||
);
|
||||
createMenu();
|
||||
|
||||
if (!isDev) {
|
||||
const result = await startBackendServer();
|
||||
logToFile("startBackendServer result:", result);
|
||||
} else {
|
||||
logToFile(
|
||||
"Skipping embedded backend (isDev=true) - expecting separate dev:backend process",
|
||||
);
|
||||
}
|
||||
|
||||
createTray();
|
||||
createWindow();
|
||||
logToFile("=== Startup complete ===");
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
if (!tray || tray.isDestroyed()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
@@ -671,8 +890,13 @@ app.on("activate", () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
app.on("will-quit", () => {
|
||||
console.log("App will quit...");
|
||||
stopBackendServer();
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const { clipboard } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
||||
@@ -10,7 +11,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getSetting: (key) => ipcRenderer.invoke("get-setting", key),
|
||||
setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value),
|
||||
|
||||
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
|
||||
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld("electronClipboard", {
|
||||
writeText: (text) => clipboard.writeText(text),
|
||||
readText: () => clipboard.readText(),
|
||||
});
|
||||
|
||||
window.IS_ELECTRON = true;
|
||||
|
||||
+9
-1
@@ -6,7 +6,7 @@ import tseslint from "typescript-eslint";
|
||||
import { globalIgnores } from "eslint/config";
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(["dist"]),
|
||||
globalIgnores(["dist", "release", "Mobile"]),
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [
|
||||
@@ -19,5 +19,13 @@ export default tseslint.config([
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-expressions": "warn",
|
||||
"no-empty": "warn",
|
||||
"no-control-regex": "off",
|
||||
"react-refresh/only-export-components": "warn",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -4,7 +4,6 @@ Branch=stable
|
||||
Title=Termix - SSH Server Management Platform
|
||||
IsRuntime=false
|
||||
Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak
|
||||
GPGKey=
|
||||
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
|
||||
Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management.
|
||||
|
||||
Generated
+86
-54
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "1.11.1",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "1.11.1",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
"@codemirror/commands": "^6.3.3",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -43,7 +44,7 @@
|
||||
"@uiw/codemirror-extensions-langs": "^4.24.1",
|
||||
"@uiw/codemirror-theme-github": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.24.1",
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
"@xterm/addon-clipboard": "^0.2.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -62,6 +63,8 @@
|
||||
"dotenv": "^17.2.0",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"express": "^5.1.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"guacamole-lite": "^1.2.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18n-auto-translation": "^2.2.3",
|
||||
"i18next": "^25.4.2",
|
||||
@@ -107,6 +110,7 @@
|
||||
"@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",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
@@ -213,7 +217,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -499,7 +502,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
|
||||
"integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -548,7 +550,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
@@ -575,7 +576,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
@@ -603,7 +603,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
@@ -804,7 +803,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
@@ -881,7 +879,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
@@ -903,7 +900,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -1231,7 +1227,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -1508,9 +1503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz",
|
||||
"integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==",
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz",
|
||||
"integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1618,6 +1613,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1639,6 +1635,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -2692,8 +2689,7 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
|
||||
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
"version": "1.1.3",
|
||||
@@ -2733,7 +2729,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
@@ -2765,7 +2760,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
@@ -2788,7 +2782,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
@@ -5236,7 +5229,6 @@
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -5400,7 +5392,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
|
||||
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -5429,6 +5420,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/guacamole-common-js": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.5.tgz",
|
||||
"integrity": "sha512-dqDYo/PhbOXFGSph23rFDRZRzXdKPXy/nsTkovFMb6P3iGrd0qGB5r5BXHmX5Cr/LK7L1TK9nYrTMbtPkhdXyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -5523,7 +5520,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
|
||||
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -5566,7 +5562,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -5577,7 +5572,6 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5754,7 +5748,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -6120,15 +6113,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-clipboard": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz",
|
||||
"integrity": "sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0.tgz",
|
||||
"integrity": "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-base64": "^3.7.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
@@ -6162,8 +6152,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/7zip-bin": {
|
||||
"version": "5.2.0",
|
||||
@@ -6198,7 +6187,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6368,6 +6356,35 @@
|
||||
"electron-builder-squirrel-windows": "26.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/@electron/rebuild": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz",
|
||||
"integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
|
||||
"@malept/cross-spawn-promise": "^2.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"debug": "^4.1.1",
|
||||
"detect-libc": "^2.0.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"got": "^11.7.0",
|
||||
"node-abi": "^3.45.0",
|
||||
"node-api-version": "^0.2.0",
|
||||
"ora": "^5.1.0",
|
||||
"read-binary-file-arch": "^1.0.6",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.0.5",
|
||||
"yargs": "^17.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"electron-rebuild": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -6626,7 +6643,6 @@
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -6769,7 +6785,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -7776,7 +7791,6 @@
|
||||
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.1",
|
||||
"import-fresh": "^3.3.0",
|
||||
@@ -7853,7 +7867,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@@ -7886,7 +7901,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -8326,7 +8340,6 @@
|
||||
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.0.12",
|
||||
"builder-util": "26.0.11",
|
||||
@@ -8437,7 +8450,8 @@
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "5.3.0",
|
||||
@@ -8812,6 +8826,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -8832,6 +8847,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -8847,6 +8863,7 @@
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -8857,6 +8874,7 @@
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
@@ -9118,7 +9136,6 @@
|
||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10574,6 +10591,25 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/guacamole-common-js": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.5.0.tgz",
|
||||
"integrity": "sha512-zxztif3GGhKbg1RgOqwmqot8kXgv2HmHFg1EvWwd4q7UfEKvBcYZ0f+7G8HzvU+FUxF0Psqm9Kl5vCbgfrRgJg==",
|
||||
"license": "Apache 2.0"
|
||||
},
|
||||
"node_modules/guacamole-lite": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/guacamole-lite/-/guacamole-lite-1.2.0.tgz",
|
||||
"integrity": "sha512-NeSYgbT5s5rxF0SE/kzJsV5Gg0IvnqoTOCbNIUMl23z1+SshaVfLExpxrEXSGTG0cdvY5lfZC1fOAepYriaXGg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ws": "^8.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -11136,7 +11172,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -12719,6 +12754,7 @@
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@@ -14756,6 +14792,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -14773,6 +14810,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -15258,7 +15296,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -15281,7 +15318,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -15340,7 +15376,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
|
||||
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -15488,7 +15523,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -15711,8 +15745,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -17197,6 +17230,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -17237,6 +17271,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -17251,6 +17286,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -17355,7 +17391,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17561,7 +17596,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17984,7 +18018,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18076,7 +18109,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
+13
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"private": true,
|
||||
"version": "1.11.1",
|
||||
"version": "2.0.0",
|
||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
@@ -22,12 +22,13 @@
|
||||
"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 .\"",
|
||||
"build:win-portable": "npm run build && electron-builder --win --dir",
|
||||
"build:win-installer": "npm run build && electron-builder --win --publish=never",
|
||||
"build:linux-portable": "npm run build && electron-builder --linux --dir",
|
||||
"build:linux-appimage": "npm run build && electron-builder --linux AppImage",
|
||||
"build:linux-targz": "npm run build && electron-builder --linux tar.gz",
|
||||
"build:mac": "npm run build && electron-builder --mac --universal"
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
@@ -57,6 +58,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -65,7 +67,7 @@
|
||||
"@uiw/codemirror-extensions-langs": "^4.24.1",
|
||||
"@uiw/codemirror-theme-github": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.24.1",
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
"@xterm/addon-clipboard": "^0.2.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -84,6 +86,8 @@
|
||||
"dotenv": "^17.2.0",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"express": "^5.1.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"guacamole-lite": "^1.2.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18n-auto-translation": "^2.2.3",
|
||||
"i18next": "^25.4.2",
|
||||
@@ -129,6 +133,7 @@
|
||||
"@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",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
|
||||
@@ -54,6 +54,13 @@ self.addEventListener("fetch", (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.startsWith("/ssh/opkssh-chooser/") ||
|
||||
url.pathname.startsWith("/ssh/opkssh-callback/")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.origin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# إحصائيات المستودع
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">تم تحقيقه في 1 سبتمبر 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
إذا كنت ترغب في ذلك، يمكنك دعم المشروع هنا!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# نظرة عامة
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، وقدرات إنشاء أنفاق SSH، وإدارة الملفات عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات.
|
||||
|
||||
# الميزات
|
||||
|
||||
- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك السمات الشائعة والخطوط والمكونات الأخرى
|
||||
- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة
|
||||
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r
|
||||
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo
|
||||
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها
|
||||
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH
|
||||
- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux
|
||||
- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم
|
||||
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار
|
||||
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً
|
||||
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات
|
||||
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات
|
||||
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS
|
||||
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين الوضع الداكن أو الفاتح. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة
|
||||
- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations))
|
||||
- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android
|
||||
- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة
|
||||
- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً
|
||||
- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال
|
||||
- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح
|
||||
- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، وغيرها
|
||||
- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة
|
||||
- **علامات التبويب الدائمة** - تبقى جلسات SSH وعلامات التبويب مفتوحة عبر الأجهزة/التحديثات إذا تم تفعيلها في ملف تعريف المستخدم
|
||||
|
||||
# الميزات المخططة
|
||||
|
||||
راجع [المشاريع](https://github.com/orgs/Termix-SSH/projects/2) لعرض جميع الميزات المخططة. إذا كنت تتطلع للمساهمة، راجع [المساهمة](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# التثبيت
|
||||
|
||||
الأجهزة المدعومة:
|
||||
|
||||
- الموقع الإلكتروني (أي متصفح حديث على أي منصة مثل Chrome و Safari و Firefox) (يتضمن دعم PWA)
|
||||
- Windows (x64/ia32)
|
||||
- نسخة محمولة
|
||||
- مثبت MSI
|
||||
- مدير حزم Chocolatey
|
||||
- Linux (x64/ia32)
|
||||
- نسخة محمولة
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 على الإصدار 12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (الإصدار 15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (الإصدار 7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# الرعاة
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# الدعم
|
||||
|
||||
إذا كنت بحاجة إلى مساعدة أو ترغب في طلب ميزة لـ Termix، قم بزيارة صفحة [المشكلات](https://github.com/Termix-SSH/Support/issues)، وسجل الدخول، واضغط على `New Issue`.
|
||||
يرجى أن تكون مفصلاً قدر الإمكان في مشكلتك، ويُفضَّل كتابتها باللغة الإنجليزية. يمكنك أيضاً الانضمام إلى خادم [Discord](https://discord.gg/jVQGdvHDrf) وزيارة قناة الدعم، ومع ذلك قد تكون أوقات الاستجابة أطول.
|
||||
|
||||
# لقطات الشاشة
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
قد تكون بعض مقاطع الفيديو والصور قديمة أو قد لا تعرض الميزات بشكل مثالي.
|
||||
|
||||
# الترخيص
|
||||
|
||||
موزع بموجب رخصة Apache License الإصدار 2.0. راجع ملف LICENSE لمزيد من المعلومات.
|
||||
@@ -1,8 +1,20 @@
|
||||
# 仓库统计
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> 英文</a> |
|
||||
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||
@@ -11,7 +23,7 @@
|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">2025年9月1日获得</small>
|
||||
</p>
|
||||
@@ -19,7 +31,7 @@
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
如果你愿意,可以在这里支持这个项目!\
|
||||
@@ -29,35 +41,37 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
|
||||
提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
|
||||
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个多平台解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
|
||||
提供 SSH 终端访问、远程桌面控制(RDP、VNC、Telnet)、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
|
||||
|
||||
# 功能
|
||||
|
||||
- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件
|
||||
- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能
|
||||
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件
|
||||
- **Docker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 docker exec 终端控制容器。它不是用来替代 Portainer 或 Dockge,而是用于简单管理你的容器而不是创建它们。
|
||||
- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能
|
||||
- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能,支持 -l 或 -r 连接
|
||||
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件,支持 sudo
|
||||
- **Docker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 docker exec 终端控制容器。它不是用来替代 Portainer 或 Dockge,而是用于简单管理你的容器而不是创建它们
|
||||
- **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
|
||||
- **服务器统计** - 在任何 SSH 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息
|
||||
- **服务器统计** - 在大多数 Linux 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间、系统信息、防火墙、端口监控
|
||||
- **仪表板** - 在仪表板上一目了然地查看服务器信息
|
||||
- **RBAC** - 创建角色并在用户/角色之间共享主机
|
||||
- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地帐户链接在一起。
|
||||
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多信息。
|
||||
- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地帐户链接在一起
|
||||
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多信息
|
||||
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据
|
||||
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
|
||||
- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。
|
||||
- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。使用 URL 路由以全屏方式打开任何连接
|
||||
- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理)
|
||||
- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序。
|
||||
- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令。
|
||||
- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序
|
||||
- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令
|
||||
- **命令历史** - 自动完成并查看以前运行的 SSH 命令
|
||||
- **快速连接** - 无需保存连接数据即可连接到服务器
|
||||
- **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接
|
||||
- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOCKS5、密码自动填充等。
|
||||
- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)等
|
||||
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,支持状态显示
|
||||
- **持久标签页** - 如果在用户配置文件中启用,SSH 会话和标签页在设备/刷新后保持打开状态
|
||||
|
||||
# 计划功能
|
||||
|
||||
@@ -84,12 +98,12 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平
|
||||
- Homebrew
|
||||
- iOS/iPadOS(v15.1+)
|
||||
- Apple App Store
|
||||
- ISO
|
||||
- IPA
|
||||
- Android(v7.0+)
|
||||
- Google Play 商店
|
||||
- APK
|
||||
|
||||
访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 Docker Compose 文件:
|
||||
访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 Docker Compose 文件(如果不打算使用远程桌面功能,可以省略 guacd 和 network):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -103,10 +117,27 @@ services:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# 赞助商
|
||||
@@ -121,7 +152,11 @@ volumes:
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -133,36 +168,36 @@ volumes:
|
||||
|
||||
# 展示
|
||||
|
||||
[](https://www.youtube.com/watch?v=j1_I1mkhUkE)
|
||||
[](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 2.png" width="400" alt="Termix Demo 2"/>
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="./repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="./repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="./repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<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%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<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>
|
||||
|
||||
某些视频和图像可能已过时或可能无法完美展示功能。
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Erreicht am 1. September 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<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!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Überblick
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, SSH-Tunneling-Funktionen, Remote-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen.
|
||||
|
||||
# Funktionen
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen
|
||||
- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen
|
||||
- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen
|
||||
- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- 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).
|
||||
- **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 dunklem oder hellem Modus. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen.
|
||||
- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations))
|
||||
- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS), PWA und dedizierte Mobil-/Tablet-App für iOS und Android.
|
||||
- **SSH-Werkzeuge** - Erstellen Sie wiederverwendbare Befehlsvorlagen, die mit einem einzigen Klick ausgeführt werden. Führen Sie einen Befehl gleichzeitig in mehreren geöffneten Terminals aus.
|
||||
- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle
|
||||
- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen
|
||||
- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen
|
||||
- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh) usw.
|
||||
- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren
|
||||
- **Persistente Tabs** - SSH-Sitzungen und Tabs bleiben über Geräte/Aktualisierungen hinweg offen, wenn im Benutzerprofil aktiviert
|
||||
|
||||
# Geplante Funktionen
|
||||
|
||||
Siehe [Projekte](https://github.com/orgs/Termix-SSH/projects/2) für alle geplanten Funktionen. Wenn Sie beitragen möchten, siehe [Mitwirken](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Installation
|
||||
|
||||
Unterstützte Geräte:
|
||||
|
||||
- Website (jeder moderne Browser auf jeder Plattform wie Chrome, Safari und Firefox) (einschließlich PWA-Unterstützung)
|
||||
- Windows (x64/ia32)
|
||||
- Portabel
|
||||
- MSI-Installationsprogramm
|
||||
- Chocolatey-Paketmanager
|
||||
- Linux (x64/ia32)
|
||||
- Portabel
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 ab v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Sponsoren
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Support
|
||||
|
||||
Wenn Sie Hilfe benötigen oder eine Funktion für Termix anfragen möchten, besuchen Sie die [Issues](https://github.com/Termix-SSH/Support/issues)-Seite, melden Sie sich an und klicken Sie auf `New Issue`.
|
||||
Bitte beschreiben Sie Ihr Anliegen so detailliert wie möglich, vorzugsweise auf Englisch. Sie können auch dem [Discord](https://discord.gg/jVQGdvHDrf)-Server beitreten und den Support-Kanal besuchen, allerdings können die Antwortzeiten dort länger sein.
|
||||
|
||||
# Screenshots
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
Einige Videos und Bilder können veraltet sein oder Funktionen möglicherweise nicht perfekt darstellen.
|
||||
|
||||
# Lizenz
|
||||
|
||||
Verteilt unter der Apache License Version 2.0. Siehe LICENSE für weitere Informationen.
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Logrado el 1 de septiembre de 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
Si lo desea, puede apoyar el proyecto aquí.\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Descripción General
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, capacidades de túneles SSH, gestión remota de archivos y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas.
|
||||
|
||||
# Características
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux
|
||||
- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard
|
||||
- **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 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.
|
||||
- **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 modo oscuro o claro. Use rutas URL para abrir cualquier conexión en pantalla completa.
|
||||
- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations))
|
||||
- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS), PWA y aplicación dedicada para móviles/tablets en iOS y Android.
|
||||
- **Herramientas SSH** - Cree fragmentos de comandos reutilizables que se ejecutan con un solo clic. Ejecute un comando simultáneamente en múltiples terminales abiertos.
|
||||
- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente
|
||||
- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión
|
||||
- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado
|
||||
- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
|
||||
- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado
|
||||
- **Pestañas Persistentes** - Las sesiones SSH y pestañas permanecen abiertas entre dispositivos/actualizaciones si está habilitado en el perfil de usuario
|
||||
|
||||
# Características Planeadas
|
||||
|
||||
Consulte [Proyectos](https://github.com/orgs/Termix-SSH/projects/2) para todas las características planeadas. Si desea contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Instalación
|
||||
|
||||
Dispositivos soportados:
|
||||
|
||||
- Sitio web (cualquier navegador moderno en cualquier plataforma como Chrome, Safari y Firefox) (incluye soporte PWA)
|
||||
- Windows (x64/ia32)
|
||||
- Portable
|
||||
- Instalador MSI
|
||||
- Gestor de paquetes Chocolatey
|
||||
- Linux (x64/ia32)
|
||||
- Portable
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 en v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Patrocinadores
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Soporte
|
||||
|
||||
Si necesita ayuda o desea solicitar una función para Termix, visite la página de [Issues](https://github.com/Termix-SSH/Support/issues), inicie sesión y pulse `New Issue`.
|
||||
Por favor, sea lo más detallado posible en su reporte, preferiblemente escrito en inglés. También puede unirse al servidor de [Discord](https://discord.gg/jVQGdvHDrf) y visitar el canal de soporte, sin embargo, los tiempos de respuesta pueden ser más largos.
|
||||
|
||||
# Capturas de Pantalla
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
Algunos videos e imágenes pueden estar desactualizados o no mostrar perfectamente las características.
|
||||
|
||||
# Licencia
|
||||
|
||||
Distribuido bajo la Licencia Apache Versión 2.0. Consulte LICENSE para más información.
|
||||
@@ -0,0 +1,204 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Obtenu le 1er septembre 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<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 !\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Présentation
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, des capacités de tunneling SSH, la gestion de fichiers à distance, et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes.
|
||||
|
||||
# Fonctionnalités
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux
|
||||
- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord
|
||||
- **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 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
|
||||
- **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 entre un thème sombre ou clair. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran
|
||||
- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations))
|
||||
- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS), PWA, et application mobile/tablette dédiée pour iOS et Android
|
||||
- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts
|
||||
- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées
|
||||
- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion
|
||||
- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier
|
||||
- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
|
||||
- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts
|
||||
- **Onglets Persistants** - Les sessions SSH et les onglets restent ouverts sur tous les appareils/actualisations si activé dans le profil utilisateur
|
||||
|
||||
# Fonctionnalités prévues
|
||||
|
||||
Consultez les [Projects](https://github.com/orgs/Termix-SSH/projects/2) pour toutes les fonctionnalités prévues. Si vous souhaitez contribuer, consultez [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Installation
|
||||
|
||||
Appareils supportés :
|
||||
|
||||
- Site web (tout navigateur moderne sur toute plateforme comme Chrome, Safari et Firefox) (support PWA inclus)
|
||||
- Windows (x64/ia32)
|
||||
- Portable
|
||||
- Installateur MSI
|
||||
- Gestionnaire de paquets Chocolatey
|
||||
- Linux (x64/ia32)
|
||||
- Portable
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 sur v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose :
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Sponsors
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Support
|
||||
|
||||
Si vous avez besoin d'aide ou souhaitez demander une fonctionnalité pour Termix, visitez la page [Issues](https://github.com/Termix-SSH/Support/issues), connectez-vous et appuyez sur `New Issue`. Veuillez être aussi détaillé que possible dans votre issue, de préférence rédigée en anglais. Vous pouvez également rejoindre le serveur [Discord](https://discord.gg/jVQGdvHDrf) et visiter le canal de support, cependant les temps de réponse peuvent être plus longs.
|
||||
|
||||
# Captures d'écran
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
Certaines vidéos et images peuvent être obsolètes ou ne pas présenter parfaitement les fonctionnalités.
|
||||
|
||||
# Licence
|
||||
|
||||
Distribué sous la licence Apache Version 2.0. Consultez LICENSE pour plus d'informations.
|
||||
@@ -0,0 +1,205 @@
|
||||
# रिपॉजिटरी आँकड़े
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">1 सितंबर, 2025 को प्राप्त</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
यदि आप चाहें, तो आप यहाँ प्रोजेक्ट को सपोर्ट कर सकते हैं!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# अवलोकन
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, SSH टनलिंग क्षमताएँ, रिमोट फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है।
|
||||
|
||||
# विशेषताएँ
|
||||
|
||||
- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है
|
||||
- **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ
|
||||
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ
|
||||
- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें
|
||||
- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है
|
||||
- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें
|
||||
- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें
|
||||
- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें
|
||||
- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें
|
||||
- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें
|
||||
- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें
|
||||
- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें
|
||||
- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन
|
||||
- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। डार्क या लाइट मोड UI के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें
|
||||
- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित)
|
||||
- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध
|
||||
- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ
|
||||
- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य
|
||||
- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें
|
||||
- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें
|
||||
- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh) आदि का सपोर्ट
|
||||
- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें
|
||||
- **परसिस्टेंट टैब** - उपयोगकर्ता प्रोफ़ाइल में सक्षम होने पर SSH सेशन और टैब डिवाइस/रीफ्रेश के पार खुले रहते हैं
|
||||
|
||||
# नियोजित विशेषताएँ
|
||||
|
||||
सभी नियोजित विशेषताओं के लिए [प्रोजेक्ट्स](https://github.com/orgs/Termix-SSH/projects/2) देखें। यदि आप योगदान देना चाहते हैं, तो [योगदान](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) देखें।
|
||||
|
||||
# इंस्टॉलेशन
|
||||
|
||||
समर्थित डिवाइस:
|
||||
|
||||
- वेबसाइट (किसी भी प्लेटफ़ॉर्म पर कोई भी आधुनिक ब्राउज़र जैसे Chrome, Safari, और Firefox) (PWA सपोर्ट सहित)
|
||||
- Windows (x64/ia32)
|
||||
- पोर्टेबल
|
||||
- MSI इंस्टॉलर
|
||||
- Chocolatey पैकेज मैनेजर
|
||||
- Linux (x64/ia32)
|
||||
- पोर्टेबल
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (v12.0+ पर x64/ia32)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# प्रायोजक
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# सहायता
|
||||
|
||||
यदि आपको सहायता चाहिए या Termix के लिए किसी विशेषता का अनुरोध करना चाहते हैं, तो [इश्यूज़](https://github.com/Termix-SSH/Support/issues) पेज पर जाएँ, लॉग इन करें, और `New Issue` दबाएँ।
|
||||
कृपया अपने इश्यू में यथासंभव विस्तृत विवरण दें, अधिमानतः अंग्रेज़ी में लिखें। आप [Discord](https://discord.gg/jVQGdvHDrf) सर्वर में भी शामिल हो सकते हैं और सहायता चैनल पर जा सकते हैं, हालाँकि, प्रतिक्रिया समय अधिक हो सकता है।
|
||||
|
||||
# स्क्रीनशॉट
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
कुछ वीडियो और छवियाँ पुरानी हो सकती हैं या विशेषताओं को पूरी तरह से प्रदर्शित नहीं कर सकती हैं।
|
||||
|
||||
# लाइसेंस
|
||||
|
||||
Apache License Version 2.0 के तहत वितरित। अधिक जानकारी के लिए LICENSE देखें।
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Ottenuto il 1 settembre 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
Se lo desideri, puoi supportare il progetto qui!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Panoramica
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, funzionalità di tunneling SSH, gestione remota dei file e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme.
|
||||
|
||||
# Funzionalità
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux
|
||||
- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard
|
||||
- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli
|
||||
- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC 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.
|
||||
- **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 modalità scura o chiara. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero.
|
||||
- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations))
|
||||
- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS), PWA e app dedicata per mobile/tablet su iOS e Android.
|
||||
- **Strumenti SSH** - Crea snippet di comandi riutilizzabili che si eseguono con un singolo clic. Esegui un comando simultaneamente su più terminali aperti.
|
||||
- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza
|
||||
- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione
|
||||
- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera
|
||||
- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), ecc.
|
||||
- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato
|
||||
- **Schede Persistenti** - Le sessioni SSH e le schede rimangono aperte tra dispositivi/aggiornamenti se abilitato nel profilo utente
|
||||
|
||||
# Funzionalità Pianificate
|
||||
|
||||
Consulta [Progetti](https://github.com/orgs/Termix-SSH/projects/2) per tutte le funzionalità pianificate. Se desideri contribuire, consulta [Contribuire](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Installazione
|
||||
|
||||
Dispositivi Supportati:
|
||||
|
||||
- Sito web (qualsiasi browser moderno su qualsiasi piattaforma come Chrome, Safari e Firefox) (include supporto PWA)
|
||||
- Windows (x64/ia32)
|
||||
- Portable
|
||||
- MSI Installer
|
||||
- Chocolatey Package Manager
|
||||
- Linux (x64/ia32)
|
||||
- Portable
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 su v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Sponsor
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Supporto
|
||||
|
||||
Se hai bIPAgno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`.
|
||||
Per favore, sii il più dettagliato possibile nella tua segnalazione, preferibilmente scritta in inglese. Puoi anche unirti al server [Discord](https://discord.gg/jVQGdvHDrf) e visitare il canale di supporto, tuttavia i tempi di risposta potrebbero essere più lunghi.
|
||||
|
||||
# Screenshot
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
Alcuni video e immagini potrebbero non essere aggiornati o potrebbero non mostrare perfettamente le funzionalità.
|
||||
|
||||
# Licenza
|
||||
|
||||
Distribuito sotto la Licenza Apache Versione 2.0. Consulta LICENSE per maggiori informazioni.
|
||||
@@ -0,0 +1,204 @@
|
||||
# リポジトリ統計
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">2025年9月1日に達成</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
プロジェクトを支援していただける方はこちらからどうぞ!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# 概要
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、SSHトンネリング機能、リモートファイル管理、その他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。
|
||||
|
||||
# 機能
|
||||
|
||||
- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応
|
||||
- **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応
|
||||
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応
|
||||
- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行
|
||||
- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易管理を目的としています
|
||||
- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化
|
||||
- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示
|
||||
- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認
|
||||
- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有
|
||||
- **ユーザー認証** - 管理者コントロールとOIDCおよび2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携
|
||||
- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください
|
||||
- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポート
|
||||
- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理
|
||||
- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ダーク/ライトモードの切り替え対応。URLルートで任意の接続をフルスクリーンで開くことが可能
|
||||
- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理)
|
||||
- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能
|
||||
- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行
|
||||
- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示
|
||||
- **クイック接続** - 接続データを保存せずにサーバーに接続
|
||||
- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセス
|
||||
- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)などに対応
|
||||
- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化
|
||||
- **永続タブ** - ユーザープロフィールで有効にすると、SSHセッションとタブがデバイス/更新をまたいで開いたまま保持されます
|
||||
|
||||
# 予定されている機能
|
||||
|
||||
すべての予定機能については[Projects](https://github.com/orgs/Termix-SSH/projects/2)をご覧ください。コントリビュートをご希望の方は[Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)をご覧ください。
|
||||
|
||||
# インストール
|
||||
|
||||
対応デバイス:
|
||||
|
||||
- Webサイト(Chrome、Safari、Firefoxなど、あらゆるプラットフォームのモダンブラウザ)(PWA対応)
|
||||
- Windows (x64/ia32)
|
||||
- ポータブル版
|
||||
- MSIインストーラー
|
||||
- Chocolateyパッケージマネージャー
|
||||
- Linux (x64/ia32)
|
||||
- ポータブル版
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32、v12.0以降)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1以降)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0以降)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。以下はDocker Composeファイルのサンプルです:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# スポンサー
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# サポート
|
||||
|
||||
Termixに関するヘルプや機能リクエストが必要な場合は、[Issues](https://github.com/Termix-SSH/Support/issues)ページにアクセスし、ログインして`New Issue`を押してください。Issueはできるだけ詳細に記述し、英語での記述が望ましいです。また、[Discord](https://discord.gg/jVQGdvHDrf)サーバーに参加してサポートチャンネルを利用することもできますが、応答時間が長くなる場合があります。
|
||||
|
||||
# スクリーンショット
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
一部の動画や画像は古い場合や、機能を完全に紹介していない場合があります。
|
||||
|
||||
# ライセンス
|
||||
|
||||
Apache License Version 2.0のもとで配布されています。詳細はLICENSEをご覧ください。
|
||||
@@ -0,0 +1,204 @@
|
||||
# 리포지토리 통계
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">2025년 9월 1일 달성</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
프로젝트를 후원하고 싶으시다면 여기에서 지원해 주세요!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# 개요
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, SSH 터널링 기능, 원격 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다.
|
||||
|
||||
# 기능
|
||||
|
||||
- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원
|
||||
- **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원
|
||||
- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원
|
||||
- **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행
|
||||
- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다
|
||||
- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화
|
||||
- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시
|
||||
- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인
|
||||
- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유
|
||||
- **사용자 인증** - 관리자 제어와 OIDC 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동
|
||||
- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요
|
||||
- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기
|
||||
- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리
|
||||
- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 다크 또는 라이트 모드 기반 UI 선택. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능
|
||||
- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리)
|
||||
- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공
|
||||
- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행
|
||||
- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회
|
||||
- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속
|
||||
- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근
|
||||
- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh) 등 지원
|
||||
- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화
|
||||
- **지속 탭** - 사용자 프로필에서 활성화된 경우 SSH 세션 및 탭이 기기/새로 고침 간에 열린 상태 유지
|
||||
|
||||
# 계획된 기능
|
||||
|
||||
모든 계획된 기능은 [Projects](https://github.com/orgs/Termix-SSH/projects/2)를 참조하세요. 기여를 원하시면 [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)을 참조하세요.
|
||||
|
||||
# 설치
|
||||
|
||||
지원 기기:
|
||||
|
||||
- 웹사이트 (Chrome, Safari, Firefox 등 모든 플랫폼의 최신 브라우저) (PWA 지원 포함)
|
||||
- Windows (x64/ia32)
|
||||
- 포터블
|
||||
- MSI 설치 프로그램
|
||||
- Chocolatey 패키지 관리자
|
||||
- Linux (x64/ia32)
|
||||
- 포터블
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32, v12.0 이상)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1 이상)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0 이상)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# 스폰서
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 지원
|
||||
|
||||
Termix에 대한 도움이 필요하거나 기능을 요청하려면 [Issues](https://github.com/Termix-SSH/Support/issues) 페이지를 방문하여 로그인하고 `New Issue`를 누르세요. 이슈는 가능한 한 상세하게 작성하고, 영어로 작성하는 것이 좋습니다. [Discord](https://discord.gg/jVQGdvHDrf) 서버에 참여하여 지원 채널을 이용할 수도 있지만, 응답 시간이 더 길 수 있습니다.
|
||||
|
||||
# 스크린샷
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
일부 비디오 및 이미지는 최신이 아니거나 기능을 완벽하게 보여주지 않을 수 있습니다.
|
||||
|
||||
# 라이선스
|
||||
|
||||
Apache License Version 2.0에 따라 배포됩니다. 자세한 내용은 LICENSE를 참조하세요.
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Conquistado em 1 de setembro de 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
Se desejar, você pode apoiar o projeto aqui!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Visão Geral
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, capacidades de tunelamento SSH, gerenciamento remoto de arquivos e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas.
|
||||
|
||||
# Funcionalidades
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Estatísticas do Servidor** - Visualize o uso de CPU, memória e disco junto com rede, tempo de atividade, informações do sistema, firewall, monitor de portas na maioria dos servidores baseados em Linux
|
||||
- **Dashboard** - Visualize informações do servidor de relance no seu dashboard
|
||||
- **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 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.
|
||||
- **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 modo escuro ou claro. Use rotas de URL para abrir qualquer conexão em tela cheia.
|
||||
- **Idiomas** - Suporte integrado para ~30 idiomas (gerenciado pelo [Crowdin](https://docs.termix.site/translations))
|
||||
- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS), PWA e aplicativo dedicado para celular/tablet para iOS e Android.
|
||||
- **Ferramentas SSH** - Crie trechos de comandos reutilizáveis que são executados com um único clique. Execute um comando simultaneamente em múltiplos terminais abertos.
|
||||
- **Histórico de Comandos** - Autocompletar e visualizar comandos SSH executados anteriormente
|
||||
- **Conexão Rápida** - Conecte-se a um servidor sem precisar salvar os dados de conexão
|
||||
- **Paleta de Comandos** - Pressione duas vezes a tecla Shift esquerda para acessar rapidamente as conexões SSH com seu teclado
|
||||
- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
|
||||
- **Gráfico de Rede** - Personalize seu Dashboard para visualizar seu homelab baseado nas suas conexões SSH com suporte de status
|
||||
- **Abas Persistentes** - Sessões SSH e abas permanecem abertas entre dispositivos/atualizações se habilitado no perfil do usuário
|
||||
|
||||
# Funcionalidades Planejadas
|
||||
|
||||
Consulte [Projetos](https://github.com/orgs/Termix-SSH/projects/2) para todas as funcionalidades planejadas. Se você deseja contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Instalação
|
||||
|
||||
Dispositivos suportados:
|
||||
|
||||
- Website (qualquer navegador moderno em qualquer plataforma como Chrome, Safari e Firefox) (inclui suporte PWA)
|
||||
- Windows (x64/ia32)
|
||||
- Portátil
|
||||
- Instalador MSI
|
||||
- Gerenciador de pacotes Chocolatey
|
||||
- Linux (x64/ia32)
|
||||
- Portátil
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 em v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Patrocinadores
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Suporte
|
||||
|
||||
Se você precisa de ajuda ou deseja solicitar uma funcionalidade para o Termix, visite a página de [Issues](https://github.com/Termix-SSH/Support/issues), faça login e clique em `New Issue`.
|
||||
Por favor, seja o mais detalhado possível no seu relato, preferencialmente escrito em inglês. Você também pode entrar no servidor do [Discord](https://discord.gg/jVQGdvHDrf) e visitar o canal de suporte, porém, os tempos de resposta podem ser mais longos.
|
||||
|
||||
# Capturas de Tela
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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.
|
||||
|
||||
# Licença
|
||||
|
||||
Distribuído sob a Licença Apache Versão 2.0. Consulte LICENSE para mais informações.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Статистика репозитория
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Достигнуто 1 сентября 2025 года</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
Если хотите, вы можете поддержать проект здесь!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Обзор
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, возможности SSH-туннелирования, удалённое управление файлами и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ.
|
||||
|
||||
# Возможности
|
||||
|
||||
- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты
|
||||
- **Доступ к удалённому рабочему столу** — Поддержка RDP, VNC и Telnet через браузер с полной настройкой и разделением экрана
|
||||
- **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r
|
||||
- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo
|
||||
- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием
|
||||
- **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей
|
||||
- **Статистика сервера** — Просмотр использования CPU, памяти и диска, а также сети, времени работы, информации о системе, файрвола и монитора портов на большинстве серверов на базе Linux
|
||||
- **Панель управления** — Просмотр информации о сервере на панели управления одним взглядом
|
||||
- **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей
|
||||
- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов
|
||||
- **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security)
|
||||
- **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера
|
||||
- **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS
|
||||
- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между тёмной и светлой темой. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме
|
||||
- **Языки** — Встроенная поддержка ~30 языков (управляется через [Crowdin](https://docs.termix.site/translations))
|
||||
- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS), PWA и специализированное мобильное/планшетное приложение для iOS и Android
|
||||
- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах
|
||||
- **История команд** — Автодополнение и просмотр ранее выполненных SSH-команд
|
||||
- **Быстрое подключение** — Подключение к серверу без необходимости сохранения данных подключения
|
||||
- **Командная палитра** — Двойное нажатие левого Shift для быстрого доступа к SSH-подключениям с клавиатуры
|
||||
- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh) и др.
|
||||
- **Сетевой граф** — Настройте панель управления для визуализации вашей домашней лаборатории на основе SSH-подключений с поддержкой статусов
|
||||
- **Постоянные вкладки** — SSH-сессии и вкладки остаются открытыми на всех устройствах/при обновлении страницы, если включено в профиле пользователя
|
||||
|
||||
# Запланированные функции
|
||||
|
||||
Смотрите [Проекты](https://github.com/orgs/Termix-SSH/projects/2) для просмотра всех запланированных функций. Если вы хотите внести вклад, смотрите [Участие в разработке](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Установка
|
||||
|
||||
Поддерживаемые устройства:
|
||||
|
||||
- Веб-сайт (любой современный браузер на любой платформе, включая Chrome, Safari и Firefox) (включая поддержку PWA)
|
||||
- Windows (x64/ia32)
|
||||
- Портативная версия
|
||||
- Установщик MSI
|
||||
- Менеджер пакетов Chocolatey
|
||||
- Linux (x64/ia32)
|
||||
- Портативная версия
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32, версия 12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (версия 15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (версия 7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Спонсоры
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Поддержка
|
||||
|
||||
Если вам нужна помощь или вы хотите запросить новую функцию для Termix, посетите страницу [Проблемы](https://github.com/Termix-SSH/Support/issues), войдите в систему и нажмите `New Issue`.
|
||||
Пожалуйста, опишите вашу проблему как можно подробнее, предпочтительно на английском языке. Вы также можете присоединиться к серверу [Discord](https://discord.gg/jVQGdvHDrf) и обратиться в канал поддержки, однако время ответа может быть дольше.
|
||||
|
||||
# Скриншоты
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность.
|
||||
|
||||
# Лицензия
|
||||
|
||||
Распространяется по лицензии Apache License Version 2.0. Подробнее см. в файле LICENSE.
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">1 Eylül 2025'te kazanıldı</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
|
||||
</p>
|
||||
|
||||
Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Genel Bakış
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, SSH tünelleme yetenekleri, uzak dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir.
|
||||
|
||||
# Özellikler
|
||||
|
||||
- **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
|
||||
- **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
|
||||
- **Sunucu İstatistikleri** - Çoğu Linux tabanlı sunucularda CPU, bellek ve disk kullanımını ağ, çalışma süresi, sistem bilgisi, güvenlik duvarı, port izleme ile birlikte görüntüleyin
|
||||
- **Kontrol Paneli** - Kontrol panelinizde sunucu bilgilerini bir bakışta görüntüleyin
|
||||
- **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın
|
||||
- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC 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.
|
||||
- **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. Karanlık veya açık tema arasında seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın.
|
||||
- **Diller** - ~30 dil için yerleşik destek ([Crowdin](https://docs.termix.site/translations) tarafından yönetilir)
|
||||
- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir.
|
||||
- **SSH Araçları** - Tek tıklamayla çalıştırılan yeniden kullanılabilir komut parçacıkları oluşturun. Birden fazla açık terminalde aynı anda tek bir komut çalıştırın.
|
||||
- **Komut Geçmişi** - Daha önce çalıştırılan SSH komutlarını otomatik tamamlayın ve görüntüleyin
|
||||
- **Hızlı Bağlantı** - Bağlantı verilerini kaydetmeden bir sunucuya bağlanın
|
||||
- **Komut Paleti** - Sol shift tuşuna iki kez basarak SSH bağlantılarına klavyenizle hızlıca erişin
|
||||
- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh) vb. destekler.
|
||||
- **Ağ Grafiği** - Kontrol panelinizi, SSH bağlantılarınıza dayalı olarak ev laboratuvarınızı durum desteğiyle görselleştirmek için özelleştirin
|
||||
- **Kalıcı Sekmeler** - Kullanıcı profilinde etkinleştirilmişse SSH oturumları ve sekmeler cihazlar/yenilemeler arasında açık kalır
|
||||
|
||||
# Planlanan Özellikler
|
||||
|
||||
Tüm planlanan özellikler için [Projeler](https://github.com/orgs/Termix-SSH/projects/2) sayfasına bakın. Katkıda bulunmak istiyorsanız, [Katkıda Bulunma](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) sayfasına bakın.
|
||||
|
||||
# Kurulum
|
||||
|
||||
Desteklenen Cihazlar:
|
||||
|
||||
- Web sitesi (Chrome, Safari ve Firefox gibi herhangi bir platformda herhangi bir modern tarayıcı) (PWA desteği dahil)
|
||||
- Windows (x64/ia32)
|
||||
- Taşınabilir
|
||||
- MSI Yükleyici
|
||||
- Chocolatey Paket Yöneticisi
|
||||
- Linux (x64/ia32)
|
||||
- Taşınabilir
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (v12.0+ üzerinde x64/ia32)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Sponsorlar
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Destek
|
||||
|
||||
Termix ile ilgili yardıma ihtiyacınız varsa veya bir özellik talep etmek istiyorsanız, [Sorunlar](https://github.com/Termix-SSH/Support/issues) sayfasını ziyaret edin, giriş yapın ve `New Issue` butonuna basın.
|
||||
Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce olarak. Ayrıca [Discord](https://discord.gg/jVQGdvHDrf) sunucusuna katılabilir ve destek kanalını ziyaret edebilirsiniz, ancak yanıt süreleri daha uzun olabilir.
|
||||
|
||||
# Ekran Görüntüleri
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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.
|
||||
|
||||
# Lisans
|
||||
|
||||
Apache Lisansı Sürüm 2.0 altında dağıtılmaktadır. Daha fazla bilgi için LICENSE dosyasına bakın.
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
|
||||
<br>
|
||||
<small style="color: #666;">Đạt được vào ngày 1 tháng 9 năm 2025</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<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!\
|
||||
[](https://github.com/sponsors/LukeGus)
|
||||
|
||||
# Tổng Quan
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Termix-SSH/Termix">
|
||||
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||
</p>
|
||||
|
||||
Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, khả năng tạo đường hầm SSH, quản lý tệp từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng.
|
||||
|
||||
# Tính Năng
|
||||
|
||||
- **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 và giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r
|
||||
- **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
|
||||
- **Thống Kê Máy Chủ** - Xem mức sử dụng CPU, bộ nhớ và ổ đĩa cùng với mạng, thời gian hoạt động, thông tin hệ thống, tường lửa, giám sát cổng trên hầu hết các máy chủ chạy Linux
|
||||
- **Bảng Điều Khiển** - Xem thông tin máy chủ trong nháy mắt trên bảng điều khiển của bạn
|
||||
- **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 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.
|
||||
- **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 giao diện chế độ tối hoặc sáng. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình.
|
||||
- **Ngôn Ngữ** - Hỗ trợ tích hợp ~30 ngôn ngữ (được quản lý bởi [Crowdin](https://docs.termix.site/translations))
|
||||
- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android.
|
||||
- **Công Cụ SSH** - Tạo đoạn lệnh có thể tái sử dụng, thực thi chỉ với một cú nhấp chuột. Chạy một lệnh đồng thời trên nhiều terminal đang mở.
|
||||
- **Lịch Sử Lệnh** - Tự động hoàn thành và xem các lệnh SSH đã chạy trước đó
|
||||
- **Kết Nối Nhanh** - Kết nối đến máy chủ mà không cần lưu dữ liệu kết nối
|
||||
- **Bảng Lệnh** - Nhấn đúp phím shift trái để truy cập nhanh các kết nối SSH bằng bàn phím
|
||||
- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), v.v.
|
||||
- **Biểu Đồ Mạng** - Tùy chỉnh Bảng Điều Khiển để trực quan hóa homelab của bạn dựa trên các kết nối SSH với hỗ trợ trạng thái
|
||||
- **Tab Liên Tục** - Các phiên SSH và tab vẫn mở trên các thiết bị/lần làm mới nếu được bật trong hồ sơ người dùng
|
||||
|
||||
# Tính Năng Dự Kiến
|
||||
|
||||
Xem [Dự Án](https://github.com/orgs/Termix-SSH/projects/2) để biết tất cả các tính năng dự kiến. Nếu bạn muốn đóng góp, xem [Đóng Góp](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Cài Đặt
|
||||
|
||||
Thiết Bị Được Hỗ Trợ:
|
||||
|
||||
- Trang web (bất kỳ trình duyệt hiện đại nào trên bất kỳ nền tảng nào như Chrome, Safari và Firefox) (bao gồm hỗ trợ PWA)
|
||||
- Windows (x64/ia32)
|
||||
- Portable
|
||||
- MSI Installer
|
||||
- Chocolatey Package Manager
|
||||
- Linux (x64/ia32)
|
||||
- Portable
|
||||
- AUR
|
||||
- AppImage
|
||||
- Deb
|
||||
- Flatpak
|
||||
- macOS (x64/ia32 trên v12.0+)
|
||||
- Apple App Store
|
||||
- DMG
|
||||
- Homebrew
|
||||
- iOS/iPadOS (v15.1+)
|
||||
- Apple App Store
|
||||
- IPA
|
||||
- Android (v7.0+)
|
||||
- Google Play Store
|
||||
- APK
|
||||
|
||||
Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
termix:
|
||||
image: ghcr.io/lukegus/termix:latest
|
||||
container_name: termix
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- termix-data:/app/data
|
||||
environment:
|
||||
PORT: "8080"
|
||||
depends_on:
|
||||
- guacd
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4822:4822"
|
||||
networks:
|
||||
- termix-net
|
||||
|
||||
volumes:
|
||||
termix-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
termix-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
# Nhà Tài Trợ
|
||||
|
||||
<p align="left">
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
|
||||
</a>
|
||||
|
||||
<a href="https://crowdin.com/">
|
||||
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.blacksmith.sh/">
|
||||
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
|
||||
</a>
|
||||
|
||||
<a href="https://www.cloudflare.com/">
|
||||
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Hỗ Trợ
|
||||
|
||||
Nếu bạn cần trợ giúp hoặc muốn yêu cầu tính năng với Termix, hãy truy cập trang [Vấn Đề](https://github.com/Termix-SSH/Support/issues), đăng nhập và nhấn `New Issue`.
|
||||
Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết bằng tiếng Anh. Bạn cũng có thể tham gia máy chủ [Discord](https://discord.gg/jVQGdvHDrf) và truy cập kênh hỗ trợ, tuy nhiên thời gian phản hồi có thể lâu hơn.
|
||||
|
||||
# Ảnh Chụp Màn Hình
|
||||
|
||||
[](https://www.youtube.com/@TermixSSH/videos)
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
|
||||
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
|
||||
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
|
||||
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
|
||||
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
|
||||
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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.
|
||||
|
||||
# Giấy Phép
|
||||
|
||||
Được phân phối theo Giấy Phép Apache Phiên Bản 2.0. Xem LICENSE để biết thêm thông tin.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 418 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB |
+14
-10
@@ -4,11 +4,11 @@ import cookieParser from "cookie-parser";
|
||||
import { getDb, DatabaseSaveTrigger } from "./database/db/index.js";
|
||||
import {
|
||||
recentActivity,
|
||||
sshData,
|
||||
hosts,
|
||||
hostAccess,
|
||||
dashboardPreferences,
|
||||
} from "./database/db/schema.js";
|
||||
import { eq, and, desc, or, sql } from "drizzle-orm";
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
import { dashboardLogger } from "./utils/logger.js";
|
||||
import { SimpleDBOps } from "./utils/simple-db-ops.js";
|
||||
import { AuthManager } from "./utils/auth-manager.js";
|
||||
@@ -55,6 +55,10 @@ app.use(
|
||||
);
|
||||
app.use(cookieParser());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(authManager.createAuthMiddleware());
|
||||
|
||||
@@ -85,9 +89,6 @@ app.use(authManager.createAuthMiddleware());
|
||||
*/
|
||||
app.get("/uptime", async (req, res) => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
const uptimeMs = Date.now() - serverStartTime;
|
||||
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
||||
const days = Math.floor(uptimeSeconds / 86400);
|
||||
@@ -175,7 +176,7 @@ app.get("/activity/recent", async (req, res) => {
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [terminal, file_manager, server_stats, tunnel, docker]
|
||||
* enum: [terminal, file_manager, server_stats, tunnel, docker, telnet, vnc, rdp]
|
||||
* hostId:
|
||||
* type: integer
|
||||
* hostName:
|
||||
@@ -218,11 +219,14 @@ app.post("/activity/log", async (req, res) => {
|
||||
"server_stats",
|
||||
"tunnel",
|
||||
"docker",
|
||||
"telnet",
|
||||
"vnc",
|
||||
"rdp",
|
||||
].includes(type)
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
"Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', or 'docker'",
|
||||
"Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', 'docker', 'telnet', 'vnc', or 'rdp'",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,8 +255,8 @@ app.post("/activity/log", async (req, res) => {
|
||||
const ownedHosts = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
@@ -296,7 +300,7 @@ app.post("/activity/log", async (req, res) => {
|
||||
|
||||
if (allActivities.length > 100) {
|
||||
const toDelete = allActivities.slice(100);
|
||||
for (const activity of toDelete) {
|
||||
for (let i = 0; i < toDelete.length; i++) {
|
||||
await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import bodyParser from "body-parser";
|
||||
import multer from "multer";
|
||||
import cookieParser from "cookie-parser";
|
||||
import userRoutes from "./routes/users.js";
|
||||
import sshRoutes from "./routes/ssh.js";
|
||||
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 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 cors from "cors";
|
||||
@@ -28,7 +29,7 @@ import { parseUserAgent } from "../utils/user-agent-parser.js";
|
||||
import { getProxyAgent } from "../utils/proxy-agent.js";
|
||||
import {
|
||||
users,
|
||||
sshData,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
@@ -45,6 +46,10 @@ import type {
|
||||
} from "../../types/index.js";
|
||||
import { getDb, DatabaseSaveTrigger } from "./db/index.js";
|
||||
import Database from "better-sqlite3";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -203,6 +208,10 @@ app.use(bodyParser.json({ limit: "1gb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "1gb", extended: true }));
|
||||
app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" }));
|
||||
app.use(cookieParser());
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
@@ -595,21 +604,41 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
error: "Password required for export",
|
||||
code: "PASSWORD_REQUIRED",
|
||||
});
|
||||
}
|
||||
const deviceInfo = parseUserAgent(req);
|
||||
const unlocked = await authManager.authenticateUser(
|
||||
userId,
|
||||
password,
|
||||
deviceInfo.type,
|
||||
);
|
||||
if (!unlocked) {
|
||||
return res.status(401).json({ error: "Invalid password" });
|
||||
|
||||
const user = await getDb().select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
const isOidcUser = !!user[0].isOidc;
|
||||
|
||||
if (!isOidcUser) {
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
error: "Password required for export",
|
||||
code: "PASSWORD_REQUIRED",
|
||||
});
|
||||
}
|
||||
|
||||
const unlocked = await authManager.authenticateUser(
|
||||
userId,
|
||||
password,
|
||||
deviceInfo.type,
|
||||
);
|
||||
if (!unlocked) {
|
||||
return res.status(401).json({ error: "Invalid password" });
|
||||
}
|
||||
} else if (!DataCrypto.getUserDataKey(userId)) {
|
||||
const oidcUnlocked = await authManager.authenticateOIDCUser(
|
||||
userId,
|
||||
deviceInfo.type,
|
||||
);
|
||||
if (!oidcUnlocked) {
|
||||
return res.status(403).json({
|
||||
error: "Failed to unlock user data with SSO credentials",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
apiLogger.info("Exporting user data as SQLite", {
|
||||
@@ -622,11 +651,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
throw new Error("User data not unlocked");
|
||||
}
|
||||
|
||||
const user = await getDb().select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0) {
|
||||
throw new Error(`User not found: ${userId}`);
|
||||
}
|
||||
|
||||
const tempDir =
|
||||
process.env.NODE_ENV === "production"
|
||||
? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports")
|
||||
@@ -805,26 +829,26 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
userRecord.id,
|
||||
userRecord.username,
|
||||
"[EXPORTED_USER_NO_PASSWORD]",
|
||||
userRecord.is_admin ? 1 : 0,
|
||||
userRecord.is_oidc ? 1 : 0,
|
||||
userRecord.oidc_identifier || null,
|
||||
userRecord.client_id || null,
|
||||
userRecord.client_secret || null,
|
||||
userRecord.issuer_url || null,
|
||||
userRecord.authorization_url || null,
|
||||
userRecord.token_url || null,
|
||||
userRecord.identifier_path || null,
|
||||
userRecord.name_path || null,
|
||||
userRecord.isAdmin ? 1 : 0,
|
||||
userRecord.isOidc ? 1 : 0,
|
||||
userRecord.oidcIdentifier || null,
|
||||
userRecord.clientId || null,
|
||||
userRecord.clientSecret || null,
|
||||
userRecord.issuerUrl || null,
|
||||
userRecord.authorizationUrl || null,
|
||||
userRecord.tokenUrl || null,
|
||||
userRecord.identifierPath || null,
|
||||
userRecord.namePath || null,
|
||||
userRecord.scopes || null,
|
||||
userRecord.totp_secret || null,
|
||||
userRecord.totp_enabled ? 1 : 0,
|
||||
userRecord.totp_backup_codes || null,
|
||||
userRecord.totpSecret || null,
|
||||
userRecord.totpEnabled ? 1 : 0,
|
||||
userRecord.totpBackupCodes || null,
|
||||
);
|
||||
|
||||
const sshHosts = await getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.userId, userId));
|
||||
.from(hosts)
|
||||
.where(eq(hosts.userId, userId));
|
||||
const insertHost = exportDb.prepare(`
|
||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -851,31 +875,31 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
decrypted.forceKeyboardInteractive || null,
|
||||
decrypted.password || null,
|
||||
decrypted.key || null,
|
||||
decrypted.key_password || null,
|
||||
decrypted.keyPassword || null,
|
||||
decrypted.keyType || null,
|
||||
decrypted.sudoPassword || null,
|
||||
decrypted.autostartPassword || null,
|
||||
decrypted.autostartKey || null,
|
||||
decrypted.autostartKeyPassword || null,
|
||||
decrypted.credentialId || null,
|
||||
Boolean(decrypted.overrideCredentialUsername) ? 1 : 0,
|
||||
Boolean(decrypted.enableTerminal) ? 1 : 0,
|
||||
Boolean(decrypted.enableTunnel) ? 1 : 0,
|
||||
decrypted.overrideCredentialUsername ? 1 : 0,
|
||||
decrypted.enableTerminal ? 1 : 0,
|
||||
decrypted.enableTunnel ? 1 : 0,
|
||||
decrypted.tunnelConnections || null,
|
||||
decrypted.jumpHosts || null,
|
||||
Boolean(decrypted.enableFileManager) ? 1 : 0,
|
||||
Boolean(decrypted.enableDocker) ? 1 : 0,
|
||||
Boolean(decrypted.showTerminalInSidebar) ? 1 : 0,
|
||||
Boolean(decrypted.showFileManagerInSidebar) ? 1 : 0,
|
||||
Boolean(decrypted.showTunnelInSidebar) ? 1 : 0,
|
||||
Boolean(decrypted.showDockerInSidebar) ? 1 : 0,
|
||||
Boolean(decrypted.showServerStatsInSidebar) ? 1 : 0,
|
||||
decrypted.enableFileManager ? 1 : 0,
|
||||
decrypted.enableDocker ? 1 : 0,
|
||||
decrypted.showTerminalInSidebar ? 1 : 0,
|
||||
decrypted.showFileManagerInSidebar ? 1 : 0,
|
||||
decrypted.showTunnelInSidebar ? 1 : 0,
|
||||
decrypted.showDockerInSidebar ? 1 : 0,
|
||||
decrypted.showServerStatsInSidebar ? 1 : 0,
|
||||
decrypted.defaultPath || null,
|
||||
decrypted.statsConfig || null,
|
||||
decrypted.terminalConfig || null,
|
||||
decrypted.quickActions || null,
|
||||
decrypted.notes || null,
|
||||
Boolean(decrypted.useSocks5) ? 1 : 0,
|
||||
decrypted.useSocks5 ? 1 : 0,
|
||||
decrypted.socks5Host || null,
|
||||
decrypted.socks5Port || null,
|
||||
decrypted.socks5Username || null,
|
||||
@@ -913,9 +937,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
decrypted.username,
|
||||
decrypted.password || null,
|
||||
decrypted.key || null,
|
||||
decrypted.private_key || null,
|
||||
decrypted.public_key || null,
|
||||
decrypted.key_password || null,
|
||||
decrypted.privateKey || null,
|
||||
decrypted.publicKey || null,
|
||||
decrypted.keyPassword || null,
|
||||
decrypted.keyType || null,
|
||||
decrypted.detectedKeyType || null,
|
||||
decrypted.usageCount || 0,
|
||||
@@ -1135,7 +1159,7 @@ app.post(
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
const isOidcUser = !!userRecords[0].is_oidc;
|
||||
const isOidcUser = !!userRecords[0].isOidc;
|
||||
|
||||
if (!isOidcUser) {
|
||||
if (!password) {
|
||||
@@ -1244,13 +1268,13 @@ app.post(
|
||||
try {
|
||||
const existing = await mainDb
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.userId, userId),
|
||||
eq(sshData.ip, host.ip),
|
||||
eq(sshData.port, host.port),
|
||||
eq(sshData.username, host.username),
|
||||
eq(hosts.userId, userId),
|
||||
eq(hosts.ip, host.ip),
|
||||
eq(hosts.port, host.port),
|
||||
eq(hosts.username, host.username),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1318,7 +1342,7 @@ app.post(
|
||||
userId,
|
||||
userDataKey,
|
||||
);
|
||||
await mainDb.insert(sshData).values(encrypted);
|
||||
await mainDb.insert(hosts).values(encrypted);
|
||||
result.summary.sshHostsImported++;
|
||||
} catch (hostError) {
|
||||
result.summary.errors.push(
|
||||
@@ -1506,7 +1530,7 @@ app.post(
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId));
|
||||
if (targetUser.length > 0 && targetUser[0].is_admin) {
|
||||
if (targetUser.length > 0 && targetUser[0].isAdmin) {
|
||||
try {
|
||||
const importedSettings = importDb
|
||||
.prepare("SELECT * FROM settings")
|
||||
@@ -1736,15 +1760,42 @@ app.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
});
|
||||
|
||||
app.use("/users", userRoutes);
|
||||
app.use("/ssh", sshRoutes);
|
||||
app.use("/host", hostRoutes);
|
||||
app.use("/alerts", alertRoutes);
|
||||
app.use("/credentials", credentialsRoutes);
|
||||
app.use("/snippets", snippetsRoutes);
|
||||
app.use("/terminal", terminalRoutes);
|
||||
app.use("/guacamole", guacamoleRoutes);
|
||||
app.use("/network-topology", networkTopologyRoutes);
|
||||
app.use("/rbac", rbacRoutes);
|
||||
|
||||
const frontendDistPaths = [
|
||||
path.join(__dirname, "../../../dist"),
|
||||
path.join(__dirname, "../../dist"),
|
||||
path.join(process.cwd(), "dist"),
|
||||
];
|
||||
|
||||
const frontendDist = frontendDistPaths.find((p) =>
|
||||
fs.existsSync(path.join(p, "index.html")),
|
||||
);
|
||||
|
||||
if (frontendDist) {
|
||||
databaseLogger.info(`Serving frontend from: ${frontendDist}`, {
|
||||
operation: "static_files",
|
||||
});
|
||||
app.use(express.static(frontendDist));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.method === "GET" && req.accepts("html")) {
|
||||
res.sendFile(path.join(frontendDist, "index.html"));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.use(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(
|
||||
err: unknown,
|
||||
req: express.Request,
|
||||
@@ -1819,13 +1870,17 @@ app.get(
|
||||
if (status.hasUnencryptedDb) {
|
||||
try {
|
||||
unencryptedSize = fs.statSync(dbPath).size;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - file may not exist
|
||||
}
|
||||
}
|
||||
|
||||
if (status.hasEncryptedDb) {
|
||||
try {
|
||||
encryptedSize = fs.statSync(encryptedDbPath).size;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - file may not exist
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -37,10 +37,11 @@ async function initializeDatabaseAsync(): Promise<void> {
|
||||
memoryDatabase = new Database(decryptedBuffer);
|
||||
|
||||
try {
|
||||
const sessionCount = memoryDatabase
|
||||
memoryDatabase
|
||||
.prepare("SELECT COUNT(*) as count FROM sessions")
|
||||
.get() as { count: number };
|
||||
} catch (countError) {
|
||||
} catch {
|
||||
// expected - sessions table may not exist yet
|
||||
}
|
||||
} else {
|
||||
const migration = new DatabaseMigration(dataDir);
|
||||
@@ -182,6 +183,18 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trusted_devices (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
device_fingerprint TEXT NOT NULL,
|
||||
device_type TEXT NOT NULL,
|
||||
device_info TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT NOT NULL,
|
||||
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -414,9 +427,7 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
`);
|
||||
|
||||
try {
|
||||
sqlite
|
||||
.prepare("DELETE FROM sessions WHERE expires_at < datetime('now')")
|
||||
.run();
|
||||
sqlite.prepare("DELETE FROM sessions").run();
|
||||
} catch (e) {
|
||||
databaseLogger.warn("Could not clear expired sessions on startup", {
|
||||
operation: "db_init_session_cleanup_failed",
|
||||
@@ -461,6 +472,42 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const row = sqlite
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
|
||||
.get();
|
||||
if (!row) {
|
||||
sqlite
|
||||
.prepare(
|
||||
"INSERT INTO settings (key, value) VALUES ('guac_enabled', 'true')",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
} catch (e) {
|
||||
databaseLogger.warn("Could not initialize guac_enabled setting", {
|
||||
operation: "db_init",
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const row = sqlite
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get();
|
||||
if (!row) {
|
||||
sqlite
|
||||
.prepare(
|
||||
"INSERT INTO settings (key, value) VALUES ('guac_url', 'guacd:4822')",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
} catch (e) {
|
||||
databaseLogger.warn("Could not initialize guac_url setting", {
|
||||
operation: "db_init",
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addColumnIfNotExists = (
|
||||
@@ -578,6 +625,11 @@ const migrateSchema = () => {
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
||||
|
||||
addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"');
|
||||
addColumnIfNotExists("ssh_data", "domain", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "security", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
|
||||
addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "notes", "TEXT");
|
||||
|
||||
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
|
||||
@@ -750,6 +802,33 @@ const migrateSchema = () => {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
sqlite
|
||||
.prepare("SELECT id FROM trusted_devices LIMIT 1")
|
||||
.get();
|
||||
} catch {
|
||||
try {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS trusted_devices (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
device_fingerprint TEXT NOT NULL,
|
||||
device_type TEXT NOT NULL,
|
||||
device_info TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT NOT NULL,
|
||||
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
} catch (createError) {
|
||||
databaseLogger.warn("Failed to create trusted_devices table", {
|
||||
operation: "schema_migration",
|
||||
error: createError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
sqlite
|
||||
.prepare("SELECT id FROM network_topology LIMIT 1")
|
||||
@@ -1033,23 +1112,15 @@ const migrateSchema = () => {
|
||||
try {
|
||||
const validSystemRoles = ['admin', 'user'];
|
||||
const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member'];
|
||||
let deletedCount = 0;
|
||||
|
||||
const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?");
|
||||
for (const roleName of unwantedRoleNames) {
|
||||
const result = deleteByName.run(roleName);
|
||||
if (result.changes > 0) {
|
||||
deletedCount += result.changes;
|
||||
}
|
||||
deleteByName.run(roleName);
|
||||
}
|
||||
|
||||
const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1");
|
||||
for (const role of existingRoles) {
|
||||
if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) {
|
||||
const result = deleteOldSystemRole.run(role.name);
|
||||
if (result.changes > 0) {
|
||||
deletedCount += result.changes;
|
||||
}
|
||||
deleteOldSystemRole.run(role.name);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
@@ -1107,7 +1178,7 @@ const migrateSchema = () => {
|
||||
for (const admin of adminUsers) {
|
||||
try {
|
||||
insertUserRole.run(admin.id, adminRole.id);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore duplicate errors
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1193,7 @@ const migrateSchema = () => {
|
||||
for (const user of normalUsers) {
|
||||
try {
|
||||
insertUserRole.run(user.id, userRole.id);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore duplicate errors
|
||||
}
|
||||
}
|
||||
@@ -1156,10 +1227,11 @@ async function saveMemoryDatabaseToFile() {
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionCount = memoryDatabase
|
||||
memoryDatabase
|
||||
.prepare("SELECT COUNT(*) as count FROM sessions")
|
||||
.get() as { count: number };
|
||||
} catch (countError) {
|
||||
} catch {
|
||||
// expected - sessions table may not exist yet
|
||||
}
|
||||
|
||||
if (enableFileEncryption) {
|
||||
@@ -1170,6 +1242,8 @@ async function saveMemoryDatabaseToFile() {
|
||||
} else {
|
||||
fs.writeFileSync(dbPath, buffer);
|
||||
}
|
||||
|
||||
DatabaseSaveTrigger.markClean();
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to save in-memory database", error, {
|
||||
operation: "memory_db_save_failed",
|
||||
@@ -1185,7 +1259,11 @@ async function handlePostInitFileEncryption() {
|
||||
if (memoryDatabase) {
|
||||
await saveMemoryDatabaseToFile();
|
||||
|
||||
setInterval(saveMemoryDatabaseToFile, 15 * 1000);
|
||||
setInterval(() => {
|
||||
if (DatabaseSaveTrigger.isDirty) {
|
||||
saveMemoryDatabaseToFile();
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
DatabaseSaveTrigger.initialize(saveMemoryDatabaseToFile);
|
||||
}
|
||||
@@ -1254,15 +1332,18 @@ async function cleanupDatabase() {
|
||||
try {
|
||||
fs.unlinkSync(path.join(tempDir, file));
|
||||
} catch {
|
||||
// expected - file cleanup best effort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmdirSync(tempDir);
|
||||
} catch {
|
||||
// expected - dir cleanup best effort
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// expected - temp dir cleanup best effort
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,6 +1352,7 @@ process.on("exit", () => {
|
||||
try {
|
||||
sqlite.close();
|
||||
} catch {
|
||||
// expected - database may already be closed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,25 +4,25 @@ import { sql } from "drizzle-orm";
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
username: text("username").notNull(),
|
||||
password_hash: text("password_hash").notNull(),
|
||||
is_admin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
||||
|
||||
is_oidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false),
|
||||
oidc_identifier: text("oidc_identifier"),
|
||||
client_id: text("client_id"),
|
||||
client_secret: text("client_secret"),
|
||||
issuer_url: text("issuer_url"),
|
||||
authorization_url: text("authorization_url"),
|
||||
token_url: text("token_url"),
|
||||
identifier_path: text("identifier_path"),
|
||||
name_path: text("name_path"),
|
||||
isOidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false),
|
||||
oidcIdentifier: text("oidc_identifier"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
issuerUrl: text("issuer_url"),
|
||||
authorizationUrl: text("authorization_url"),
|
||||
tokenUrl: text("token_url"),
|
||||
identifierPath: text("identifier_path"),
|
||||
namePath: text("name_path"),
|
||||
scopes: text().default("openid email profile"),
|
||||
|
||||
totp_secret: text("totp_secret"),
|
||||
totp_enabled: integer("totp_enabled", { mode: "boolean" })
|
||||
totpSecret: text("totp_secret"),
|
||||
totpEnabled: integer("totp_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
totp_backup_codes: text("totp_backup_codes"),
|
||||
totpBackupCodes: text("totp_backup_codes"),
|
||||
});
|
||||
|
||||
export const settings = sqliteTable("settings", {
|
||||
@@ -47,11 +47,29 @@ export const sessions = sqliteTable("sessions", {
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
export const sshData = sqliteTable("ssh_data", {
|
||||
export const trustedDevices = sqliteTable("trusted_devices", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
deviceFingerprint: text("device_fingerprint").notNull(),
|
||||
deviceType: text("device_type").notNull(),
|
||||
deviceInfo: text("device_info").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
lastUsedAt: text("last_used_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
export const hosts = sqliteTable("ssh_data", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
connectionType: text("connection_type").notNull().default("ssh"),
|
||||
name: text("name"),
|
||||
ip: text("ip").notNull(),
|
||||
port: integer("port").notNull(),
|
||||
@@ -64,7 +82,7 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
|
||||
password: text("password"),
|
||||
key: text("key", { length: 8192 }),
|
||||
key_password: text("key_password"),
|
||||
keyPassword: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
sudoPassword: text("sudo_password"),
|
||||
|
||||
@@ -107,9 +125,14 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
.default(false),
|
||||
defaultPath: text("default_path"),
|
||||
statsConfig: text("stats_config"),
|
||||
dockerConfig: text("docker_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
domain: text("domain"),
|
||||
security: text("security"),
|
||||
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
|
||||
guacamoleConfig: text("guacamole_config"),
|
||||
|
||||
useSocks5: integer("use_socks5", { mode: "boolean" }),
|
||||
socks5Host: text("socks5_host"),
|
||||
@@ -140,7 +163,7 @@ export const fileManagerRecent = sqliteTable("file_manager_recent", {
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
path: text("path").notNull(),
|
||||
lastOpened: text("last_opened")
|
||||
@@ -155,7 +178,7 @@ export const fileManagerPinned = sqliteTable("file_manager_pinned", {
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
path: text("path").notNull(),
|
||||
pinnedAt: text("pinned_at")
|
||||
@@ -170,7 +193,7 @@ export const fileManagerShortcuts = sqliteTable("file_manager_shortcuts", {
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
path: text("path").notNull(),
|
||||
createdAt: text("created_at")
|
||||
@@ -202,9 +225,9 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
|
||||
username: text("username"),
|
||||
password: text("password"),
|
||||
key: text("key", { length: 16384 }),
|
||||
private_key: text("private_key", { length: 16384 }),
|
||||
public_key: text("public_key", { length: 4096 }),
|
||||
key_password: text("key_password"),
|
||||
privateKey: text("private_key", { length: 16384 }),
|
||||
publicKey: text("public_key", { length: 4096 }),
|
||||
keyPassword: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
detectedKeyType: text("detected_key_type"),
|
||||
|
||||
@@ -229,7 +252,7 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", {
|
||||
.references(() => sshCredentials.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
@@ -296,7 +319,7 @@ export const recentActivity = sqliteTable("recent_activity", {
|
||||
type: text("type").notNull(),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
hostName: text("host_name"),
|
||||
timestamp: text("timestamp")
|
||||
.notNull()
|
||||
@@ -310,7 +333,7 @@ export const commandHistory = sqliteTable("command_history", {
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
command: text("command").notNull(),
|
||||
executedAt: text("executed_at")
|
||||
.notNull()
|
||||
@@ -350,7 +373,7 @@ export const hostAccess = sqliteTable("host_access", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
|
||||
userId: text("user_id")
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
@@ -475,7 +498,7 @@ export const sessionRecordings = sqliteTable("session_recordings", {
|
||||
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
@@ -506,7 +529,7 @@ export const opksshTokens = sqliteTable("opkssh_tokens", {
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => sshData.id, { onDelete: "cascade" }),
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
|
||||
sshCert: text("ssh_cert", { length: 8192 }).notNull(),
|
||||
privateKey: text("private_key", { length: 8192 }).notNull(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { db } from "../db/index.js";
|
||||
import {
|
||||
sshCredentials,
|
||||
sshCredentialUsage,
|
||||
sshData,
|
||||
hosts,
|
||||
hostAccess,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
@@ -225,9 +225,9 @@ router.post(
|
||||
username: username?.trim() || null,
|
||||
password: plainPassword,
|
||||
key: plainKey,
|
||||
private_key: keyInfo?.privateKey || plainKey,
|
||||
public_key: keyInfo?.publicKey || null,
|
||||
key_password: plainKeyPassword,
|
||||
privateKey: keyInfo?.privateKey || plainKey,
|
||||
publicKey: keyInfo?.publicKey || null,
|
||||
keyPassword: plainKeyPassword,
|
||||
keyType: keyType || null,
|
||||
detectedKeyType: keyInfo?.keyType || null,
|
||||
usageCount: 0,
|
||||
@@ -434,14 +434,14 @@ router.get(
|
||||
if (credential.key) {
|
||||
output.key = credential.key;
|
||||
}
|
||||
if (credential.private_key) {
|
||||
output.privateKey = credential.private_key;
|
||||
if (credential.privateKey) {
|
||||
output.privateKey = credential.privateKey;
|
||||
}
|
||||
if (credential.public_key) {
|
||||
output.publicKey = credential.public_key;
|
||||
if (credential.publicKey) {
|
||||
output.publicKey = credential.publicKey;
|
||||
}
|
||||
if (credential.key_password) {
|
||||
output.keyPassword = credential.key_password;
|
||||
if (credential.keyPassword) {
|
||||
output.keyPassword = credential.keyPassword;
|
||||
}
|
||||
|
||||
res.json(output);
|
||||
@@ -564,13 +564,13 @@ router.put(
|
||||
error: `Invalid SSH key: ${keyInfo.error}`,
|
||||
});
|
||||
}
|
||||
updateFields.private_key = keyInfo.privateKey;
|
||||
updateFields.public_key = keyInfo.publicKey;
|
||||
updateFields.privateKey = keyInfo.privateKey;
|
||||
updateFields.publicKey = keyInfo.publicKey;
|
||||
updateFields.detectedKeyType = keyInfo.keyType;
|
||||
}
|
||||
}
|
||||
if (updateData.keyPassword !== undefined) {
|
||||
updateFields.key_password = updateData.keyPassword || null;
|
||||
updateFields.keyPassword = updateData.keyPassword || null;
|
||||
}
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
@@ -614,7 +614,6 @@ router.put(
|
||||
userId,
|
||||
);
|
||||
|
||||
const credential = updated[0];
|
||||
authLogger.success("SSH credential updated", {
|
||||
operation: "credential_update_success",
|
||||
userId,
|
||||
@@ -691,29 +690,23 @@ router.delete(
|
||||
|
||||
const hostsUsingCredential = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(id)),
|
||||
eq(sshData.userId, userId),
|
||||
),
|
||||
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
|
||||
);
|
||||
|
||||
if (hostsUsingCredential.length > 0) {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
credentialId: null,
|
||||
password: null,
|
||||
key: null,
|
||||
key_password: null,
|
||||
keyPassword: null,
|
||||
authType: "password",
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(id)),
|
||||
eq(sshData.userId, userId),
|
||||
),
|
||||
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
|
||||
);
|
||||
|
||||
for (const host of hostsUsingCredential) {
|
||||
@@ -751,7 +744,6 @@ router.delete(
|
||||
),
|
||||
);
|
||||
|
||||
const credential = credentialToDelete[0];
|
||||
authLogger.success("SSH credential deleted", {
|
||||
operation: "credential_delete_success",
|
||||
userId,
|
||||
@@ -837,20 +829,18 @@ router.post(
|
||||
const credential = credentials[0];
|
||||
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
credentialId: parseInt(credentialId),
|
||||
username: (credential.username as string) || "",
|
||||
authType: (credential.auth_type || credential.authType) as string,
|
||||
authType: credential.authType as string,
|
||||
password: null,
|
||||
key: null,
|
||||
key_password: null,
|
||||
keyPassword: null,
|
||||
keyType: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(
|
||||
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
||||
);
|
||||
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId)));
|
||||
|
||||
await db.insert(sshCredentialUsage).values({
|
||||
credentialId: parseInt(credentialId),
|
||||
@@ -917,17 +907,17 @@ router.get(
|
||||
}
|
||||
|
||||
try {
|
||||
const hosts = await db
|
||||
const hostsUsingCredential = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(credentialId)),
|
||||
eq(sshData.userId, userId),
|
||||
eq(hosts.credentialId, parseInt(credentialId)),
|
||||
eq(hosts.userId, userId),
|
||||
),
|
||||
);
|
||||
|
||||
res.json(hosts.map((host) => formatSSHHostOutput(host)));
|
||||
res.json(hostsUsingCredential.map((host) => formatSSHHostOutput(host)));
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to fetch hosts using credential", err);
|
||||
res.status(500).json({
|
||||
@@ -954,15 +944,15 @@ function formatCredentialOutput(
|
||||
? credential.tags.split(",").filter(Boolean)
|
||||
: []
|
||||
: [],
|
||||
authType: credential.authType || credential.auth_type,
|
||||
authType: credential.authType,
|
||||
username: credential.username || null,
|
||||
publicKey: credential.public_key || credential.publicKey,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
|
||||
usageCount: credential.usage_count || credential.usageCount || 0,
|
||||
lastUsed: credential.last_used || credential.lastUsed,
|
||||
createdAt: credential.created_at || credential.createdAt,
|
||||
updatedAt: credential.updated_at || credential.updatedAt,
|
||||
publicKey: credential.publicKey,
|
||||
keyType: credential.keyType,
|
||||
detectedKeyType: credential.detectedKeyType,
|
||||
usageCount: credential.usageCount || 0,
|
||||
lastUsed: credential.lastUsed,
|
||||
createdAt: credential.createdAt,
|
||||
updatedAt: credential.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1508,7 +1498,7 @@ async function deploySSHKeyToHost(
|
||||
hostConfig: Record<string, unknown>,
|
||||
credData: CredentialBackend,
|
||||
): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
const publicKey = credData.public_key as string;
|
||||
const publicKey = credData.publicKey as string;
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
|
||||
@@ -1934,7 +1924,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const publicKey = credData.public_key;
|
||||
const publicKey = credData.publicKey;
|
||||
if (!publicKey) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -1942,7 +1932,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
const targetHost = await SimpleDBOps.select(
|
||||
db.select().from(sshData).where(eq(sshData.id, targetHostId)).limit(1),
|
||||
db.select().from(hosts).where(eq(hosts.id, targetHostId)).limit(1),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
@@ -1990,15 +1980,14 @@ router.post(
|
||||
if (hostCredential && hostCredential.length > 0) {
|
||||
const cred = hostCredential[0];
|
||||
|
||||
hostConfig.authType = cred.auth_type || cred.authType;
|
||||
hostConfig.authType = cred.authType;
|
||||
hostConfig.username = cred.username;
|
||||
|
||||
if ((cred.auth_type || cred.authType) === "password") {
|
||||
if (cred.authType === "password") {
|
||||
hostConfig.password = cred.password;
|
||||
} else if ((cred.auth_type || cred.authType) === "key") {
|
||||
hostConfig.privateKey =
|
||||
cred.private_key || cred.privateKey || cred.key;
|
||||
hostConfig.keyPassword = cred.key_password || cred.keyPassword;
|
||||
} else if (cred.authType === "key") {
|
||||
hostConfig.privateKey = cred.privateKey || cred.key;
|
||||
hostConfig.keyPassword = cred.keyPassword;
|
||||
}
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,14 @@ import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import {
|
||||
hostAccess,
|
||||
sshData,
|
||||
hosts,
|
||||
users,
|
||||
roles,
|
||||
userRoles,
|
||||
auditLogs,
|
||||
sharedCredentials,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm";
|
||||
import type { Request, Response } from "express";
|
||||
import type { Response } from "express";
|
||||
import { databaseLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { PermissionManager } from "../../utils/permission-manager.js";
|
||||
@@ -112,8 +111,8 @@ router.post(
|
||||
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -335,8 +334,8 @@ router.delete(
|
||||
try {
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -403,8 +402,8 @@ router.get(
|
||||
try {
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -483,21 +482,21 @@ router.get(
|
||||
|
||||
const sharedHosts = await db
|
||||
.select({
|
||||
id: sshData.id,
|
||||
name: sshData.name,
|
||||
ip: sshData.ip,
|
||||
port: sshData.port,
|
||||
username: sshData.username,
|
||||
folder: sshData.folder,
|
||||
tags: sshData.tags,
|
||||
id: hosts.id,
|
||||
name: hosts.name,
|
||||
ip: hosts.ip,
|
||||
port: hosts.port,
|
||||
username: hosts.username,
|
||||
folder: hosts.folder,
|
||||
tags: hosts.tags,
|
||||
permissionLevel: hostAccess.permissionLevel,
|
||||
expiresAt: hostAccess.expiresAt,
|
||||
grantedBy: hostAccess.grantedBy,
|
||||
ownerUsername: users.username,
|
||||
})
|
||||
.from(hostAccess)
|
||||
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
|
||||
.innerJoin(users, eq(sshData.userId, users.id))
|
||||
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
|
||||
.innerJoin(users, eq(hosts.userId, users.id))
|
||||
.where(
|
||||
and(
|
||||
eq(hostAccess.userId, userId),
|
||||
@@ -857,10 +856,7 @@ router.delete(
|
||||
permissionManager.invalidateUserPermissionCache(userId);
|
||||
}
|
||||
|
||||
const deletedHostAccess = await db
|
||||
.delete(hostAccess)
|
||||
.where(eq(hostAccess.roleId, roleId))
|
||||
.returning({ id: hostAccess.id });
|
||||
await db.delete(hostAccess).where(eq(hostAccess.roleId, roleId));
|
||||
|
||||
await db.delete(roles).where(eq(roles.id, roleId));
|
||||
|
||||
@@ -980,7 +976,7 @@ router.post(
|
||||
const hostsSharedWithRole = await db
|
||||
.select()
|
||||
.from(hostAccess)
|
||||
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
|
||||
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
|
||||
.where(eq(hostAccess.roleId, roleId));
|
||||
|
||||
const { SharedCredentialManager } =
|
||||
|
||||
@@ -632,17 +632,15 @@ router.post(
|
||||
const snippet = snippetResult[0];
|
||||
|
||||
const { Client } = await import("ssh2");
|
||||
const { sshData, sshCredentials } = await import("../db/schema.js");
|
||||
const { hosts, sshCredentials } = await import("../db/schema.js");
|
||||
|
||||
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
|
||||
|
||||
const hostResult = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(
|
||||
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
||||
),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
@@ -655,7 +653,7 @@ router.post(
|
||||
|
||||
let password = host.password;
|
||||
let privateKey = host.key;
|
||||
let passphrase = host.key_password;
|
||||
let passphrase = host.keyPassword;
|
||||
let authType = host.authType;
|
||||
|
||||
if (host.credentialId) {
|
||||
@@ -675,12 +673,12 @@ router.post(
|
||||
|
||||
if (credResult.length > 0) {
|
||||
const cred = credResult[0];
|
||||
authType = (cred.auth_type || cred.authType || authType) as string;
|
||||
authType = (cred.authType || authType) as string;
|
||||
password = (cred.password || undefined) as string | undefined;
|
||||
privateKey = (cred.private_key || cred.key || undefined) as
|
||||
privateKey = (cred.privateKey || cred.key || undefined) as
|
||||
| string
|
||||
| undefined;
|
||||
passphrase = (cred.key_password || undefined) as string | undefined;
|
||||
passphrase = (cred.keyPassword || undefined) as string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,7 +729,7 @@ router.post(
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const config: any = {
|
||||
const config: Record<string, unknown> = {
|
||||
host: host.ip,
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
|
||||
@@ -285,4 +285,118 @@ router.delete(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /terminal/session_settings:
|
||||
* get:
|
||||
* summary: Get session persistence settings
|
||||
* description: Returns the session timeout and persistence enabled flag.
|
||||
* tags:
|
||||
* - Terminal
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Session settings.
|
||||
* 500:
|
||||
* description: Failed to fetch settings.
|
||||
*/
|
||||
router.get(
|
||||
"/session_settings",
|
||||
authenticateJWT,
|
||||
async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const timeoutRow = db.$client
|
||||
.prepare(
|
||||
"SELECT value FROM settings WHERE key = 'terminal_session_timeout_minutes'",
|
||||
)
|
||||
.get() as { value: string } | undefined;
|
||||
const enabledRow = db.$client
|
||||
.prepare(
|
||||
"SELECT value FROM settings WHERE key = 'terminal_session_persistence_enabled'",
|
||||
)
|
||||
.get() as { value: string } | undefined;
|
||||
|
||||
res.json({
|
||||
timeoutMinutes: timeoutRow ? parseInt(timeoutRow.value, 10) : 30,
|
||||
enabled: enabledRow ? enabledRow.value === "true" : true,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to fetch session settings", err);
|
||||
res.status(500).json({
|
||||
error: err instanceof Error ? err.message : "Failed to fetch settings",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /terminal/session_settings:
|
||||
* post:
|
||||
* summary: Update session persistence settings
|
||||
* description: Saves session timeout and persistence enabled flag.
|
||||
* tags:
|
||||
* - Terminal
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* timeoutMinutes:
|
||||
* type: integer
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Settings saved successfully.
|
||||
* 400:
|
||||
* description: Invalid parameters.
|
||||
* 500:
|
||||
* description: Failed to save settings.
|
||||
*/
|
||||
router.post(
|
||||
"/session_settings",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const { timeoutMinutes, enabled } = req.body;
|
||||
|
||||
if (
|
||||
timeoutMinutes !== undefined &&
|
||||
(typeof timeoutMinutes !== "number" ||
|
||||
timeoutMinutes < 1 ||
|
||||
timeoutMinutes > 1440)
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "timeoutMinutes must be between 1 and 1440" });
|
||||
}
|
||||
|
||||
try {
|
||||
if (timeoutMinutes !== undefined) {
|
||||
db.$client
|
||||
.prepare(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('terminal_session_timeout_minutes', ?)",
|
||||
)
|
||||
.run(String(timeoutMinutes));
|
||||
}
|
||||
|
||||
if (enabled !== undefined) {
|
||||
db.$client
|
||||
.prepare(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('terminal_session_persistence_enabled', ?)",
|
||||
)
|
||||
.run(String(enabled));
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to save session settings", err);
|
||||
res.status(500).json({
|
||||
error: err instanceof Error ? err.message : "Failed to save settings",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
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 } {
|
||||
const parts = url.split(":");
|
||||
return {
|
||||
host: parts[0] || "localhost",
|
||||
port: parseInt(parts[1] || "4822", 10),
|
||||
};
|
||||
}
|
||||
|
||||
function readGuacdOptions(): { host: string; port: number } {
|
||||
let host = process.env.GUACD_HOST || "localhost";
|
||||
let port = parseInt(process.env.GUACD_PORT || "4822", 10);
|
||||
try {
|
||||
const db = getDb();
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
if (urlRow?.value) {
|
||||
const parsed = parseGuacUrl(urlRow.value);
|
||||
host = parsed.host;
|
||||
port = parsed.port;
|
||||
}
|
||||
} catch {
|
||||
// DB not available yet, use env var defaults
|
||||
}
|
||||
return { host, port };
|
||||
}
|
||||
|
||||
const GUAC_WS_PORT = 30008;
|
||||
|
||||
const websocketOptions = {
|
||||
port: GUAC_WS_PORT,
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
crypt: {
|
||||
cypher: "AES-256-CBC",
|
||||
key: tokenService.getEncryptionKey(),
|
||||
},
|
||||
log: {
|
||||
level: "ERRORS",
|
||||
stdLog: (...args: unknown[]) => {
|
||||
guacLogger.info(args.join(" "));
|
||||
},
|
||||
errorLog: (...args: unknown[]) => {
|
||||
guacLogger.error(args.join(" "));
|
||||
},
|
||||
},
|
||||
allowedUnencryptedConnectionSettings: {
|
||||
rdp: ["width", "height", "dpi"],
|
||||
vnc: ["width", "height", "dpi"],
|
||||
telnet: ["width", "height"],
|
||||
},
|
||||
connectionDefaultSettings: {
|
||||
rdp: {
|
||||
security: "any",
|
||||
"ignore-cert": true,
|
||||
"enable-wallpaper": false,
|
||||
"enable-font-smoothing": true,
|
||||
"enable-desktop-composition": false,
|
||||
"disable-audio": false,
|
||||
"enable-drive": false,
|
||||
"resize-method": "display-update",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
dpi: 96,
|
||||
},
|
||||
vnc: {
|
||||
"swap-red-blue": false,
|
||||
cursor: "remote",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
telnet: {
|
||||
"terminal-type": "xterm-256color",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const _origConsoleLog = console.log;
|
||||
console.log = (...args: unknown[]) => {
|
||||
const msg = args[0];
|
||||
if (typeof msg === "string" && msg.startsWith("New client connection"))
|
||||
return;
|
||||
_origConsoleLog(...args);
|
||||
};
|
||||
|
||||
function createGuacServer(): GuacamoleLite {
|
||||
const guacdOptions = readGuacdOptions();
|
||||
const server = new GuacamoleLite(
|
||||
websocketOptions,
|
||||
guacdOptions,
|
||||
clientOptions,
|
||||
);
|
||||
|
||||
server.on(
|
||||
"open",
|
||||
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
|
||||
guacLogger.info("Guacamole connection opened", {
|
||||
operation: "guac_connection_open",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.on(
|
||||
"close",
|
||||
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
|
||||
guacLogger.info("Guacamole connection closed", {
|
||||
operation: "guac_connection_close",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.on(
|
||||
"error",
|
||||
(
|
||||
clientConnection: { connectionSettings?: Record<string, unknown> },
|
||||
error: Error,
|
||||
) => {
|
||||
guacLogger.error("Guacamole connection error", error, {
|
||||
operation: "guac_connection_error",
|
||||
type: clientConnection.connectionSettings?.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
let guacServer = createGuacServer();
|
||||
|
||||
export async function restartGuacServer(): Promise<void> {
|
||||
try {
|
||||
guacServer.close();
|
||||
} catch (err) {
|
||||
guacLogger.error("Error closing guac server during restart", err as Error);
|
||||
}
|
||||
guacServer = createGuacServer();
|
||||
}
|
||||
|
||||
export { guacServer, tokenService };
|
||||
@@ -0,0 +1,303 @@
|
||||
import express from "express";
|
||||
import { GuacamoleTokenService } from "./token-service.js";
|
||||
import { guacLogger } from "../utils/logger.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
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 type { AuthenticatedRequest } from "../../types/index.js";
|
||||
|
||||
const router = express.Router();
|
||||
const tokenService = GuacamoleTokenService.getInstance();
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
router.use(authManager.createAuthMiddleware());
|
||||
|
||||
/**
|
||||
* POST /guacamole/token
|
||||
* Generate an encrypted connection token for guacamole-lite
|
||||
*
|
||||
* Body: {
|
||||
* type: "rdp" | "vnc" | "telnet",
|
||||
* hostname: string,
|
||||
* port?: number,
|
||||
* username?: string,
|
||||
* password?: string,
|
||||
* domain?: string,
|
||||
* // Additional protocol-specific options
|
||||
* }
|
||||
*/
|
||||
router.post("/token", async (req, res) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { type, hostname, port, username, password, domain, ...options } =
|
||||
req.body;
|
||||
|
||||
if (!type || !hostname) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Missing required fields: type and hostname" });
|
||||
}
|
||||
|
||||
if (!["rdp", "vnc", "telnet"].includes(type)) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid connection type. Must be rdp, vnc, or telnet",
|
||||
});
|
||||
}
|
||||
|
||||
let token: string;
|
||||
|
||||
switch (type) {
|
||||
case "rdp":
|
||||
token = tokenService.createRdpToken(
|
||||
hostname,
|
||||
username || "",
|
||||
password || "",
|
||||
{
|
||||
port: port || 3389,
|
||||
domain,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "vnc":
|
||||
token = tokenService.createVncToken(hostname, password, {
|
||||
port: port || 5900,
|
||||
...options,
|
||||
});
|
||||
break;
|
||||
case "telnet":
|
||||
token = tokenService.createTelnetToken(hostname, username, password, {
|
||||
port: port || 23,
|
||||
...options,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: "Invalid connection type" });
|
||||
}
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to generate guacamole token", error, {
|
||||
operation: "guac_token_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to generate connection token" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /guacamole/connect-host/{hostId}:
|
||||
* post:
|
||||
* summary: Generate Guacamole connection token from host configuration
|
||||
* description: Fetches host configuration from database and generates a connection token for RDP/VNC/Telnet
|
||||
* tags:
|
||||
* - Guacamole
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: hostId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Host ID to connect to
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Connection token generated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* description: Encrypted connection token
|
||||
* 400:
|
||||
* description: Invalid request or unsupported connection type
|
||||
* 403:
|
||||
* description: Access denied to host
|
||||
* 404:
|
||||
* description: Host not found
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post(
|
||||
"/connect-host/:hostId",
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId!;
|
||||
const hostId = parseInt(req.params.hostId, 10);
|
||||
|
||||
if (!hostId || isNaN(hostId)) {
|
||||
return res.status(400).json({ error: "Invalid host ID" });
|
||||
}
|
||||
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hostResults.length === 0) {
|
||||
return res.status(404).json({ error: "Host not found" });
|
||||
}
|
||||
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.userId !== userId) {
|
||||
const permissionManager = PermissionManager.getInstance();
|
||||
const accessInfo = await permissionManager.canAccessHost(
|
||||
userId,
|
||||
hostId,
|
||||
"read",
|
||||
);
|
||||
|
||||
if (!accessInfo.hasAccess) {
|
||||
guacLogger.warn("User attempted to access host without permission", {
|
||||
operation: "guac_access_denied",
|
||||
userId,
|
||||
hostId,
|
||||
});
|
||||
return res.status(403).json({ error: "Access denied to this host" });
|
||||
}
|
||||
}
|
||||
|
||||
const connectionType = (host.connectionType as string) || "ssh";
|
||||
if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
|
||||
return res.status(400).json({
|
||||
error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
|
||||
});
|
||||
}
|
||||
|
||||
let guacConfig: Record<string, unknown> = {};
|
||||
if (host.guacamoleConfig) {
|
||||
try {
|
||||
guacConfig =
|
||||
typeof host.guacamoleConfig === "string"
|
||||
? JSON.parse(host.guacamoleConfig as string)
|
||||
: (host.guacamoleConfig as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
guacLogger.warn("Failed to parse guacamole config", {
|
||||
operation: "guac_config_parse_error",
|
||||
hostId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let token: string;
|
||||
const hostname = host.ip as string;
|
||||
const port = host.port as number;
|
||||
const username = (host.username as string) || "";
|
||||
const password = (host.password as string) || "";
|
||||
const domain = (host.domain as string) || "";
|
||||
|
||||
switch (connectionType) {
|
||||
case "rdp":
|
||||
token = tokenService.createRdpToken(hostname, username, password, {
|
||||
port: port || 3389,
|
||||
domain,
|
||||
security: (host.security as string) || undefined,
|
||||
"ignore-cert": (host.ignoreCert as boolean) || false,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
case "vnc":
|
||||
token = tokenService.createVncToken(hostname, password, {
|
||||
port: port || 5900,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
case "telnet":
|
||||
token = tokenService.createTelnetToken(hostname, username, password, {
|
||||
port: port || 23,
|
||||
...guacConfig,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: "Invalid connection type" });
|
||||
}
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to generate guacamole token for host", error, {
|
||||
operation: "guac_host_token_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to generate connection token" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /guacamole/status
|
||||
* Check if guacd is reachable
|
||||
*/
|
||||
router.get("/status", async (req, res) => {
|
||||
try {
|
||||
let guacdHost = process.env.GUACD_HOST || "localhost";
|
||||
let guacdPort = parseInt(process.env.GUACD_PORT || "4822", 10);
|
||||
try {
|
||||
const db = getDb();
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
if (urlRow?.value) {
|
||||
const parts = urlRow.value.split(":");
|
||||
guacdHost = parts[0] || guacdHost;
|
||||
guacdPort = parseInt(parts[1] || String(guacdPort), 10);
|
||||
}
|
||||
} catch {
|
||||
// Fall back to env vars
|
||||
}
|
||||
|
||||
const net = await import("net");
|
||||
|
||||
const checkConnection = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
socket.setTimeout(3000);
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(guacdPort, guacdHost);
|
||||
});
|
||||
};
|
||||
|
||||
const isConnected = await checkConnection();
|
||||
|
||||
res.json({
|
||||
guacd: {
|
||||
host: guacdHost,
|
||||
port: guacdPort,
|
||||
status: isConnected ? "connected" : "disconnected",
|
||||
},
|
||||
websocket: {
|
||||
port: 30008,
|
||||
status: "running",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to check guacamole status", error, {
|
||||
operation: "guac_status_error",
|
||||
});
|
||||
res.status(500).json({ error: "Failed to check status" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,181 @@
|
||||
import crypto from "crypto";
|
||||
import { guacLogger } from "../utils/logger.js";
|
||||
|
||||
export interface GuacamoleConnectionSettings {
|
||||
type: "rdp" | "vnc" | "telnet";
|
||||
settings: {
|
||||
hostname: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
security?: string;
|
||||
"ignore-cert"?: boolean;
|
||||
"enable-wallpaper"?: boolean;
|
||||
"enable-drive"?: boolean;
|
||||
"drive-path"?: string;
|
||||
"create-drive-path"?: boolean;
|
||||
"swap-red-blue"?: boolean;
|
||||
cursor?: string;
|
||||
"terminal-type"?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuacamoleToken {
|
||||
connection: GuacamoleConnectionSettings;
|
||||
}
|
||||
|
||||
const CIPHER = "aes-256-cbc";
|
||||
const KEY_LENGTH = 32;
|
||||
|
||||
export class GuacamoleTokenService {
|
||||
private static instance: GuacamoleTokenService;
|
||||
private encryptionKey: Buffer;
|
||||
|
||||
private constructor() {
|
||||
this.encryptionKey = this.initializeKey();
|
||||
}
|
||||
|
||||
static getInstance(): GuacamoleTokenService {
|
||||
if (!GuacamoleTokenService.instance) {
|
||||
GuacamoleTokenService.instance = new GuacamoleTokenService();
|
||||
}
|
||||
return GuacamoleTokenService.instance;
|
||||
}
|
||||
|
||||
private initializeKey(): Buffer {
|
||||
const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY;
|
||||
if (existingKey) {
|
||||
if (existingKey.length === 64 && /^[0-9a-fA-F]+$/.test(existingKey)) {
|
||||
return Buffer.from(existingKey, "hex");
|
||||
}
|
||||
if (existingKey.length === KEY_LENGTH) {
|
||||
return Buffer.from(existingKey, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (jwtSecret) {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(jwtSecret + "_guacamole")
|
||||
.digest();
|
||||
}
|
||||
|
||||
guacLogger.warn(
|
||||
"No persistent encryption key found, generating random key",
|
||||
{
|
||||
operation: "guac_key_generation",
|
||||
},
|
||||
);
|
||||
return crypto.randomBytes(KEY_LENGTH);
|
||||
}
|
||||
|
||||
getEncryptionKey(): Buffer {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
encryptToken(tokenObject: GuacamoleToken): string {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(CIPHER, this.encryptionKey, iv);
|
||||
|
||||
let encrypted = cipher.update(
|
||||
JSON.stringify(tokenObject),
|
||||
"utf8",
|
||||
"base64",
|
||||
);
|
||||
encrypted += cipher.final("base64");
|
||||
|
||||
const data = {
|
||||
iv: iv.toString("base64"),
|
||||
value: encrypted,
|
||||
};
|
||||
|
||||
return Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
}
|
||||
|
||||
decryptToken(token: string): GuacamoleToken | null {
|
||||
try {
|
||||
const data = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
|
||||
const iv = Buffer.from(data.iv, "base64");
|
||||
const decipher = crypto.createDecipheriv(CIPHER, this.encryptionKey, iv);
|
||||
|
||||
let decrypted = decipher.update(data.value, "base64", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return JSON.parse(decrypted) as GuacamoleToken;
|
||||
} catch (error) {
|
||||
guacLogger.error("Failed to decrypt guacamole token", error, {
|
||||
operation: "guac_token_decrypt_error",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
createRdpToken(
|
||||
hostname: string,
|
||||
username: string,
|
||||
password: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "rdp",
|
||||
settings: {
|
||||
hostname,
|
||||
username,
|
||||
password,
|
||||
port: 3389,
|
||||
security: "nla",
|
||||
"ignore-cert": true,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
|
||||
createVncToken(
|
||||
hostname: string,
|
||||
password?: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "vnc",
|
||||
settings: {
|
||||
hostname,
|
||||
password,
|
||||
port: 5900,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
|
||||
createTelnetToken(
|
||||
hostname: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
|
||||
): string {
|
||||
const token: GuacamoleToken = {
|
||||
connection: {
|
||||
type: "telnet",
|
||||
settings: {
|
||||
hostname,
|
||||
username,
|
||||
password,
|
||||
port: 23,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
return this.encryptToken(token);
|
||||
}
|
||||
}
|
||||
@@ -99,5 +99,4 @@ main() {
|
||||
setup_ssl_certificates
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
main "$@"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Client } from "ssh2";
|
||||
import type { WebSocket } from "ws";
|
||||
import { sshLogger, authLogger } from "../utils/logger.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials, sshData } from "../database/db/schema.js";
|
||||
import { sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { UserCrypto } from "../utils/user-crypto.js";
|
||||
|
||||
interface ResolvedCredentials {
|
||||
username: string;
|
||||
password?: string;
|
||||
@@ -44,9 +41,6 @@ interface AuthContext {
|
||||
totpAttempts: number;
|
||||
}
|
||||
|
||||
const userCrypto = UserCrypto.getInstance();
|
||||
const MAX_TOTP_ATTEMPTS = 3;
|
||||
|
||||
export class SSHAuthManager {
|
||||
public context: AuthContext;
|
||||
|
||||
@@ -85,7 +79,7 @@ export class SSHAuthManager {
|
||||
key: cred.privateKey
|
||||
? Buffer.from(cred.privateKey as string)
|
||||
: undefined,
|
||||
keyPassword: (cred.passphrase as string) || undefined,
|
||||
keyPassword: (cred.keyPassword as string) || undefined,
|
||||
authType: (cred.authType as string) || "none",
|
||||
};
|
||||
}
|
||||
@@ -163,7 +157,7 @@ export class SSHAuthManager {
|
||||
);
|
||||
|
||||
if (urlMatch) {
|
||||
this.context.keyboardInteractiveFinish = (responses: string[]) => {
|
||||
this.context.keyboardInteractiveFinish = () => {
|
||||
finish([""]);
|
||||
};
|
||||
|
||||
@@ -358,7 +352,7 @@ export class SSHAuthManager {
|
||||
stage: string,
|
||||
level: string,
|
||||
message: string,
|
||||
details?: Record<string, any>,
|
||||
details?: Record<string, unknown>,
|
||||
): void {
|
||||
this.context.ws.send(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Client as SSHClient } from "ssh2";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { parse as parseUrl } from "url";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -13,7 +13,7 @@ const sshLogger = systemLogger;
|
||||
|
||||
interface SSHSession {
|
||||
client: SSHClient;
|
||||
stream: any;
|
||||
stream: import("ssh2").ClientChannel | null;
|
||||
isConnected: boolean;
|
||||
containerId?: string;
|
||||
shell?: string;
|
||||
@@ -24,7 +24,7 @@ const activeSessions = new Map<string, SSHSession>();
|
||||
|
||||
const wss = new WebSocketServer({
|
||||
host: "0.0.0.0",
|
||||
port: 30008,
|
||||
port: 30009,
|
||||
verifyClient: async (info) => {
|
||||
try {
|
||||
const url = parseUrl(info.req.url || "", true);
|
||||
@@ -42,7 +42,7 @@ const wss = new WebSocketServer({
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -92,7 +92,7 @@ async function detectShell(
|
||||
}
|
||||
|
||||
async function createJumpHostChain(
|
||||
jumpHosts: any[],
|
||||
jumpHosts: Array<{ hostId: number }>,
|
||||
userId: string,
|
||||
): Promise<SSHClient | null> {
|
||||
if (!jumpHosts || jumpHosts.length === 0) {
|
||||
@@ -107,8 +107,8 @@ async function createJumpHostChain(
|
||||
const jumpHostData = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, jumpHostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, jumpHostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
@@ -129,7 +129,12 @@ async function createJumpHostChain(
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedCredentials: any = {
|
||||
let resolvedCredentials: {
|
||||
password?: string;
|
||||
sshKey?: string;
|
||||
keyPassword?: string;
|
||||
authType?: string;
|
||||
} = {
|
||||
password: jumpHost.password,
|
||||
sshKey: jumpHost.key,
|
||||
keyPassword: jumpHost.keyPassword,
|
||||
@@ -154,19 +159,18 @@ async function createJumpHostChain(
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const client = new SSHClient();
|
||||
|
||||
const config: any = {
|
||||
host: jumpHost.ip,
|
||||
const config: Record<string, unknown> = {
|
||||
host: jumpHost.ip?.replace(/^\[|\]$/g, "") || jumpHost.ip,
|
||||
port: jumpHost.port || 22,
|
||||
username: jumpHost.username,
|
||||
tryKeyboard: true,
|
||||
@@ -225,7 +229,7 @@ async function createJumpHostChain(
|
||||
}
|
||||
|
||||
wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
|
||||
sshLogger.info("Docker console WebSocket connected", {
|
||||
operation: "docker_console_connect",
|
||||
@@ -235,6 +239,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
|
||||
let sshSession: SSHSession | null = null;
|
||||
|
||||
const wsPingInterval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.ping();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
ws.on("message", async (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
@@ -286,7 +296,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
}
|
||||
|
||||
try {
|
||||
let resolvedCredentials: any = {
|
||||
let resolvedCredentials: {
|
||||
password?: string;
|
||||
sshKey?: string;
|
||||
keyPassword?: string;
|
||||
authType?: string;
|
||||
} = {
|
||||
password: hostConfig.password,
|
||||
sshKey: hostConfig.key,
|
||||
keyPassword: hostConfig.keyPassword,
|
||||
@@ -311,22 +326,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key,
|
||||
keyPassword:
|
||||
credential.key_password || credential.keyPassword,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const client = new SSHClient();
|
||||
|
||||
const config: any = {
|
||||
host: hostConfig.ip,
|
||||
const config: Record<string, unknown> = {
|
||||
host: hostConfig.ip?.replace(/^\[|\]$/g, "") || hostConfig.ip,
|
||||
port: hostConfig.port || 22,
|
||||
username: hostConfig.username,
|
||||
tryKeyboard: true,
|
||||
@@ -362,18 +373,20 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
userId,
|
||||
);
|
||||
if (jumpClient) {
|
||||
const stream = await new Promise<any>((resolve, reject) => {
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
hostConfig.ip,
|
||||
hostConfig.port || 22,
|
||||
(err, stream) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stream);
|
||||
},
|
||||
);
|
||||
});
|
||||
const stream = await new Promise<import("ssh2").ClientChannel>(
|
||||
(resolve, reject) => {
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
hostConfig.ip,
|
||||
hostConfig.port || 22,
|
||||
(err, stream) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stream);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
config.sock = stream;
|
||||
}
|
||||
}
|
||||
@@ -496,7 +509,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
}
|
||||
});
|
||||
|
||||
stream.stderr.on("data", (data: Buffer) => {});
|
||||
stream.stderr.on("data", () => {
|
||||
// stderr output ignored
|
||||
});
|
||||
|
||||
stream.on("close", () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
@@ -556,7 +571,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
case "resize": {
|
||||
if (sshSession && sshSession.stream) {
|
||||
const { cols, rows } = message.data;
|
||||
sshSession.stream.setWindow(rows, cols);
|
||||
sshSession.stream.setWindow(rows, cols, rows, cols);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -608,6 +623,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
clearInterval(wsPingInterval);
|
||||
sshLogger.info("Docker console disconnected", {
|
||||
operation: "docker_console_disconnect",
|
||||
sessionId,
|
||||
@@ -641,7 +657,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
activeSessions.forEach((session, sessionId) => {
|
||||
activeSessions.forEach((session) => {
|
||||
if (session.stream) {
|
||||
session.stream.end();
|
||||
}
|
||||
|
||||
+283
-190
@@ -3,15 +3,17 @@ import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import axios from "axios";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import type { ClientChannel } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js";
|
||||
import {
|
||||
createSocks5Connection,
|
||||
type SOCKS5Config,
|
||||
} from "../utils/socks5-helper.js";
|
||||
import type { SSHHost, ProxyNode } from "../../types/index.js";
|
||||
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
||||
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||
|
||||
@@ -21,7 +23,7 @@ function createConnectionLog(
|
||||
type: "info" | "success" | "warning" | "error",
|
||||
stage: ConnectionStage,
|
||||
message: string,
|
||||
details?: Record<string, any>,
|
||||
details?: Record<string, unknown>,
|
||||
): Omit<LogEntry, "id" | "timestamp"> {
|
||||
return {
|
||||
type,
|
||||
@@ -43,7 +45,7 @@ interface SSHSession {
|
||||
interface PendingTOTPSession {
|
||||
client: SSHClient;
|
||||
finish: (responses: string[]) => void;
|
||||
config: any;
|
||||
config: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
sessionId: string;
|
||||
hostId?: number;
|
||||
@@ -70,7 +72,9 @@ setInterval(() => {
|
||||
if (now - session.createdAt > 180000) {
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
}
|
||||
});
|
||||
@@ -94,7 +98,9 @@ function cleanupSession(sessionId: string) {
|
||||
|
||||
try {
|
||||
session.client.end();
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
clearTimeout(session.timeout);
|
||||
delete sshSessions[sessionId];
|
||||
}
|
||||
@@ -111,25 +117,39 @@ function scheduleSessionCleanup(sessionId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
interface JumpHostConfig {
|
||||
id: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
authType?: string;
|
||||
credentialId?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
async function resolveJumpHost(
|
||||
hostId: number,
|
||||
userId: string,
|
||||
): Promise<any | null> {
|
||||
): Promise<JumpHostConfig | null> {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.credentialId) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
@@ -150,17 +170,16 @@ async function resolveJumpHost(
|
||||
const credential = credentials[0];
|
||||
return {
|
||||
...host,
|
||||
password: credential.password,
|
||||
key:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
};
|
||||
password: credential.password as string | undefined,
|
||||
key: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
} as JumpHostConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return host;
|
||||
return host as JumpHostConfig;
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to resolve jump host", error, {
|
||||
operation: "resolve_jump_host",
|
||||
@@ -174,6 +193,7 @@ async function resolveJumpHost(
|
||||
async function createJumpHostChain(
|
||||
jumpHosts: Array<{ hostId: number }>,
|
||||
userId: string,
|
||||
socks5Config?: SOCKS5Config | null,
|
||||
): Promise<SSHClient | null> {
|
||||
if (!jumpHosts || jumpHosts.length === 0) {
|
||||
return null;
|
||||
@@ -187,19 +207,33 @@ async function createJumpHostChain(
|
||||
jumpHosts.map((jh) => resolveJumpHost(jh.hostId, userId)),
|
||||
);
|
||||
|
||||
const totalHops = jumpHostConfigs.length;
|
||||
|
||||
for (let i = 0; i < jumpHostConfigs.length; i++) {
|
||||
if (!jumpHostConfigs[i]) {
|
||||
sshLogger.error(`Jump host ${i + 1} not found`, undefined, {
|
||||
operation: "jump_host_chain",
|
||||
hostId: jumpHosts[i].hostId,
|
||||
hopIndex: i,
|
||||
totalHops,
|
||||
});
|
||||
clients.forEach((c) => c.end());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let proxySocket: import("net").Socket | null = null;
|
||||
if (socks5Config?.useSocks5) {
|
||||
const firstHop = jumpHostConfigs[0]!;
|
||||
proxySocket = await createSocks5Connection(
|
||||
firstHop.ip,
|
||||
firstHop.port || 22,
|
||||
socks5Config,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < jumpHostConfigs.length; i++) {
|
||||
const jumpHostConfig = jumpHostConfigs[i];
|
||||
const jumpHostConfig = jumpHostConfigs[i]!;
|
||||
|
||||
const jumpClient = new SSHClient();
|
||||
clients.push(jumpClient);
|
||||
@@ -225,16 +259,29 @@ async function createJumpHostChain(
|
||||
|
||||
jumpClient.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
sshLogger.error(`Jump host ${i + 1} connection failed`, err, {
|
||||
operation: "jump_host_connect",
|
||||
hostId: jumpHostConfig.id,
|
||||
ip: jumpHostConfig.ip,
|
||||
});
|
||||
sshLogger.error(
|
||||
`Jump host ${i + 1}/${totalHops} connection failed`,
|
||||
err,
|
||||
{
|
||||
operation: "jump_host_connect",
|
||||
hostId: jumpHostConfig.id,
|
||||
ip: jumpHostConfig.ip,
|
||||
hopIndex: i,
|
||||
totalHops,
|
||||
previousHop:
|
||||
i > 0
|
||||
? jumpHostConfigs[i - 1]?.ip
|
||||
: proxySocket
|
||||
? "proxy"
|
||||
: "direct",
|
||||
usedProxySocket: i === 0 && !!proxySocket,
|
||||
},
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
const connectConfig: any = {
|
||||
host: jumpHostConfig.ip,
|
||||
const connectConfig: Record<string, unknown> = {
|
||||
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
|
||||
port: jumpHostConfig.port || 22,
|
||||
username: jumpHostConfig.username,
|
||||
tryKeyboard: true,
|
||||
@@ -271,6 +318,9 @@ async function createJumpHostChain(
|
||||
jumpClient.connect(connectConfig);
|
||||
},
|
||||
);
|
||||
} else if (proxySocket) {
|
||||
connectConfig.sock = proxySocket;
|
||||
jumpClient.connect(connectConfig);
|
||||
} else {
|
||||
jumpClient.connect(connectConfig);
|
||||
}
|
||||
@@ -411,6 +461,10 @@ app.use(
|
||||
app.use(cookieParser());
|
||||
app.use(express.json({ limit: "100mb" }));
|
||||
app.use(express.urlencoded({ limit: "100mb", extended: true }));
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
app.use(authManager.createAuthMiddleware());
|
||||
@@ -450,7 +504,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
userProvidedPassword,
|
||||
userProvidedSshKey,
|
||||
userProvidedKeyPassword,
|
||||
forceKeyboardInteractive,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
socks5Port,
|
||||
@@ -458,7 +511,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
socks5Password,
|
||||
socks5ProxyChain,
|
||||
} = req.body;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = [];
|
||||
|
||||
@@ -517,20 +570,20 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
);
|
||||
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
getDb().select().from(sshData).where(eq(sshData.id, hostId)),
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
connectionLogs.push(
|
||||
createConnectionLog("error", "docker_connecting", "Host not found"),
|
||||
);
|
||||
return res.status(404).json({ error: "Host not found", connectionLogs });
|
||||
}
|
||||
|
||||
const host = hosts[0] as unknown as SSHHost;
|
||||
const host = hostResults[0] as unknown as SSHHost;
|
||||
|
||||
if (host.userId !== userId) {
|
||||
const { PermissionManager } =
|
||||
@@ -605,11 +658,18 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
if (pendingTOTPSessions[sessionId]) {
|
||||
try {
|
||||
pendingTOTPSessions[sessionId].client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
}
|
||||
|
||||
let resolvedCredentials: any = {
|
||||
let resolvedCredentials: {
|
||||
password?: string;
|
||||
sshKey?: string;
|
||||
keyPassword?: string;
|
||||
authType?: string;
|
||||
} = {
|
||||
password: host.password,
|
||||
sshKey: host.key,
|
||||
keyPassword: host.keyPassword,
|
||||
@@ -673,11 +733,10 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -685,8 +744,8 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
|
||||
const client = new SSHClient();
|
||||
|
||||
const config: any = {
|
||||
host: host.ip,
|
||||
const config: Record<string, unknown> = {
|
||||
host: host.ip?.replace(/^\[|\]$/g, "") || host.ip,
|
||||
port: host.port || 22,
|
||||
username: host.username,
|
||||
tryKeyboard: true,
|
||||
@@ -706,6 +765,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
};
|
||||
|
||||
if (resolvedCredentials.authType === "none") {
|
||||
// no credentials needed
|
||||
} else if (resolvedCredentials.authType === "password") {
|
||||
if (resolvedCredentials.password) {
|
||||
config.password = resolvedCredentials.password;
|
||||
@@ -871,8 +931,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
}
|
||||
|
||||
let responseSent = false;
|
||||
let keyboardInteractiveResponded = false;
|
||||
|
||||
connectionLogs.push(
|
||||
createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`),
|
||||
);
|
||||
@@ -1089,8 +1147,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
if (responseSent) return;
|
||||
responseSent = true;
|
||||
|
||||
keyboardInteractiveResponded = true;
|
||||
|
||||
pendingTOTPSessions[sessionId] = {
|
||||
client,
|
||||
finish,
|
||||
@@ -1158,8 +1214,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardInteractiveResponded = true;
|
||||
|
||||
pendingTOTPSessions[sessionId] = {
|
||||
client,
|
||||
finish,
|
||||
@@ -1244,8 +1298,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardInteractiveResponded = true;
|
||||
|
||||
pendingTOTPSessions[sessionId] = {
|
||||
client,
|
||||
finish,
|
||||
@@ -1283,121 +1335,160 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
const proxyConfig: SOCKS5Config | null =
|
||||
useSocks5 &&
|
||||
(socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0))
|
||||
) {
|
||||
try {
|
||||
connectionLogs.push(
|
||||
createConnectionLog("info", "proxy", "Connecting via SOCKS5 proxy"),
|
||||
);
|
||||
const socks5Socket = await createSocks5Connection(
|
||||
host.ip,
|
||||
host.port || 22,
|
||||
{
|
||||
(socks5Host ||
|
||||
(socks5ProxyChain && (socks5ProxyChain as ProxyNode[]).length > 0))
|
||||
? {
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
socks5Port,
|
||||
socks5Username,
|
||||
socks5Password,
|
||||
socks5ProxyChain: socks5ProxyChain as any,
|
||||
},
|
||||
socks5ProxyChain: socks5ProxyChain as ProxyNode[],
|
||||
}
|
||||
: null;
|
||||
|
||||
const hasJumpHosts = host.jumpHosts && host.jumpHosts.length > 0;
|
||||
|
||||
if (hasJumpHosts) {
|
||||
try {
|
||||
if (proxyConfig) {
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"proxy",
|
||||
"Connecting via proxy + jump hosts",
|
||||
),
|
||||
);
|
||||
}
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"jump",
|
||||
`Connecting via ${host.jumpHosts!.length} jump host(s)`,
|
||||
),
|
||||
);
|
||||
const jumpClient = await createJumpHostChain(
|
||||
host.jumpHosts as Array<{ hostId: number }>,
|
||||
userId,
|
||||
proxyConfig,
|
||||
);
|
||||
|
||||
if (socks5Socket) {
|
||||
config.sock = socks5Socket;
|
||||
client.connect(config);
|
||||
return;
|
||||
if (!jumpClient) {
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"error",
|
||||
"jump",
|
||||
"Failed to establish jump host chain",
|
||||
),
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Failed to establish jump host chain",
|
||||
connectionLogs,
|
||||
});
|
||||
}
|
||||
} catch (socks5Error) {
|
||||
sshLogger.error("SOCKS5 connection failed", socks5Error, {
|
||||
operation: "docker_socks5_connect",
|
||||
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
host.ip,
|
||||
host.port || 22,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
sshLogger.error("Failed to forward through jump host", err, {
|
||||
operation: "docker_jump_forward",
|
||||
sessionId,
|
||||
hostId,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"error",
|
||||
"jump",
|
||||
`Failed to forward through jump host: ${err.message}`,
|
||||
),
|
||||
);
|
||||
jumpClient.end();
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
return res.status(500).json({
|
||||
error: "Failed to forward through jump host: " + err.message,
|
||||
connectionLogs,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
config.sock = stream;
|
||||
client.connect(config);
|
||||
},
|
||||
);
|
||||
} catch (jumpError) {
|
||||
sshLogger.error("Jump host connection failed", jumpError, {
|
||||
operation: "docker_jump_connect",
|
||||
sessionId,
|
||||
hostId,
|
||||
proxyHost: socks5Host,
|
||||
proxyPort: socks5Port || 1080,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"error",
|
||||
"proxy",
|
||||
`SOCKS5 proxy connection failed: ${socks5Error instanceof Error ? socks5Error.message : "Unknown error"}`,
|
||||
"jump",
|
||||
`Jump host connection failed: ${jumpError instanceof Error ? jumpError.message : "Unknown error"}`,
|
||||
),
|
||||
);
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
return res.status(500).json({
|
||||
error:
|
||||
"SOCKS5 proxy connection failed: " +
|
||||
(socks5Error instanceof Error
|
||||
? socks5Error.message
|
||||
"Jump host connection failed: " +
|
||||
(jumpError instanceof Error
|
||||
? jumpError.message
|
||||
: "Unknown error"),
|
||||
connectionLogs,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (host.jumpHosts && host.jumpHosts.length > 0) {
|
||||
} else if (proxyConfig) {
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"jump",
|
||||
`Connecting via ${host.jumpHosts.length} jump host(s)`,
|
||||
),
|
||||
createConnectionLog("info", "proxy", "Connecting via proxy"),
|
||||
);
|
||||
const jumpClient = await createJumpHostChain(
|
||||
host.jumpHosts as Array<{ hostId: number }>,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (!jumpClient) {
|
||||
try {
|
||||
const proxySocket = await createSocks5Connection(
|
||||
host.ip,
|
||||
host.port || 22,
|
||||
proxyConfig,
|
||||
);
|
||||
if (proxySocket) {
|
||||
config.sock = proxySocket;
|
||||
}
|
||||
client.connect(config);
|
||||
} catch (proxyError) {
|
||||
sshLogger.error("Proxy connection failed", proxyError, {
|
||||
operation: "docker_proxy_connect",
|
||||
sessionId,
|
||||
hostId,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"error",
|
||||
"jump",
|
||||
"Failed to establish jump host chain",
|
||||
"proxy",
|
||||
`Proxy connection failed: ${proxyError instanceof Error ? proxyError.message : "Unknown error"}`,
|
||||
),
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Failed to establish jump host chain",
|
||||
connectionLogs,
|
||||
});
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
return res.status(500).json({
|
||||
error:
|
||||
"Proxy connection failed: " +
|
||||
(proxyError instanceof Error
|
||||
? proxyError.message
|
||||
: "Unknown error"),
|
||||
connectionLogs,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
host.ip,
|
||||
host.port || 22,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
sshLogger.error("Failed to forward through jump host", err, {
|
||||
operation: "docker_jump_forward",
|
||||
sessionId,
|
||||
hostId,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"error",
|
||||
"jump",
|
||||
`Failed to forward through jump host: ${err.message}`,
|
||||
),
|
||||
);
|
||||
jumpClient.end();
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
return res.status(500).json({
|
||||
error: "Failed to forward through jump host: " + err.message,
|
||||
connectionLogs,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
config.sock = stream;
|
||||
client.connect(config);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
client.connect(config);
|
||||
}
|
||||
@@ -1489,7 +1580,7 @@ app.post("/docker/ssh/disconnect", async (req, res) => {
|
||||
*/
|
||||
app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
const { sessionId, totpCode } = req.body;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
sshLogger.error("TOTP verification rejected: no authenticated user", {
|
||||
@@ -1521,7 +1612,9 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
sshLogger.warn("TOTP session timeout before code submission", {
|
||||
operation: "docker_totp_verify",
|
||||
sessionId,
|
||||
@@ -1544,7 +1637,19 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
});
|
||||
|
||||
let responseSent = false;
|
||||
let responseTimeout: NodeJS.Timeout;
|
||||
|
||||
const responseTimeout = setTimeout(() => {
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
sshLogger.warn("TOTP verification timeout", {
|
||||
operation: "docker_totp_verify",
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
res.status(408).json({ error: "TOTP verification timeout" });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
session.client.once("ready", () => {
|
||||
if (responseSent) return;
|
||||
@@ -1571,14 +1676,14 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1586,8 +1691,8 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
await axios.post(
|
||||
@@ -1633,19 +1738,6 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
res.status(401).json({ status: "error", message: "Invalid TOTP code" });
|
||||
});
|
||||
|
||||
responseTimeout = setTimeout(() => {
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
sshLogger.warn("TOTP verification timeout", {
|
||||
operation: "docker_totp_verify",
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
res.status(408).json({ error: "TOTP verification timeout" });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
session.finish(responses);
|
||||
});
|
||||
|
||||
@@ -1679,7 +1771,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
*/
|
||||
app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
const { sessionId } = req.body;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
sshLogger.error("Warpgate verification rejected: no authenticated user", {
|
||||
@@ -1715,7 +1807,9 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
sshLogger.warn("Warpgate session timeout before completion", {
|
||||
operation: "docker_warpgate_verify",
|
||||
sessionId,
|
||||
@@ -1728,7 +1822,19 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
}
|
||||
|
||||
let responseSent = false;
|
||||
let responseTimeout: NodeJS.Timeout;
|
||||
|
||||
const responseTimeout = setTimeout(() => {
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
sshLogger.warn("Warpgate verification timeout", {
|
||||
operation: "docker_warpgate_verify",
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
res.status(408).json({ error: "Warpgate verification timeout" });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
session.client.once("ready", () => {
|
||||
if (responseSent) return;
|
||||
@@ -1755,14 +1861,14 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1770,8 +1876,8 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
await axios.post(
|
||||
@@ -1819,19 +1925,6 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
.json({ status: "error", message: "Warpgate authentication failed" });
|
||||
});
|
||||
|
||||
responseTimeout = setTimeout(() => {
|
||||
if (!responseSent) {
|
||||
responseSent = true;
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
sshLogger.warn("Warpgate verification timeout", {
|
||||
operation: "docker_warpgate_verify",
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
res.status(408).json({ error: "Warpgate verification timeout" });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
session.finish([""]);
|
||||
});
|
||||
|
||||
@@ -1941,7 +2034,7 @@ app.get("/docker/ssh/status", async (req, res) => {
|
||||
*/
|
||||
app.get("/docker/validate/:sessionId", async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2020,7 +2113,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
|
||||
code: "DOCKER_ERROR",
|
||||
});
|
||||
}
|
||||
} catch (installError) {
|
||||
} catch {
|
||||
session.activeOperations--;
|
||||
return res.json({
|
||||
available: false,
|
||||
@@ -2073,7 +2166,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
|
||||
app.get("/docker/containers/:sessionId", async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const all = req.query.all !== "false";
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2115,7 +2208,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
sshLogger.warn("Failed to parse container line", {
|
||||
operation: "parse_container",
|
||||
line,
|
||||
@@ -2174,7 +2267,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
|
||||
*/
|
||||
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2269,7 +2362,7 @@ app.post(
|
||||
"/docker/containers/:sessionId/:containerId/start",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2369,7 +2462,7 @@ app.post(
|
||||
"/docker/containers/:sessionId/:containerId/stop",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2469,7 +2562,7 @@ app.post(
|
||||
"/docker/containers/:sessionId/:containerId/restart",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2569,7 +2662,7 @@ app.post(
|
||||
"/docker/containers/:sessionId/:containerId/pause",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2669,7 +2762,7 @@ app.post(
|
||||
"/docker/containers/:sessionId/:containerId/unpause",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2774,7 +2867,7 @@ app.delete(
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const force = req.query.force === "true";
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -2902,7 +2995,7 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
|
||||
const timestamps = req.query.timestamps === "true";
|
||||
const since = req.query.since as string;
|
||||
const until = req.query.until as string;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
@@ -3011,7 +3104,7 @@ app.get(
|
||||
"/docker/containers/:sessionId/:containerId/stats",
|
||||
async (req, res) => {
|
||||
const { sessionId, containerId } = req.params;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as unknown as { userId: string }).userId;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
|
||||
+1056
-758
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
||||
import type { WebSocket } from "ws";
|
||||
import { db } from "../database/db/index.js";
|
||||
import { sshData } from "../database/db/schema.js";
|
||||
import { hosts } from "../database/db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
interface HostKeyVerificationData {
|
||||
scenario: "new" | "changed";
|
||||
@@ -22,17 +21,6 @@ interface VerificationResponse {
|
||||
}
|
||||
|
||||
export class SSHHostKeyVerifier {
|
||||
/**
|
||||
* Creates a hostVerifier callback for ssh2 Client.connect()
|
||||
*
|
||||
* @param hostId - Database ID of the host (null for quick connect)
|
||||
* @param ip - IP address or hostname
|
||||
* @param port - SSH port
|
||||
* @param ws - WebSocket for user prompts (null for non-interactive connections)
|
||||
* @param userId - User ID for logging
|
||||
* @param isJumpHost - If true, auto-accepts without prompting
|
||||
* @returns async hostVerifier callback
|
||||
*/
|
||||
static async createHostVerifier(
|
||||
hostId: number | null,
|
||||
ip: string,
|
||||
@@ -64,8 +52,8 @@ export class SSHHostKeyVerifier {
|
||||
return;
|
||||
}
|
||||
|
||||
const host = await db.query.sshData.findFirst({
|
||||
where: eq(sshData.id, hostId),
|
||||
const host = await db.query.hosts.findFirst({
|
||||
where: eq(hosts.id, hostId),
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
@@ -154,11 +142,11 @@ export class SSHHostKeyVerifier {
|
||||
|
||||
if (host.hostKeyFingerprint === fingerprint) {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
|
||||
sshLogger.info("Host key verified successfully", {
|
||||
operation: "host_key_verified",
|
||||
@@ -288,7 +276,7 @@ export class SSHHostKeyVerifier {
|
||||
algorithm: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyFingerprint: fingerprint,
|
||||
hostKeyType: keyType,
|
||||
@@ -296,7 +284,7 @@ export class SSHHostKeyVerifier {
|
||||
hostKeyFirstSeen: new Date().toISOString(),
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
}
|
||||
|
||||
private static async updateHostKey(
|
||||
@@ -307,7 +295,7 @@ export class SSHHostKeyVerifier {
|
||||
currentChangeCount: number,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyFingerprint: fingerprint,
|
||||
hostKeyType: keyType,
|
||||
@@ -315,7 +303,7 @@ export class SSHHostKeyVerifier {
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
hostKeyChangedCount: currentChangeCount + 1,
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
}
|
||||
|
||||
private static async promptUserForNewKey(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { FieldCrypto } from "../utils/field-crypto.js";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
import { getRequestOrigin } from "../utils/request-origin.js";
|
||||
|
||||
const AUTH_TIMEOUT = 60 * 1000;
|
||||
|
||||
@@ -48,41 +49,6 @@ interface OPKSSHAuthSession {
|
||||
const activeAuthSessions = new Map<string, OPKSSHAuthSession>();
|
||||
const cleanupInProgress = new Set<string>();
|
||||
|
||||
export function getRequestOrigin(req: IncomingMessage): string {
|
||||
const protoHeader =
|
||||
req.headers["x-forwarded-proto"] ||
|
||||
((req.socket as any).encrypted ? "https" : "http");
|
||||
const proto =
|
||||
typeof protoHeader === "string"
|
||||
? protoHeader.split(",")[0].trim()
|
||||
: String(protoHeader);
|
||||
|
||||
const portHeader = req.headers["x-forwarded-port"];
|
||||
const port =
|
||||
typeof portHeader === "string"
|
||||
? portHeader.split(",")[0].trim()
|
||||
: undefined;
|
||||
|
||||
const hostHeaderRaw =
|
||||
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
|
||||
const hostHeader =
|
||||
typeof hostHeaderRaw === "string"
|
||||
? hostHeaderRaw.split(",")[0].trim()
|
||||
: String(hostHeaderRaw);
|
||||
|
||||
if (port) {
|
||||
const hostWithoutPort = hostHeader.split(":")[0];
|
||||
const isDefaultPort =
|
||||
(proto === "http" && port === "80") ||
|
||||
(proto === "https" && port === "443");
|
||||
return isDefaultPort
|
||||
? `${proto}://${hostWithoutPort}`
|
||||
: `${proto}://${hostWithoutPort}:${port}`;
|
||||
}
|
||||
|
||||
return `${proto}://${hostHeader}`;
|
||||
}
|
||||
|
||||
function getOPKConfigPath(): string {
|
||||
const dataDir =
|
||||
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
|
||||
@@ -157,7 +123,7 @@ async function checkOPKConfigExists(): Promise<{
|
||||
return {
|
||||
exists: false,
|
||||
configPath,
|
||||
error: `OPKSSH configuration is missing 'redirect_uris' field.`,
|
||||
error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/host/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,7 +179,7 @@ export async function startOPKSSHAuth(
|
||||
}
|
||||
|
||||
const requestId = randomUUID();
|
||||
const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`;
|
||||
const remoteRedirectUri = `${requestOrigin}/host/opkssh-callback`;
|
||||
|
||||
const session: Partial<OPKSSHAuthSession> = {
|
||||
requestId,
|
||||
@@ -235,11 +201,11 @@ export async function startOPKSSHAuth(
|
||||
try {
|
||||
const binaryPath = OPKSSHBinaryManager.getBinaryPath();
|
||||
const configPath = getOPKConfigPath();
|
||||
const configDir = path.dirname(configPath);
|
||||
|
||||
const args = [
|
||||
"login",
|
||||
"--print-key",
|
||||
"--disable-browser-open",
|
||||
`--config-path=${configPath}`,
|
||||
`--remote-redirect-uri=${remoteRedirectUri}`,
|
||||
];
|
||||
@@ -284,7 +250,10 @@ export async function startOPKSSHAuth(
|
||||
opksshProcess.stderr?.on("data", async (data) => {
|
||||
const stderr = data.toString();
|
||||
|
||||
if (stderr.includes("Opening browser to")) {
|
||||
if (
|
||||
stderr.includes("Opening browser to") ||
|
||||
stderr.includes("Open your browser to:")
|
||||
) {
|
||||
handleOPKSSHOutput(requestId, stderr);
|
||||
}
|
||||
|
||||
@@ -371,7 +340,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
|
||||
session.stdoutBuffer += output;
|
||||
|
||||
const chooserUrlMatch = session.stdoutBuffer.match(
|
||||
/Opening browser to http:\/\/localhost:(\d+)\/chooser/,
|
||||
/(?:Opening browser to|Open your browser to:)\s*http:\/\/localhost:(\d+)\/chooser/,
|
||||
);
|
||||
if (chooserUrlMatch && session.status === "starting") {
|
||||
const actualPort = parseInt(chooserUrlMatch[1], 10);
|
||||
@@ -383,7 +352,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
|
||||
/\/ssh\/opkssh-callback$/,
|
||||
"",
|
||||
);
|
||||
const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`;
|
||||
const proxiedChooserUrl = `${baseUrl}/host/opkssh-chooser/${requestId}`;
|
||||
|
||||
session.status = "waiting_for_auth";
|
||||
session.ws.send(
|
||||
@@ -784,7 +753,10 @@ export function getActiveSessionsAll(): OPKSSHAuthSession[] {
|
||||
return Array.from(activeAuthSessions.values());
|
||||
}
|
||||
|
||||
export async function getUserIdFromRequest(req: any): Promise<string | null> {
|
||||
export async function getUserIdFromRequest(req: {
|
||||
cookies?: Record<string, string>;
|
||||
headers: Record<string, string | undefined>;
|
||||
}): Promise<string | null> {
|
||||
try {
|
||||
const { AuthManager } = await import("../utils/auth-manager.js");
|
||||
const authManager = AuthManager.getInstance();
|
||||
@@ -797,7 +769,7 @@ export async function getUserIdFromRequest(req: any): Promise<string | null> {
|
||||
|
||||
const decoded = await authManager.verifyJWTToken(token);
|
||||
return decoded?.userId || null;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+620
-454
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
import { Client } from "ssh2";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
|
||||
interface PooledConnection {
|
||||
client: Client;
|
||||
lastUsed: number;
|
||||
inUse: boolean;
|
||||
hostKey: string;
|
||||
}
|
||||
|
||||
class SSHConnectionPool {
|
||||
private connections = new Map<string, PooledConnection[]>();
|
||||
private maxConnectionsPerHost = 3;
|
||||
private cleanupInterval: NodeJS.Timeout;
|
||||
|
||||
constructor() {
|
||||
this.cleanupInterval = setInterval(
|
||||
() => {
|
||||
this.cleanup();
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
private isConnectionHealthy(client: Client): boolean {
|
||||
try {
|
||||
const sock = (
|
||||
client as unknown as {
|
||||
_sock?: { destroyed?: boolean; writable?: boolean };
|
||||
}
|
||||
)._sock;
|
||||
if (sock && (sock.destroyed || !sock.writable)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getConnection(
|
||||
key: string,
|
||||
factory: () => Promise<Client>,
|
||||
): Promise<Client> {
|
||||
let connections = this.connections.get(key) || [];
|
||||
|
||||
const available = connections.find((conn) => !conn.inUse);
|
||||
if (available) {
|
||||
if (!this.isConnectionHealthy(available.client)) {
|
||||
sshLogger.warn("Removing unhealthy connection from pool", {
|
||||
operation: "pool_remove_dead",
|
||||
hostKey: key,
|
||||
});
|
||||
try {
|
||||
available.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
connections = connections.filter((c) => c !== available);
|
||||
this.connections.set(key, connections);
|
||||
} else {
|
||||
available.inUse = true;
|
||||
available.lastUsed = Date.now();
|
||||
return available.client;
|
||||
}
|
||||
}
|
||||
|
||||
if (connections.length < this.maxConnectionsPerHost) {
|
||||
const client = await factory();
|
||||
const pooled: PooledConnection = {
|
||||
client,
|
||||
lastUsed: Date.now(),
|
||||
inUse: true,
|
||||
hostKey: key,
|
||||
};
|
||||
connections.push(pooled);
|
||||
this.connections.set(key, connections);
|
||||
|
||||
client.on("end", () => {
|
||||
this.removeConnection(key, client);
|
||||
});
|
||||
client.on("close", () => {
|
||||
this.removeConnection(key, client);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const checkAvailable = () => {
|
||||
const conns = this.connections.get(key) || [];
|
||||
const avail = conns.find((conn) => !conn.inUse);
|
||||
if (avail) {
|
||||
if (!this.isConnectionHealthy(avail.client)) {
|
||||
try {
|
||||
avail.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
const filtered = conns.filter((c) => c !== avail);
|
||||
this.connections.set(key, filtered);
|
||||
factory().then((client) => {
|
||||
const pooled: PooledConnection = {
|
||||
client,
|
||||
lastUsed: Date.now(),
|
||||
inUse: true,
|
||||
hostKey: key,
|
||||
};
|
||||
filtered.push(pooled);
|
||||
this.connections.set(key, filtered);
|
||||
client.on("end", () => this.removeConnection(key, client));
|
||||
client.on("close", () => this.removeConnection(key, client));
|
||||
resolve(client);
|
||||
});
|
||||
} else {
|
||||
avail.inUse = true;
|
||||
avail.lastUsed = Date.now();
|
||||
resolve(avail.client);
|
||||
}
|
||||
} else {
|
||||
setTimeout(checkAvailable, 100);
|
||||
}
|
||||
};
|
||||
checkAvailable();
|
||||
});
|
||||
}
|
||||
|
||||
releaseConnection(key: string, client: Client): void {
|
||||
const connections = this.connections.get(key) || [];
|
||||
const pooled = connections.find((conn) => conn.client === client);
|
||||
if (pooled) {
|
||||
pooled.inUse = false;
|
||||
pooled.lastUsed = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
private removeConnection(key: string, client: Client): void {
|
||||
const connections = this.connections.get(key);
|
||||
if (!connections) return;
|
||||
const filtered = connections.filter((c) => c.client !== client);
|
||||
if (filtered.length === 0) {
|
||||
this.connections.delete(key);
|
||||
} else {
|
||||
this.connections.set(key, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
clearKeyConnections(key: string): void {
|
||||
const connections = this.connections.get(key) || [];
|
||||
for (const conn of connections) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
this.connections.delete(key);
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
const maxAge = 10 * 60 * 1000;
|
||||
|
||||
for (const [hostKey, connections] of this.connections.entries()) {
|
||||
const activeConnections = connections.filter((conn) => {
|
||||
if (!conn.inUse && now - conn.lastUsed > maxAge) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!this.isConnectionHealthy(conn.client)) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (activeConnections.length === 0) {
|
||||
this.connections.delete(hostKey);
|
||||
} else {
|
||||
this.connections.set(hostKey, activeConnections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearAllConnections(): void {
|
||||
for (const connections of this.connections.values()) {
|
||||
for (const conn of connections) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
}
|
||||
this.connections.clear();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.clearAllConnections();
|
||||
}
|
||||
}
|
||||
|
||||
export const connectionPool = new SSHConnectionPool();
|
||||
|
||||
export async function withConnection<T>(
|
||||
key: string,
|
||||
factory: () => Promise<Client>,
|
||||
fn: (client: Client) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const client = await connectionPool.getConnection(key, factory);
|
||||
try {
|
||||
return await fn(client);
|
||||
} finally {
|
||||
connectionPool.releaseConnection(key, client);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
import { type Client, type ClientChannel } from "ssh2";
|
||||
import { WebSocket } from "ws";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
|
||||
const MAX_BUFFER_BYTES = 512 * 1024;
|
||||
const DEFAULT_TIMEOUT_MINUTES = 30;
|
||||
const HEALTH_CHECK_INTERVAL_MS = 60_000;
|
||||
const MAX_SESSIONS_PER_USER = 10;
|
||||
|
||||
export interface TerminalSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
hostId: number;
|
||||
hostName: string;
|
||||
tabInstanceId?: string;
|
||||
attachedTabInstanceId?: string;
|
||||
|
||||
sshConn: Client | null;
|
||||
sshStream: ClientChannel | null;
|
||||
jumpClient: Client | null;
|
||||
opksshTempFiles: { keyPath: string; certPath: string } | null;
|
||||
|
||||
cols: number;
|
||||
rows: number;
|
||||
isConnected: boolean;
|
||||
createdAt: number;
|
||||
|
||||
attachedWs: WebSocket | null;
|
||||
lastDetachedAt: number | null;
|
||||
detachTimeout: NodeJS.Timeout | null;
|
||||
|
||||
outputBuffer: string[];
|
||||
outputBufferBytes: number;
|
||||
}
|
||||
|
||||
class TerminalSessionManager {
|
||||
private static instance: TerminalSessionManager;
|
||||
private sessions = new Map<string, TerminalSession>();
|
||||
private healthCheckTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.healthCheckTimer = setInterval(
|
||||
() => this.healthCheck(),
|
||||
HEALTH_CHECK_INTERVAL_MS,
|
||||
);
|
||||
}
|
||||
|
||||
static getInstance(): TerminalSessionManager {
|
||||
if (!TerminalSessionManager.instance) {
|
||||
TerminalSessionManager.instance = new TerminalSessionManager();
|
||||
}
|
||||
return TerminalSessionManager.instance;
|
||||
}
|
||||
|
||||
createSession(
|
||||
userId: string,
|
||||
hostId: number,
|
||||
hostName: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
tabInstanceId?: string,
|
||||
): string {
|
||||
const userSessions = this.getUserSessions(userId);
|
||||
if (userSessions.length >= MAX_SESSIONS_PER_USER) {
|
||||
const detached = userSessions
|
||||
.filter((s) => s.attachedWs === null)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.lastDetachedAt ?? a.createdAt) -
|
||||
(b.lastDetachedAt ?? b.createdAt),
|
||||
);
|
||||
if (detached.length > 0) {
|
||||
this.destroySession(detached[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
if (tabInstanceId) {
|
||||
const tabSessions = userSessions.filter(
|
||||
(s) => s.tabInstanceId === tabInstanceId,
|
||||
);
|
||||
if (tabSessions.length > 0) {
|
||||
sshLogger.warn("Tab instance already has session, destroying old", {
|
||||
operation: "session_tab_duplicate_cleanup",
|
||||
existingSessionId: tabSessions[0].id,
|
||||
tabInstanceId,
|
||||
});
|
||||
this.destroySession(tabSessions[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const session: TerminalSession = {
|
||||
id,
|
||||
userId,
|
||||
hostId,
|
||||
hostName,
|
||||
tabInstanceId,
|
||||
sshConn: null,
|
||||
sshStream: null,
|
||||
jumpClient: null,
|
||||
opksshTempFiles: null,
|
||||
cols,
|
||||
rows,
|
||||
isConnected: false,
|
||||
createdAt: Date.now(),
|
||||
attachedWs: null,
|
||||
lastDetachedAt: null,
|
||||
detachTimeout: null,
|
||||
outputBuffer: [],
|
||||
outputBufferBytes: 0,
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
|
||||
sshLogger.info("Terminal session created", {
|
||||
operation: "session_created",
|
||||
sessionId: id,
|
||||
userId,
|
||||
hostId,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
getSession(sessionId: string | null): TerminalSession | null {
|
||||
if (!sessionId) return null;
|
||||
return this.sessions.get(sessionId) ?? null;
|
||||
}
|
||||
|
||||
setSSHState(
|
||||
sessionId: string,
|
||||
conn: Client,
|
||||
stream: ClientChannel,
|
||||
jumpClient?: Client | null,
|
||||
opksshTempFiles?: { keyPath: string; certPath: string } | null,
|
||||
): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
session.sshConn = conn;
|
||||
session.sshStream = stream;
|
||||
session.jumpClient = jumpClient ?? null;
|
||||
session.opksshTempFiles = opksshTempFiles ?? null;
|
||||
session.isConnected = true;
|
||||
}
|
||||
|
||||
attachWs(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
ws: WebSocket,
|
||||
tabInstanceId?: string,
|
||||
): TerminalSession | null {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
sshLogger.warn("Session not found for attachment", {
|
||||
operation: "session_attach_not_found",
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (session.userId !== userId) {
|
||||
sshLogger.warn("Session userId mismatch", {
|
||||
operation: "session_attach_user_mismatch",
|
||||
sessionId,
|
||||
expectedUserId: session.userId,
|
||||
providedUserId: userId,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!session.isConnected) {
|
||||
sshLogger.warn("Session not connected", {
|
||||
operation: "session_attach_not_connected",
|
||||
sessionId,
|
||||
userId,
|
||||
createdAt: session.createdAt,
|
||||
elapsed: Date.now() - session.createdAt,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDetached =
|
||||
!session.attachedWs || session.attachedWs.readyState !== WebSocket.OPEN;
|
||||
const isOriginalTab = session.tabInstanceId === tabInstanceId;
|
||||
|
||||
if (
|
||||
!isDetached &&
|
||||
!isOriginalTab &&
|
||||
session.tabInstanceId &&
|
||||
tabInstanceId
|
||||
) {
|
||||
sshLogger.warn("Session actively attached to different tab instance", {
|
||||
operation: "session_attach_instance_conflict",
|
||||
sessionId,
|
||||
sessionInstanceId: session.tabInstanceId,
|
||||
providedInstanceId: tabInstanceId,
|
||||
});
|
||||
try {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "sessionExpired",
|
||||
sessionId,
|
||||
message: "Session belongs to a different tab instance",
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
session.tabInstanceId &&
|
||||
tabInstanceId &&
|
||||
session.tabInstanceId !== tabInstanceId
|
||||
) {
|
||||
sshLogger.info(
|
||||
"Session attached to different tab instance (split-screen)",
|
||||
{
|
||||
operation: "session_attach_split_screen",
|
||||
originalInstanceId: session.tabInstanceId,
|
||||
newInstanceId: tabInstanceId,
|
||||
sessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (session.attachedWs && session.attachedWs !== ws) {
|
||||
try {
|
||||
session.attachedWs.send(
|
||||
JSON.stringify({
|
||||
type: "sessionTakenOver",
|
||||
sessionId,
|
||||
message: "Session was attached from another tab",
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
session.attachedWs = null;
|
||||
}
|
||||
|
||||
if (session.detachTimeout) {
|
||||
clearTimeout(session.detachTimeout);
|
||||
session.detachTimeout = null;
|
||||
}
|
||||
|
||||
session.attachedWs = ws;
|
||||
session.attachedTabInstanceId = tabInstanceId;
|
||||
session.lastDetachedAt = null;
|
||||
|
||||
sshLogger.info("WebSocket attached to session", {
|
||||
operation: "session_attach",
|
||||
sessionId,
|
||||
userId,
|
||||
tabInstanceId,
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
detachWs(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.detachTimeout) {
|
||||
clearTimeout(session.detachTimeout);
|
||||
session.detachTimeout = null;
|
||||
}
|
||||
|
||||
session.attachedWs = null;
|
||||
session.lastDetachedAt = Date.now();
|
||||
|
||||
const timeoutMs = this.getTimeoutMs();
|
||||
|
||||
session.detachTimeout = setTimeout(() => {
|
||||
sshLogger.info("Session idle timeout expired", {
|
||||
operation: "session_idle_timeout",
|
||||
sessionId,
|
||||
userId: session.userId,
|
||||
});
|
||||
this.destroySession(sessionId);
|
||||
}, timeoutMs);
|
||||
|
||||
sshLogger.info("WebSocket detached from session", {
|
||||
operation: "session_detach",
|
||||
sessionId,
|
||||
userId: session.userId,
|
||||
timeoutMinutes: timeoutMs / 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
destroySession(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.detachTimeout) {
|
||||
clearTimeout(session.detachTimeout);
|
||||
session.detachTimeout = null;
|
||||
}
|
||||
|
||||
if (session.sshStream) {
|
||||
try {
|
||||
session.sshStream.end();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
session.sshStream = null;
|
||||
}
|
||||
|
||||
if (session.sshConn) {
|
||||
try {
|
||||
session.sshConn.end();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
session.sshConn = null;
|
||||
}
|
||||
|
||||
if (session.jumpClient) {
|
||||
try {
|
||||
session.jumpClient.end();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
session.jumpClient = null;
|
||||
}
|
||||
|
||||
if (session.opksshTempFiles) {
|
||||
const tempFiles = session.opksshTempFiles;
|
||||
session.opksshTempFiles = null;
|
||||
this.cleanupOpksshFiles(tempFiles);
|
||||
}
|
||||
|
||||
session.isConnected = false;
|
||||
session.outputBuffer = [];
|
||||
session.outputBufferBytes = 0;
|
||||
|
||||
this.sessions.delete(sessionId);
|
||||
|
||||
sshLogger.info("Terminal session destroyed", {
|
||||
operation: "session_destroyed",
|
||||
sessionId,
|
||||
userId: session.userId,
|
||||
hostId: session.hostId,
|
||||
});
|
||||
}
|
||||
|
||||
getUserSessions(userId: string): TerminalSession[] {
|
||||
const result: TerminalSession[] = [];
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.userId === userId) {
|
||||
result.push(session);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bufferOutput(sessionId: string, data: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
session.outputBuffer.push(data);
|
||||
session.outputBufferBytes += data.length;
|
||||
|
||||
while (
|
||||
session.outputBufferBytes > MAX_BUFFER_BYTES &&
|
||||
session.outputBuffer.length > 0
|
||||
) {
|
||||
const removed = session.outputBuffer.shift();
|
||||
if (removed) session.outputBufferBytes -= removed.length;
|
||||
}
|
||||
}
|
||||
|
||||
flushBuffer(session: TerminalSession): string | null {
|
||||
if (session.outputBuffer.length === 0) return null;
|
||||
const data = session.outputBuffer.join("");
|
||||
session.outputBuffer = [];
|
||||
session.outputBufferBytes = 0;
|
||||
return data;
|
||||
}
|
||||
|
||||
getBuffer(session: TerminalSession): string | null {
|
||||
if (session.outputBuffer.length === 0) return null;
|
||||
return session.outputBuffer.join("");
|
||||
}
|
||||
|
||||
private getTimeoutMs(): number {
|
||||
try {
|
||||
const db = getDb();
|
||||
const row = db.$client
|
||||
.prepare(
|
||||
"SELECT value FROM settings WHERE key = 'terminal_session_timeout_minutes'",
|
||||
)
|
||||
.get() as { value: string } | undefined;
|
||||
if (row) {
|
||||
const minutes = parseInt(row.value, 10);
|
||||
if (!isNaN(minutes) && minutes > 0) {
|
||||
return minutes * 60_000;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// DB not available, use default
|
||||
}
|
||||
return DEFAULT_TIMEOUT_MINUTES * 60_000;
|
||||
}
|
||||
|
||||
private healthCheck(): void {
|
||||
const toDestroy: string[] = [];
|
||||
const now = Date.now();
|
||||
const GRACE_PERIOD_MS = 10_000;
|
||||
|
||||
for (const [id, session] of this.sessions) {
|
||||
if (!session.isConnected) continue;
|
||||
|
||||
if (
|
||||
session.attachedWs &&
|
||||
session.attachedWs.readyState === WebSocket.OPEN
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (session.sshStream?.destroyed) {
|
||||
const detachedDuration = session.lastDetachedAt
|
||||
? now - session.lastDetachedAt
|
||||
: 0;
|
||||
|
||||
if (detachedDuration > GRACE_PERIOD_MS) {
|
||||
sshLogger.info(
|
||||
"SSH stream destroyed during detach window, cleaning up",
|
||||
{
|
||||
operation: "session_health_check_stream_destroyed",
|
||||
sessionId: id,
|
||||
userId: session.userId,
|
||||
detachedFor: detachedDuration,
|
||||
},
|
||||
);
|
||||
toDestroy.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.sshConn) {
|
||||
toDestroy.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toDestroy) {
|
||||
this.destroySession(id);
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupOpksshFiles(tempFiles: {
|
||||
keyPath: string;
|
||||
certPath: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { promises: fs } = await import("fs");
|
||||
const results = await Promise.allSettled([
|
||||
fs.unlink(tempFiles.keyPath),
|
||||
fs.unlink(tempFiles.certPath),
|
||||
]);
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
sshLogger.warn("Failed to cleanup OPKSSH temp file", {
|
||||
operation: "opkssh_temp_cleanup_failed",
|
||||
file: index === 0 ? "keyPath" : "certPath",
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to cleanup OPKSSH temp files", {
|
||||
operation: "opkssh_temp_cleanup_error",
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroyAll(): void {
|
||||
for (const id of [...this.sessions.keys()]) {
|
||||
this.destroySession(id);
|
||||
}
|
||||
if (this.healthCheckTimer) {
|
||||
clearInterval(this.healthCheckTimer);
|
||||
this.healthCheckTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionManager = TerminalSessionManager.getInstance();
|
||||
+615
-288
File diff suppressed because it is too large
Load Diff
+205
-236
@@ -6,7 +6,7 @@ import { ChildProcess } from "child_process";
|
||||
import axios from "axios";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type {
|
||||
SSHHost,
|
||||
TunnelConfig,
|
||||
@@ -16,13 +16,14 @@ import type {
|
||||
AuthenticatedRequest,
|
||||
} from "../../types/index.js";
|
||||
import { CONNECTION_STATES } from "../../types/index.js";
|
||||
import { tunnelLogger, sshLogger } from "../utils/logger.js";
|
||||
import { tunnelLogger } from "../utils/logger.js";
|
||||
import { SystemCrypto } from "../utils/system-crypto.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { DataCrypto } from "../utils/data-crypto.js";
|
||||
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { PermissionManager } from "../utils/permission-manager.js";
|
||||
import { withConnection } from "./ssh-connection-pool.js";
|
||||
|
||||
const app = express();
|
||||
app.use(
|
||||
@@ -61,6 +62,10 @@ app.use(
|
||||
);
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
const permissionManager = PermissionManager.getInstance();
|
||||
@@ -655,16 +660,10 @@ async function connectSSHTunnel(
|
||||
const credential = credentials[0];
|
||||
resolvedSourceCredentials = {
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password ||
|
||||
credential.keyPassword) as string | undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type ||
|
||||
credential.authType) as string,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authMethod: credential.authType as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -746,16 +745,10 @@ async function connectSSHTunnel(
|
||||
const credential = credentials[0];
|
||||
resolvedEndpointCredentials = {
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type || credential.authType) as string,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authMethod: credential.authType as string,
|
||||
};
|
||||
} else {
|
||||
tunnelLogger.warn("No endpoint credentials found in database", {
|
||||
@@ -802,7 +795,9 @@ async function connectSSHTunnel(
|
||||
|
||||
try {
|
||||
conn.end();
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
activeTunnels.delete(tunnelName);
|
||||
|
||||
@@ -996,7 +991,9 @@ async function connectSSHTunnel(
|
||||
const verification = tunnelVerifications.get(tunnelName);
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
verification?.conn.end();
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -1110,7 +1107,8 @@ async function connectSSHTunnel(
|
||||
});
|
||||
|
||||
const connOptions: Record<string, unknown> = {
|
||||
host: tunnelConfig.sourceIP,
|
||||
host:
|
||||
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
tryKeyboard: true,
|
||||
@@ -1347,16 +1345,10 @@ async function killRemoteTunnelByMarker(
|
||||
const credential = credentials[0];
|
||||
resolvedSourceCredentials = {
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type || credential.authType) as string,
|
||||
sshKey: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authMethod: credential.authType as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1369,50 +1361,6 @@ async function killRemoteTunnelByMarker(
|
||||
}
|
||||
}
|
||||
|
||||
const conn = new Client();
|
||||
const connOptions: Record<string, unknown> = {
|
||||
host: tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 15000,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
],
|
||||
cipher: [
|
||||
"aes128-ctr",
|
||||
"aes192-ctr",
|
||||
"aes256-ctr",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes128-cbc",
|
||||
"aes192-cbc",
|
||||
"aes256-cbc",
|
||||
"3des-cbc",
|
||||
],
|
||||
hmac: [
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
resolvedSourceCredentials.authMethod === "key" &&
|
||||
resolvedSourceCredentials.sshKey
|
||||
@@ -1424,151 +1372,83 @@ async function killRemoteTunnelByMarker(
|
||||
callback(new Error("Invalid SSH key format"));
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanKey = resolvedSourceCredentials.sshKey
|
||||
.trim()
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
connOptions.privateKey = Buffer.from(cleanKey, "utf8");
|
||||
if (resolvedSourceCredentials.keyPassword) {
|
||||
connOptions.passphrase = resolvedSourceCredentials.keyPassword;
|
||||
}
|
||||
if (
|
||||
resolvedSourceCredentials.keyType &&
|
||||
resolvedSourceCredentials.keyType !== "auto"
|
||||
) {
|
||||
connOptions.privateKeyType = resolvedSourceCredentials.keyType;
|
||||
}
|
||||
} else {
|
||||
connOptions.password = resolvedSourceCredentials.password;
|
||||
}
|
||||
|
||||
conn.on("ready", () => {
|
||||
const tunnelType = tunnelConfig.tunnelType || "remote";
|
||||
const tunnelFlag = tunnelType === "local" ? "-L" : "-R";
|
||||
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*${tunnelFlag})' | grep -v grep`;
|
||||
const poolKey = `tunnel:${tunnelConfig.sourceUserId}:${tunnelConfig.sourceIP}:${tunnelConfig.sourceSSHPort}:${tunnelConfig.sourceUsername}`;
|
||||
|
||||
conn.exec(checkCmd, (_err, stream) => {
|
||||
let foundProcesses = false;
|
||||
const factory = async (): Promise<Client> => {
|
||||
const connOptions: Record<string, unknown> = {
|
||||
host:
|
||||
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 15000,
|
||||
algorithms: {
|
||||
kex: [
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
],
|
||||
cipher: [
|
||||
"aes128-ctr",
|
||||
"aes192-ctr",
|
||||
"aes256-ctr",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes128-cbc",
|
||||
"aes192-cbc",
|
||||
"aes256-cbc",
|
||||
"3des-cbc",
|
||||
],
|
||||
hmac: [
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
};
|
||||
|
||||
stream.on("data", (data) => {
|
||||
const output = data.toString().trim();
|
||||
if (output) {
|
||||
foundProcesses = true;
|
||||
}
|
||||
});
|
||||
if (
|
||||
resolvedSourceCredentials.authMethod === "key" &&
|
||||
resolvedSourceCredentials.sshKey
|
||||
) {
|
||||
const cleanKey = resolvedSourceCredentials.sshKey
|
||||
.trim()
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
connOptions.privateKey = Buffer.from(cleanKey, "utf8");
|
||||
if (resolvedSourceCredentials.keyPassword) {
|
||||
connOptions.passphrase = resolvedSourceCredentials.keyPassword;
|
||||
}
|
||||
if (
|
||||
resolvedSourceCredentials.keyType &&
|
||||
resolvedSourceCredentials.keyType !== "auto"
|
||||
) {
|
||||
connOptions.privateKeyType = resolvedSourceCredentials.keyType;
|
||||
}
|
||||
} else {
|
||||
connOptions.password = resolvedSourceCredentials.password;
|
||||
}
|
||||
|
||||
stream.on("close", () => {
|
||||
if (!foundProcesses) {
|
||||
tunnelLogger.warn("Remote tunnel process not found", {
|
||||
operation: "tunnel_remote_not_found",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
marker: tunnelMarker,
|
||||
});
|
||||
conn.end();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
tunnelLogger.info("Remote tunnel process found, proceeding to kill", {
|
||||
operation: "tunnel_remote_found",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
marker: tunnelMarker,
|
||||
});
|
||||
|
||||
const killCmds = [
|
||||
`pkill -TERM -f '${tunnelMarker}'`,
|
||||
`sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
|
||||
`sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`,
|
||||
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
|
||||
];
|
||||
|
||||
let commandIndex = 0;
|
||||
|
||||
function executeNextKillCommand() {
|
||||
if (commandIndex >= killCmds.length) {
|
||||
conn.exec(checkCmd, (_err, verifyStream) => {
|
||||
let stillRunning = false;
|
||||
|
||||
verifyStream.on("data", (data) => {
|
||||
const output = data.toString().trim();
|
||||
if (output) {
|
||||
stillRunning = true;
|
||||
tunnelLogger.warn(
|
||||
`Processes still running after cleanup for '${tunnelName}': ${output}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
verifyStream.on("close", () => {
|
||||
if (stillRunning) {
|
||||
tunnelLogger.warn(
|
||||
`Some tunnel processes may still be running for '${tunnelName}'`,
|
||||
);
|
||||
} else {
|
||||
tunnelLogger.success("Remote tunnel process killed", {
|
||||
operation: "tunnel_remote_killed",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
});
|
||||
}
|
||||
conn.end();
|
||||
callback();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const killCmd = killCmds[commandIndex];
|
||||
|
||||
conn.exec(killCmd, (err, stream) => {
|
||||
if (err) {
|
||||
tunnelLogger.warn(
|
||||
`Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
stream.on("close", () => {
|
||||
commandIndex++;
|
||||
executeNextKillCommand();
|
||||
});
|
||||
|
||||
stream.on("data", () => {});
|
||||
|
||||
stream.stderr.on("data", (data) => {
|
||||
const output = data.toString().trim();
|
||||
if (output && !output.includes("debug1")) {
|
||||
tunnelLogger.warn(
|
||||
`Kill command ${commandIndex + 1} stderr for '${tunnelName}': ${output}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
executeNextKillCommand();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
conn.on("error", (err) => {
|
||||
tunnelLogger.error(
|
||||
`Failed to connect to source host for killing tunnel '${tunnelName}': ${err.message}`,
|
||||
);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
if (
|
||||
tunnelConfig.useSocks5 &&
|
||||
(tunnelConfig.socks5Host ||
|
||||
(tunnelConfig.socks5ProxyChain &&
|
||||
tunnelConfig.socks5ProxyChain.length > 0))
|
||||
) {
|
||||
(async () => {
|
||||
if (
|
||||
tunnelConfig.useSocks5 &&
|
||||
(tunnelConfig.socks5Host ||
|
||||
(tunnelConfig.socks5ProxyChain &&
|
||||
tunnelConfig.socks5ProxyChain.length > 0))
|
||||
) {
|
||||
try {
|
||||
const socks5Socket = await createSocks5Connection(
|
||||
tunnelConfig.sourceIP,
|
||||
@@ -1585,9 +1465,8 @@ async function killRemoteTunnelByMarker(
|
||||
|
||||
if (socks5Socket) {
|
||||
connOptions.sock = socks5Socket;
|
||||
conn.connect(connOptions);
|
||||
} else {
|
||||
callback(new Error("Failed to create SOCKS5 connection"));
|
||||
throw new Error("Failed to create SOCKS5 connection");
|
||||
}
|
||||
} catch (socks5Error) {
|
||||
tunnelLogger.error(
|
||||
@@ -1600,18 +1479,109 @@ async function killRemoteTunnelByMarker(
|
||||
proxyPort: tunnelConfig.socks5Port || 1080,
|
||||
},
|
||||
);
|
||||
callback(
|
||||
new Error(
|
||||
"SOCKS5 proxy connection failed: " +
|
||||
(socks5Error instanceof Error
|
||||
? socks5Error.message
|
||||
: "Unknown error"),
|
||||
),
|
||||
throw new Error(
|
||||
"SOCKS5 proxy connection failed: " +
|
||||
(socks5Error instanceof Error
|
||||
? socks5Error.message
|
||||
: "Unknown error"),
|
||||
);
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
conn.connect(connOptions);
|
||||
}
|
||||
|
||||
return new Promise<Client>((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
conn.on("ready", () => resolve(conn));
|
||||
conn.on("error", (err) => reject(err));
|
||||
conn.connect(connOptions);
|
||||
});
|
||||
};
|
||||
|
||||
const execCommand = (client: Client, cmd: string): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
client.exec(cmd, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let output = "";
|
||||
stream.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
});
|
||||
stream.stderr.on("data", (data: Buffer) => {
|
||||
const stderr = data.toString().trim();
|
||||
if (stderr && !stderr.includes("debug1")) {
|
||||
tunnelLogger.warn(
|
||||
`Kill command stderr for '${tunnelName}': ${stderr}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
stream.on("close", () => resolve(output.trim()));
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await withConnection(poolKey, factory, async (client) => {
|
||||
const tunnelType = tunnelConfig.tunnelType || "remote";
|
||||
const tunnelFlag = tunnelType === "local" ? "-L" : "-R";
|
||||
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*${tunnelFlag})' | grep -v grep`;
|
||||
|
||||
const checkOutput = await execCommand(client, checkCmd);
|
||||
if (!checkOutput) {
|
||||
tunnelLogger.warn("Remote tunnel process not found", {
|
||||
operation: "tunnel_remote_not_found",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
marker: tunnelMarker,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
tunnelLogger.info("Remote tunnel process found, proceeding to kill", {
|
||||
operation: "tunnel_remote_found",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
marker: tunnelMarker,
|
||||
});
|
||||
|
||||
const killCmds = [
|
||||
`pkill -TERM -f '${tunnelMarker}'`,
|
||||
`sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
|
||||
`sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`,
|
||||
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
|
||||
];
|
||||
|
||||
for (const killCmd of killCmds) {
|
||||
try {
|
||||
await execCommand(client, killCmd);
|
||||
} catch (err) {
|
||||
tunnelLogger.warn(
|
||||
`Kill command failed for '${tunnelName}': ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const verifyOutput = await execCommand(client, checkCmd);
|
||||
if (verifyOutput) {
|
||||
tunnelLogger.warn(
|
||||
`Some tunnel processes may still be running for '${tunnelName}'`,
|
||||
);
|
||||
} else {
|
||||
tunnelLogger.success("Remote tunnel process killed", {
|
||||
operation: "tunnel_remote_killed",
|
||||
userId: tunnelConfig.sourceUserId,
|
||||
hostId: tunnelConfig.sourceHostId,
|
||||
tunnelName,
|
||||
});
|
||||
}
|
||||
});
|
||||
callback();
|
||||
} catch (err) {
|
||||
tunnelLogger.error(
|
||||
`Failed to connect to source host for killing tunnel '${tunnelName}': ${(err as Error).message}`,
|
||||
);
|
||||
callback(err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1750,7 +1720,7 @@ app.post(
|
||||
if (pendingTunnelOperations.has(tunnelName)) {
|
||||
try {
|
||||
await pendingTunnelOperations.get(tunnelName);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
tunnelLogger.warn(`Previous tunnel operation failed`, { tunnelName });
|
||||
}
|
||||
}
|
||||
@@ -1779,7 +1749,7 @@ app.post(
|
||||
const internalAuthToken = await systemCrypto.getInternalAuthToken();
|
||||
|
||||
const allHostsResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal/all",
|
||||
"http://localhost:30001/host/db/host/internal/all",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2057,7 +2027,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
const internalAuthToken = await systemCrypto.getInternalAuthToken();
|
||||
|
||||
const autostartResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal",
|
||||
"http://localhost:30001/host/db/host/internal",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2067,7 +2037,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
);
|
||||
|
||||
const allHostsResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal/all",
|
||||
"http://localhost:30001/host/db/host/internal/all",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2079,7 +2049,6 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
const autostartHosts: SSHHost[] = autostartResponse.data || [];
|
||||
const allHosts: SSHHost[] = allHostsResponse.data || [];
|
||||
const autoStartTunnels: TunnelConfig[] = [];
|
||||
|
||||
tunnelLogger.info(
|
||||
`Found ${autostartHosts.length} autostart hosts and ${allHosts.length} total hosts for endpointHost resolution`,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Client } from "ssh2";
|
||||
import type { Client, ClientChannel } from "ssh2";
|
||||
|
||||
export function execCommand(
|
||||
client: Client,
|
||||
@@ -11,7 +11,7 @@ export function execCommand(
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let stream: any = null;
|
||||
let stream: ClientChannel | null = null;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) {
|
||||
@@ -30,8 +30,8 @@ export function execCommand(
|
||||
stream.stderr.removeAllListeners();
|
||||
}
|
||||
stream.destroy();
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
} catch {
|
||||
// expected - cleanup errors ignored
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function collectCpuMetrics(client: Client): Promise<{
|
||||
|
||||
const coresNum = Number((coresOut.stdout || "").trim());
|
||||
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
cpuPercent = null;
|
||||
cores = null;
|
||||
loadTriplet = null;
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function collectDiskMetrics(client: Client): Promise<{
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
diskPercent = null;
|
||||
usedHuman = null;
|
||||
totalHuman = null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { statsLogger } from "../../utils/logger.js";
|
||||
|
||||
export interface LoginRecord {
|
||||
user: string;
|
||||
@@ -53,7 +52,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
|
||||
parsedTime = isNaN(date.getTime())
|
||||
? new Date().toISOString()
|
||||
: date.toISOString();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
parsedTime = new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -70,7 +69,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
const failedOut = await execCommand(
|
||||
@@ -111,7 +112,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
|
||||
parsedTime = isNaN(date.getTime())
|
||||
? new Date().toISOString()
|
||||
: date.toISOString();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
parsedTime = new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -126,7 +127,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
return {
|
||||
recentLogins: recentLogins.slice(0, 10),
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function collectMemoryMetrics(client: Client): Promise<{
|
||||
usedGiB = kibToGiB(usedKb);
|
||||
totalGiB = kibToGiB(totalKb);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
memPercent = null;
|
||||
usedGiB = null;
|
||||
totalGiB = null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { statsLogger } from "../../utils/logger.js";
|
||||
|
||||
export async function collectNetworkMetrics(client: Client): Promise<{
|
||||
interfaces: Array<{
|
||||
@@ -68,7 +67,9 @@ export async function collectNetworkMetrics(client: Client): Promise<{
|
||||
txBytes: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
return { interfaces };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { statsLogger } from "../../utils/logger.js";
|
||||
|
||||
export async function collectProcessesMetrics(client: Client): Promise<{
|
||||
total: number | null;
|
||||
@@ -54,7 +53,9 @@ export async function collectProcessesMetrics(client: Client): Promise<{
|
||||
|
||||
const runningCount2 = Number(runningCount.stdout.trim());
|
||||
runningProcesses = Number.isFinite(runningCount2) ? runningCount2 : null;
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
return {
|
||||
total: totalProcesses,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { statsLogger } from "../../utils/logger.js";
|
||||
|
||||
export async function collectSystemMetrics(client: Client): Promise<{
|
||||
hostname: string | null;
|
||||
@@ -22,8 +21,8 @@ export async function collectSystemMetrics(client: Client): Promise<{
|
||||
hostname = hostnameOut.stdout.trim() || null;
|
||||
kernel = kernelOut.stdout.trim() || null;
|
||||
os = osOut.stdout.trim() || null;
|
||||
} catch (e) {
|
||||
// No error log
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { statsLogger } from "../../utils/logger.js";
|
||||
|
||||
export async function collectUptimeMetrics(client: Client): Promise<{
|
||||
seconds: number | null;
|
||||
@@ -21,7 +20,9 @@ export async function collectUptimeMetrics(client: Client): Promise<{
|
||||
uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
return {
|
||||
seconds: uptimeSeconds,
|
||||
|
||||
+40
-1
@@ -22,7 +22,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
if (persistentConfig.parsed) {
|
||||
Object.assign(process.env, persistentConfig.parsed);
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - env file may not exist
|
||||
}
|
||||
|
||||
systemLogger.info("Termix backend initialization started", {
|
||||
operation: "backend_init_start",
|
||||
@@ -140,6 +142,33 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
await import("./ssh/docker-console.js");
|
||||
await import("./dashboard.js");
|
||||
|
||||
// Initialize Guacamole server for RDP/VNC/Telnet support
|
||||
const { getDb: getDbForGuac } = await import("./database/db/index.js");
|
||||
const guacDb = getDbForGuac();
|
||||
const guacEnabledRow = guacDb.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
|
||||
.get() as { value: string } | undefined;
|
||||
const guacEnabled = guacEnabledRow
|
||||
? guacEnabledRow.value !== "false"
|
||||
: true;
|
||||
|
||||
if (process.env.ENABLE_GUACAMOLE !== "false" && guacEnabled) {
|
||||
try {
|
||||
await import("./guacamole/guacamole-server.js");
|
||||
systemLogger.info("Guacamole server initialized", {
|
||||
operation: "guac_init",
|
||||
});
|
||||
} catch (error) {
|
||||
systemLogger.warn(
|
||||
"Failed to initialize Guacamole server (guacd may not be available)",
|
||||
{
|
||||
operation: "guac_init_skip",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
systemLogger.success("Termix backend started successfully", {
|
||||
operation: "backend_init_complete",
|
||||
port: process.env.PORT || 4090,
|
||||
@@ -163,6 +192,16 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("message", (msg: { type?: string }) => {
|
||||
if (msg?.type === "shutdown") {
|
||||
systemLogger.info(
|
||||
"Received IPC shutdown, initiating graceful shutdown...",
|
||||
{ operation: "shutdown" },
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
systemLogger.error("Uncaught exception occurred", error, {
|
||||
operation: "error_handling",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DataCrypto } from "./data-crypto.js";
|
||||
import { databaseLogger, authLogger } from "./logger.js";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { db } from "../database/db/index.js";
|
||||
import { sessions } from "../database/db/schema.js";
|
||||
import { sessions, trustedDevices } from "../database/db/schema.js";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DeviceType } from "./user-agent-parser.js";
|
||||
@@ -99,7 +99,7 @@ class AuthManager {
|
||||
const sessionDurationMs =
|
||||
deviceType === "desktop" || deviceType === "mobile"
|
||||
? 30 * 24 * 60 * 60 * 1000
|
||||
: 7 * 24 * 60 * 60 * 1000;
|
||||
: 24 * 60 * 60 * 1000;
|
||||
|
||||
const authenticated = await this.userCrypto.authenticateOIDCUser(
|
||||
userId,
|
||||
@@ -121,7 +121,7 @@ class AuthManager {
|
||||
const sessionDurationMs =
|
||||
deviceType === "desktop" || deviceType === "mobile"
|
||||
? 30 * 24 * 60 * 60 * 1000
|
||||
: 7 * 24 * 60 * 60 * 1000;
|
||||
: 24 * 60 * 60 * 1000;
|
||||
|
||||
const authenticated = await this.userCrypto.authenticateUser(
|
||||
userId,
|
||||
@@ -199,6 +199,7 @@ class AuthManager {
|
||||
options: {
|
||||
expiresIn?: string;
|
||||
pendingTOTP?: boolean;
|
||||
rememberMe?: boolean;
|
||||
deviceType?: DeviceType;
|
||||
deviceInfo?: string;
|
||||
} = {},
|
||||
@@ -207,13 +208,13 @@ class AuthManager {
|
||||
|
||||
let expiresIn = options.expiresIn;
|
||||
if (!expiresIn && !options.pendingTOTP) {
|
||||
if (options.deviceType === "desktop" || options.deviceType === "mobile") {
|
||||
if (options.rememberMe) {
|
||||
expiresIn = "30d";
|
||||
} else {
|
||||
expiresIn = "7d";
|
||||
expiresIn = "24h";
|
||||
}
|
||||
} else if (!expiresIn) {
|
||||
expiresIn = "7d";
|
||||
expiresIn = "24h";
|
||||
}
|
||||
|
||||
const payload: JWTPayload = { userId };
|
||||
@@ -276,7 +277,7 @@ class AuthManager {
|
||||
|
||||
private parseExpiresIn(expiresIn: string): number {
|
||||
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
||||
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
||||
if (!match) return 24 * 60 * 60 * 1000;
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
@@ -291,7 +292,7 @@ class AuthManager {
|
||||
case "d":
|
||||
return value * 24 * 60 * 60 * 1000;
|
||||
default:
|
||||
return 7 * 24 * 60 * 60 * 1000;
|
||||
return 24 * 60 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +341,15 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
invalidateJWTToken(token: string): void {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
invalidateJWTToken(_token: string): void {
|
||||
// expected - no-op, JWT tokens are stateless
|
||||
}
|
||||
|
||||
invalidateUserTokens(userId: string): void {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
invalidateUserTokens(_userId: string): void {
|
||||
// expected - no-op, handled by session management
|
||||
}
|
||||
|
||||
async revokeSession(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -488,7 +495,7 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllSessions(): Promise<any[]> {
|
||||
async getAllSessions(): Promise<Record<string, unknown>[]> {
|
||||
try {
|
||||
const allSessions = await db.select().from(sessions);
|
||||
return allSessions;
|
||||
@@ -500,7 +507,7 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getUserSessions(userId: string): Promise<any[]> {
|
||||
async getUserSessions(userId: string): Promise<Record<string, unknown>[]> {
|
||||
try {
|
||||
const userSessions = await db
|
||||
.select()
|
||||
@@ -518,7 +525,7 @@ class AuthManager {
|
||||
|
||||
getSecureCookieOptions(
|
||||
req: RequestWithHeaders,
|
||||
maxAge: number = 7 * 24 * 60 * 60 * 1000,
|
||||
maxAge: number = 24 * 60 * 60 * 1000,
|
||||
) {
|
||||
return {
|
||||
httpOnly: false,
|
||||
@@ -711,7 +718,7 @@ class AuthManager {
|
||||
.from(users)
|
||||
.where(eq(users.id, payload.userId));
|
||||
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
if (!user || user.length === 0 || !user[0].isAdmin) {
|
||||
databaseLogger.warn(
|
||||
"Non-admin user attempted to access admin endpoint",
|
||||
{
|
||||
@@ -768,6 +775,7 @@ class AuthManager {
|
||||
if (remainingSessions.length === 0) {
|
||||
this.userCrypto.logoutUser(userId);
|
||||
} else {
|
||||
// expected - other sessions still active, keep user crypto state
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete session on logout", error, {
|
||||
@@ -810,6 +818,112 @@ class AuthManager {
|
||||
newPassword,
|
||||
);
|
||||
}
|
||||
|
||||
async isTrustedDevice(
|
||||
userId: string,
|
||||
deviceFingerprint: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const device = await db
|
||||
.select()
|
||||
.from(trustedDevices)
|
||||
.where(
|
||||
and(
|
||||
eq(trustedDevices.userId, userId),
|
||||
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!device || device.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(device[0].expiresAt);
|
||||
|
||||
if (now > expiresAt) {
|
||||
await this.removeTrustedDevice(userId, deviceFingerprint);
|
||||
return false;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(trustedDevices)
|
||||
.set({ lastUsedAt: now.toISOString() })
|
||||
.where(
|
||||
and(
|
||||
eq(trustedDevices.userId, userId),
|
||||
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
authLogger.error("Failed to check trusted device", { userId, error });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async addTrustedDevice(
|
||||
userId: string,
|
||||
deviceFingerprint: string,
|
||||
deviceType: string,
|
||||
deviceInfo: string,
|
||||
): Promise<void> {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const existingDevice = await db
|
||||
.select()
|
||||
.from(trustedDevices)
|
||||
.where(
|
||||
and(
|
||||
eq(trustedDevices.userId, userId),
|
||||
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingDevice && existingDevice.length > 0) {
|
||||
await db
|
||||
.update(trustedDevices)
|
||||
.set({
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
lastUsedAt: now.toISOString(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(trustedDevices.userId, userId),
|
||||
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await db.insert(trustedDevices).values({
|
||||
id: nanoid(),
|
||||
userId,
|
||||
deviceFingerprint,
|
||||
deviceType,
|
||||
deviceInfo,
|
||||
createdAt: now.toISOString(),
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
lastUsedAt: now.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async removeTrustedDevice(
|
||||
userId: string,
|
||||
deviceFingerprint: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.delete(trustedDevices)
|
||||
.where(
|
||||
and(
|
||||
eq(trustedDevices.userId, userId),
|
||||
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AuthManager, type AuthenticationResult, type JWTPayload };
|
||||
|
||||
@@ -233,7 +233,9 @@ IP.3 = 0.0.0.0
|
||||
let envContent = "";
|
||||
try {
|
||||
envContent = await fs.readFile(this.ENV_FILE, "utf8");
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - env file may not exist yet
|
||||
}
|
||||
|
||||
let updatedContent = envContent;
|
||||
let hasChanges = false;
|
||||
|
||||
@@ -59,9 +59,9 @@ export class CredentialSystemEncryptionMigration {
|
||||
)
|
||||
: null;
|
||||
|
||||
const plainKeyPassword = cred.key_password
|
||||
const plainKeyPassword = cred.keyPassword
|
||||
? FieldCrypto.decryptField(
|
||||
cred.key_password,
|
||||
cred.keyPassword,
|
||||
userDEK,
|
||||
cred.id.toString(),
|
||||
"key_password",
|
||||
|
||||
@@ -131,12 +131,12 @@ class DataCrypto {
|
||||
db.prepare(updateQuery).run(
|
||||
updatedRecord.password || null,
|
||||
updatedRecord.key || null,
|
||||
updatedRecord.key_password || updatedRecord.keyPassword || null,
|
||||
updatedRecord.keyType || null,
|
||||
updatedRecord.autostartPassword || null,
|
||||
updatedRecord.autostartKey || null,
|
||||
updatedRecord.autostartKeyPassword || null,
|
||||
updatedRecord.sudoPassword || null,
|
||||
updatedRecord.key_password || null,
|
||||
updatedRecord.key_type || null,
|
||||
updatedRecord.autostart_password || null,
|
||||
updatedRecord.autostart_key || null,
|
||||
updatedRecord.autostart_key_password || null,
|
||||
updatedRecord.sudo_password || null,
|
||||
record.id,
|
||||
);
|
||||
|
||||
@@ -171,10 +171,10 @@ class DataCrypto {
|
||||
db.prepare(updateQuery).run(
|
||||
updatedRecord.password || null,
|
||||
updatedRecord.key || null,
|
||||
updatedRecord.key_password || updatedRecord.keyPassword || null,
|
||||
updatedRecord.private_key || updatedRecord.privateKey || null,
|
||||
updatedRecord.public_key || updatedRecord.publicKey || null,
|
||||
updatedRecord.keyType || null,
|
||||
updatedRecord.key_password || null,
|
||||
updatedRecord.private_key || null,
|
||||
updatedRecord.public_key || null,
|
||||
updatedRecord.key_type || null,
|
||||
record.id,
|
||||
);
|
||||
|
||||
@@ -207,14 +207,10 @@ class DataCrypto {
|
||||
WHERE id = ?
|
||||
`;
|
||||
db.prepare(updateQuery).run(
|
||||
updatedRecord.totp_secret || updatedRecord.totpSecret || null,
|
||||
updatedRecord.totp_backup_codes ||
|
||||
updatedRecord.totpBackupCodes ||
|
||||
null,
|
||||
updatedRecord.client_secret || updatedRecord.clientSecret || null,
|
||||
updatedRecord.oidc_identifier ||
|
||||
updatedRecord.oidcIdentifier ||
|
||||
null,
|
||||
updatedRecord.totp_secret || null,
|
||||
updatedRecord.totp_backup_codes || null,
|
||||
updatedRecord.client_secret || null,
|
||||
updatedRecord.oidc_identifier || null,
|
||||
userId,
|
||||
);
|
||||
|
||||
@@ -267,46 +263,41 @@ class DataCrypto {
|
||||
"password",
|
||||
"key",
|
||||
"key_password",
|
||||
"keyPassword",
|
||||
"keyType",
|
||||
"autostartPassword",
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"sudo_password",
|
||||
"autostart_password",
|
||||
"autostart_key",
|
||||
"autostart_key_password",
|
||||
],
|
||||
},
|
||||
{
|
||||
table: "ssh_credentials",
|
||||
fields: [
|
||||
"password",
|
||||
"private_key",
|
||||
"privateKey",
|
||||
"key_password",
|
||||
"keyPassword",
|
||||
"key",
|
||||
"private_key",
|
||||
"public_key",
|
||||
"publicKey",
|
||||
"keyType",
|
||||
"key_password",
|
||||
],
|
||||
},
|
||||
{
|
||||
table: "users",
|
||||
fields: [
|
||||
"client_secret",
|
||||
"clientSecret",
|
||||
"totp_secret",
|
||||
"totpSecret",
|
||||
"totp_backup_codes",
|
||||
"totpBackupCodes",
|
||||
"oidc_identifier",
|
||||
"oidcIdentifier",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { table, fields } of tablesToReencrypt) {
|
||||
try {
|
||||
const selectQuery =
|
||||
table === "users"
|
||||
? `SELECT * FROM ${table} WHERE id = ?`
|
||||
: `SELECT * FROM ${table} WHERE user_id = ?`;
|
||||
const records = db
|
||||
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
|
||||
.prepare(selectQuery)
|
||||
.all(userId) as DatabaseRecord[];
|
||||
|
||||
for (const record of records) {
|
||||
@@ -359,7 +350,13 @@ class DataCrypto {
|
||||
(field) => updatedRecord[field] !== record[field],
|
||||
);
|
||||
if (updateFields.length > 0) {
|
||||
const updateQuery = `UPDATE ${table} SET ${updateFields.map((f) => `${f} = ?`).join(", ")}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const setClause = updateFields
|
||||
.map((f) => `${f} = ?`)
|
||||
.join(", ");
|
||||
const updateQuery =
|
||||
table === "users"
|
||||
? `UPDATE ${table} SET ${setClause} WHERE id = ?`
|
||||
: `UPDATE ${table} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const updateValues = updateFields.map(
|
||||
(field) => updatedRecord[field],
|
||||
);
|
||||
@@ -507,9 +504,9 @@ class DataCrypto {
|
||||
);
|
||||
}
|
||||
|
||||
if (record.key_password && typeof record.key_password === "string") {
|
||||
if (record.keyPassword && typeof record.keyPassword === "string") {
|
||||
systemEncrypted.systemKeyPassword = FieldCrypto.encryptField(
|
||||
record.key_password as string,
|
||||
record.keyPassword as string,
|
||||
systemKey,
|
||||
recordId as string,
|
||||
"key_password",
|
||||
|
||||
@@ -327,7 +327,9 @@ class DatabaseFileEncryption {
|
||||
fs.accessSync(envPath, fs.constants.R_OK);
|
||||
envFileReadable = true;
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - env file access check may fail
|
||||
}
|
||||
|
||||
databaseLogger.error(
|
||||
"Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write",
|
||||
@@ -581,7 +583,9 @@ class DatabaseFileEncryption {
|
||||
try {
|
||||
fs.accessSync(envPath, fs.constants.R_OK);
|
||||
result.environment.envFileReadable = true;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - env file access check may fail
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -5,12 +5,21 @@ export class DatabaseSaveTrigger {
|
||||
private static isInitialized = false;
|
||||
private static pendingSave = false;
|
||||
private static saveTimeout: NodeJS.Timeout | null = null;
|
||||
private static _dirty = false;
|
||||
|
||||
static initialize(saveFunction: () => Promise<void>): void {
|
||||
this.saveFunction = saveFunction;
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
static get isDirty(): boolean {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
static markClean(): void {
|
||||
this._dirty = false;
|
||||
}
|
||||
|
||||
static async triggerSave(
|
||||
reason: string = "data_modification",
|
||||
): Promise<void> {
|
||||
@@ -22,6 +31,8 @@ export class DatabaseSaveTrigger {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
if (this.saveTimeout) {
|
||||
clearTimeout(this.saveTimeout);
|
||||
}
|
||||
@@ -35,6 +46,7 @@ export class DatabaseSaveTrigger {
|
||||
|
||||
try {
|
||||
await this.saveFunction!();
|
||||
this._dirty = false;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Database save failed", error, {
|
||||
operation: "db_save_trigger_failed",
|
||||
|
||||
@@ -16,43 +16,29 @@ class FieldCrypto {
|
||||
|
||||
private static readonly ENCRYPTED_FIELDS = {
|
||||
users: new Set([
|
||||
"password_hash",
|
||||
"passwordHash",
|
||||
"client_secret",
|
||||
"clientSecret",
|
||||
"totp_secret",
|
||||
"totpSecret",
|
||||
"totp_backup_codes",
|
||||
"totpBackupCodes",
|
||||
"oidc_identifier",
|
||||
"oidcIdentifier",
|
||||
]),
|
||||
ssh_data: new Set([
|
||||
"password",
|
||||
"key",
|
||||
"key_password",
|
||||
"keyPassword",
|
||||
"sudoPassword",
|
||||
"autostartPassword",
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"sudoPassword",
|
||||
]),
|
||||
ssh_credentials: new Set([
|
||||
"password",
|
||||
"private_key",
|
||||
"privateKey",
|
||||
"key_password",
|
||||
"keyPassword",
|
||||
"key",
|
||||
"public_key",
|
||||
"publicKey",
|
||||
]),
|
||||
opkssh_tokens: new Set([
|
||||
"ssh_cert",
|
||||
"sshCert",
|
||||
"private_key",
|
||||
"privateKey",
|
||||
]),
|
||||
opkssh_tokens: new Set(["sshCert", "privateKey"]),
|
||||
};
|
||||
|
||||
static encryptField(
|
||||
|
||||
@@ -82,19 +82,21 @@ export class LazyFieldEncryption {
|
||||
legacyFieldName,
|
||||
);
|
||||
return decrypted;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - legacy decryption may fail, try other methods
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
"totp_secret",
|
||||
"totp_backup_codes",
|
||||
"totpSecret",
|
||||
"totpBackupCodes",
|
||||
"password",
|
||||
"key",
|
||||
"key_password",
|
||||
"private_key",
|
||||
"public_key",
|
||||
"client_secret",
|
||||
"oidc_identifier",
|
||||
"keyPassword",
|
||||
"privateKey",
|
||||
"publicKey",
|
||||
"clientSecret",
|
||||
"oidcIdentifier",
|
||||
];
|
||||
|
||||
if (sensitiveFields.includes(fieldName)) {
|
||||
@@ -174,7 +176,9 @@ export class LazyFieldEncryption {
|
||||
wasPlaintext: false,
|
||||
wasLegacyEncryption: true,
|
||||
};
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - re-encryption may fail, return original
|
||||
}
|
||||
}
|
||||
return {
|
||||
encrypted: fieldValue,
|
||||
@@ -200,7 +204,8 @@ export class LazyFieldEncryption {
|
||||
let needsUpdate = false;
|
||||
|
||||
for (const fieldName of sensitiveFields) {
|
||||
const fieldValue = record[fieldName];
|
||||
const column = this.propertyToColumn(fieldName);
|
||||
const fieldValue = record[column] ?? record[fieldName];
|
||||
|
||||
if (fieldValue) {
|
||||
try {
|
||||
@@ -213,7 +218,7 @@ export class LazyFieldEncryption {
|
||||
);
|
||||
|
||||
if (wasPlaintext || wasLegacyEncryption) {
|
||||
updatedRecord[fieldName] = encrypted;
|
||||
updatedRecord[column] = encrypted;
|
||||
migratedFields.push(fieldName);
|
||||
needsUpdate = true;
|
||||
}
|
||||
@@ -230,22 +235,48 @@ export class LazyFieldEncryption {
|
||||
return { updatedRecord, migratedFields, needsUpdate };
|
||||
}
|
||||
|
||||
private static readonly PROPERTY_TO_COLUMN: Record<string, string> = {
|
||||
keyPassword: "key_password",
|
||||
privateKey: "private_key",
|
||||
publicKey: "public_key",
|
||||
sudoPassword: "sudo_password",
|
||||
autostartPassword: "autostart_password",
|
||||
autostartKey: "autostart_key",
|
||||
autostartKeyPassword: "autostart_key_password",
|
||||
totpSecret: "totp_secret",
|
||||
totpBackupCodes: "totp_backup_codes",
|
||||
clientSecret: "client_secret",
|
||||
oidcIdentifier: "oidc_identifier",
|
||||
};
|
||||
|
||||
static getSensitiveFieldsForTable(tableName: string): string[] {
|
||||
const sensitiveFieldsMap: Record<string, string[]> = {
|
||||
ssh_data: ["password", "key", "key_password"],
|
||||
ssh_data: [
|
||||
"password",
|
||||
"key",
|
||||
"keyPassword",
|
||||
"sudoPassword",
|
||||
"autostartPassword",
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
],
|
||||
ssh_credentials: [
|
||||
"password",
|
||||
"key",
|
||||
"key_password",
|
||||
"private_key",
|
||||
"public_key",
|
||||
"keyPassword",
|
||||
"privateKey",
|
||||
"publicKey",
|
||||
],
|
||||
users: ["totp_secret", "totp_backup_codes"],
|
||||
users: ["totpSecret", "totpBackupCodes"],
|
||||
};
|
||||
|
||||
return sensitiveFieldsMap[tableName] || [];
|
||||
}
|
||||
|
||||
static propertyToColumn(propertyName: string): string {
|
||||
return this.PROPERTY_TO_COLUMN[propertyName] || propertyName;
|
||||
}
|
||||
|
||||
static fieldNeedsMigration(
|
||||
fieldValue: string,
|
||||
userKEK: Buffer,
|
||||
@@ -310,10 +341,11 @@ export class LazyFieldEncryption {
|
||||
const hostPlaintextFields: string[] = [];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
const column = this.propertyToColumn(field);
|
||||
if (
|
||||
host[field] &&
|
||||
host[column] &&
|
||||
this.fieldNeedsMigration(
|
||||
host[field] as string,
|
||||
host[column] as string,
|
||||
userKEK,
|
||||
host.id.toString(),
|
||||
field,
|
||||
@@ -344,10 +376,11 @@ export class LazyFieldEncryption {
|
||||
const credentialPlaintextFields: string[] = [];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
const column = this.propertyToColumn(field);
|
||||
if (
|
||||
credential[field] &&
|
||||
credential[column] &&
|
||||
this.fieldNeedsMigration(
|
||||
credential[field] as string,
|
||||
credential[column] as string,
|
||||
userKEK,
|
||||
credential.id.toString(),
|
||||
field,
|
||||
@@ -373,9 +406,10 @@ export class LazyFieldEncryption {
|
||||
const userPlaintextFields: string[] = [];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
const column = this.propertyToColumn(field);
|
||||
if (
|
||||
user[field] &&
|
||||
this.fieldNeedsMigration(user[field], userKEK, userId, field)
|
||||
user[column] &&
|
||||
this.fieldNeedsMigration(user[column], userKEK, userId, field)
|
||||
) {
|
||||
userPlaintextFields.push(field);
|
||||
needsMigration = true;
|
||||
|
||||
@@ -254,5 +254,6 @@ export const authLogger = new Logger("AUTH", "🔐", "#ef4444");
|
||||
export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6");
|
||||
export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6");
|
||||
export const dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899");
|
||||
export const guacLogger = new Logger("GUACAMOLE", "🖼️", "#ff6b6b");
|
||||
|
||||
export const logger = systemLogger;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
hostAccess,
|
||||
roles,
|
||||
userRoles,
|
||||
sshData,
|
||||
hosts,
|
||||
users,
|
||||
} from "../database/db/schema.js";
|
||||
import { eq, and, or, isNull, gte, sql } from "drizzle-orm";
|
||||
@@ -66,15 +66,14 @@ class PermissionManager {
|
||||
private async cleanupExpiredAccess(): Promise<void> {
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const result = await db
|
||||
await db
|
||||
.delete(hostAccess)
|
||||
.where(
|
||||
and(
|
||||
sql`${hostAccess.expiresAt} IS NOT NULL`,
|
||||
sql`${hostAccess.expiresAt} <= ${now}`,
|
||||
),
|
||||
)
|
||||
.returning({ id: hostAccess.id });
|
||||
);
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to cleanup expired host access", error, {
|
||||
operation: "host_access_cleanup_failed",
|
||||
@@ -168,8 +167,8 @@ class PermissionManager {
|
||||
try {
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length > 0) {
|
||||
@@ -211,9 +210,9 @@ class PermissionManager {
|
||||
const access = sharedAccess[0];
|
||||
|
||||
const hostOwnerCheck = await db
|
||||
.select({ ownerId: sshData.userId })
|
||||
.from(sshData)
|
||||
.where(eq(sshData.id, hostId))
|
||||
.select({ ownerId: hosts.userId })
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, hostId))
|
||||
.limit(1);
|
||||
|
||||
if (hostOwnerCheck.length > 0 && hostOwnerCheck[0].ownerId === userId) {
|
||||
@@ -280,7 +279,7 @@ class PermissionManager {
|
||||
async isAdmin(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const user = await db
|
||||
.select({ isAdmin: users.is_admin })
|
||||
.select({ isAdmin: users.isAdmin })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
@@ -383,7 +382,8 @@ class PermissionManager {
|
||||
});
|
||||
}
|
||||
|
||||
(req as any).hostAccessInfo = accessInfo;
|
||||
(req as unknown as { hostAccessInfo: HostAccessInfo }).hostAccessInfo =
|
||||
accessInfo;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -18,9 +18,7 @@ export function getProxyAgent(targetUrl?: string): Agent | undefined {
|
||||
const trimmed = entry.trim().toLowerCase();
|
||||
if (!trimmed) continue;
|
||||
|
||||
const normalized = trimmed
|
||||
.replace(/^\*\./, "")
|
||||
.replace(/^\./, "");
|
||||
const normalized = trimmed.replace(/^\*\./, "").replace(/^\./, "");
|
||||
|
||||
if (hostname === normalized || hostname.endsWith(`.${normalized}`)) {
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import { SocksClient } from "socks";
|
||||
import type { SocksClientOptions } from "socks";
|
||||
import net from "net";
|
||||
import { sshLogger } from "./logger.js";
|
||||
import type { ProxyNode } from "../../types/index.js";
|
||||
|
||||
export interface SOCKS5Config {
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
socks5Port?: number;
|
||||
socks5Username?: string;
|
||||
socks5Password?: string;
|
||||
socks5ProxyChain?: ProxyNode[];
|
||||
}
|
||||
|
||||
export async function createProxyConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
socks5Config: SOCKS5Config,
|
||||
): Promise<net.Socket | null> {
|
||||
if (!socks5Config.useSocks5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
socks5Config.socks5ProxyChain &&
|
||||
socks5Config.socks5ProxyChain.length > 0
|
||||
) {
|
||||
return createMixedProxyChainConnection(
|
||||
targetHost,
|
||||
targetPort,
|
||||
socks5Config.socks5ProxyChain,
|
||||
);
|
||||
}
|
||||
|
||||
if (socks5Config.socks5Host) {
|
||||
return createSingleProxyConnection(targetHost, targetPort, socks5Config);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const createSocks5Connection = createProxyConnection;
|
||||
|
||||
async function createSingleProxyConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
socks5Config: SOCKS5Config,
|
||||
): Promise<net.Socket> {
|
||||
const socksOptions: SocksClientOptions = {
|
||||
proxy: {
|
||||
host: socks5Config.socks5Host!,
|
||||
port: socks5Config.socks5Port || 1080,
|
||||
type: 5,
|
||||
userId: socks5Config.socks5Username,
|
||||
password: socks5Config.socks5Password,
|
||||
},
|
||||
command: "connect",
|
||||
destination: {
|
||||
host: targetHost,
|
||||
port: targetPort,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await SocksClient.createConnection(socksOptions);
|
||||
|
||||
return info.socket;
|
||||
} catch (error) {
|
||||
sshLogger.error("SOCKS5 connection failed", error, {
|
||||
operation: "socks5_connect_failed",
|
||||
proxyHost: socks5Config.socks5Host,
|
||||
proxyPort: socks5Config.socks5Port || 1080,
|
||||
targetHost,
|
||||
targetPort,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createHttpConnectConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
proxyHost: string,
|
||||
proxyPort: number,
|
||||
username?: string,
|
||||
password?: string,
|
||||
existingSocket?: net.Socket,
|
||||
): Promise<net.Socket> {
|
||||
return new Promise<net.Socket>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`HTTP CONNECT proxy timeout connecting to ${proxyHost}:${proxyPort}`,
|
||||
),
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
function sendConnect(socket: net.Socket) {
|
||||
let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`;
|
||||
if (username && password) {
|
||||
const credentials = Buffer.from(`${username}:${password}`).toString(
|
||||
"base64",
|
||||
);
|
||||
connectReq += `Proxy-Authorization: Basic ${credentials}\r\n`;
|
||||
}
|
||||
connectReq += "\r\n";
|
||||
|
||||
let responseBuffer = "";
|
||||
|
||||
function onData(chunk: Buffer) {
|
||||
responseBuffer += chunk.toString("utf8");
|
||||
const headerEnd = responseBuffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd === -1) return;
|
||||
|
||||
clearTimeout(timeout);
|
||||
socket.removeListener("data", onData);
|
||||
socket.removeListener("error", onError);
|
||||
|
||||
const statusLine = responseBuffer.slice(
|
||||
0,
|
||||
responseBuffer.indexOf("\r\n"),
|
||||
);
|
||||
const statusCode = parseInt(statusLine.split(" ")[1], 10);
|
||||
|
||||
if (statusCode === 200) {
|
||||
resolve(socket);
|
||||
} else {
|
||||
socket.destroy();
|
||||
reject(
|
||||
new Error(
|
||||
`HTTP CONNECT proxy returned ${statusCode}: ${statusLine}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(err: Error) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`HTTP CONNECT proxy error: ${err.message}`));
|
||||
}
|
||||
|
||||
socket.on("data", onData);
|
||||
socket.on("error", onError);
|
||||
socket.write(connectReq);
|
||||
}
|
||||
|
||||
if (existingSocket) {
|
||||
sendConnect(existingSocket);
|
||||
} else {
|
||||
const socket = net.connect(proxyPort, proxyHost, () => {
|
||||
sendConnect(socket);
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`HTTP CONNECT proxy TCP error: ${err.message}`));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMixedProxyChainConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
proxyChain: ProxyNode[],
|
||||
): Promise<net.Socket> {
|
||||
if (proxyChain.length === 0) {
|
||||
throw new Error("Proxy chain is empty");
|
||||
}
|
||||
|
||||
const hasMixedTypes = proxyChain.some((p) => p.type === "http");
|
||||
|
||||
if (!hasMixedTypes) {
|
||||
return createPureSocksChainConnection(targetHost, targetPort, proxyChain);
|
||||
}
|
||||
|
||||
return createHopByHopConnection(targetHost, targetPort, proxyChain);
|
||||
}
|
||||
|
||||
async function createPureSocksChainConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
proxyChain: ProxyNode[],
|
||||
): Promise<net.Socket> {
|
||||
try {
|
||||
const info = await SocksClient.createConnectionChain({
|
||||
proxies: proxyChain.map((p) => ({
|
||||
host: p.host,
|
||||
port: p.port,
|
||||
type: p.type as 4 | 5,
|
||||
userId: p.username,
|
||||
password: p.password,
|
||||
timeout: 10000,
|
||||
})),
|
||||
command: "connect",
|
||||
destination: {
|
||||
host: targetHost,
|
||||
port: targetPort,
|
||||
},
|
||||
});
|
||||
return info.socket;
|
||||
} catch (error) {
|
||||
sshLogger.error("SOCKS proxy chain connection failed", error, {
|
||||
operation: "socks5_chain_connect_failed",
|
||||
chainLength: proxyChain.length,
|
||||
targetHost,
|
||||
targetPort,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createHopByHopConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
proxyChain: ProxyNode[],
|
||||
): Promise<net.Socket> {
|
||||
let currentSocket: net.Socket | null = null;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < proxyChain.length; i++) {
|
||||
const node = proxyChain[i];
|
||||
const isLast = i === proxyChain.length - 1;
|
||||
const nextTarget = isLast
|
||||
? { host: targetHost, port: targetPort }
|
||||
: { host: proxyChain[i + 1].host, port: proxyChain[i + 1].port };
|
||||
|
||||
if (node.type === "http") {
|
||||
currentSocket = await createHttpConnectConnection(
|
||||
nextTarget.host,
|
||||
nextTarget.port,
|
||||
node.host,
|
||||
node.port,
|
||||
node.username,
|
||||
node.password,
|
||||
currentSocket ?? undefined,
|
||||
);
|
||||
} else {
|
||||
const socksOptions: SocksClientOptions = {
|
||||
proxy: {
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
type: node.type as 4 | 5,
|
||||
userId: node.username,
|
||||
password: node.password,
|
||||
},
|
||||
command: "connect",
|
||||
destination: nextTarget,
|
||||
};
|
||||
if (currentSocket) {
|
||||
socksOptions.existing_socket = currentSocket;
|
||||
}
|
||||
const info = await SocksClient.createConnection(socksOptions);
|
||||
currentSocket = info.socket;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentSocket) {
|
||||
throw new Error("Proxy chain produced no socket");
|
||||
}
|
||||
return currentSocket;
|
||||
} catch (error) {
|
||||
if (currentSocket) {
|
||||
currentSocket.destroy();
|
||||
}
|
||||
sshLogger.error("Mixed proxy chain connection failed", error, {
|
||||
operation: "mixed_chain_connect_failed",
|
||||
chainLength: proxyChain.length,
|
||||
targetHost,
|
||||
targetPort,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testProxyConnectivity(options: {
|
||||
singleProxy?: {
|
||||
host: string;
|
||||
port: number;
|
||||
type?: 4 | 5 | "http";
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
proxyChain?: ProxyNode[];
|
||||
testTarget?: { host: string; port: number };
|
||||
}): Promise<{ success: boolean; latencyMs: number }> {
|
||||
const target = options.testTarget ?? { host: "google.com", port: 443 };
|
||||
const start = Date.now();
|
||||
|
||||
let socket: net.Socket | null = null;
|
||||
try {
|
||||
if (options.proxyChain && options.proxyChain.length > 0) {
|
||||
socket = await createMixedProxyChainConnection(
|
||||
target.host,
|
||||
target.port,
|
||||
options.proxyChain,
|
||||
);
|
||||
} else if (options.singleProxy) {
|
||||
const proxy = options.singleProxy;
|
||||
if (proxy.type === "http") {
|
||||
socket = await createHttpConnectConnection(
|
||||
target.host,
|
||||
target.port,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
proxy.username,
|
||||
proxy.password,
|
||||
);
|
||||
} else {
|
||||
const socksOptions: SocksClientOptions = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
type: (proxy.type as 4 | 5) || 5,
|
||||
userId: proxy.username,
|
||||
password: proxy.password,
|
||||
},
|
||||
command: "connect",
|
||||
destination: target,
|
||||
timeout: 10000,
|
||||
};
|
||||
const info = await SocksClient.createConnection(socksOptions);
|
||||
socket = info.socket;
|
||||
}
|
||||
} else {
|
||||
throw new Error("No proxy configuration provided");
|
||||
}
|
||||
|
||||
const latencyMs = Date.now() - start;
|
||||
socket.destroy();
|
||||
return { success: true, latencyMs };
|
||||
} catch (error) {
|
||||
if (socket) socket.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Request } from "express";
|
||||
import type { IncomingMessage } from "http";
|
||||
|
||||
export function getRequestOrigin(req: Request | IncomingMessage): string {
|
||||
let protocol: string;
|
||||
const protoHeader = req.headers["x-forwarded-proto"];
|
||||
|
||||
if (protoHeader) {
|
||||
protocol =
|
||||
typeof protoHeader === "string"
|
||||
? protoHeader.split(",")[0].trim()
|
||||
: protoHeader[0];
|
||||
} else if ("protocol" in req && req.protocol) {
|
||||
protocol = req.protocol;
|
||||
} else {
|
||||
protocol = (req.socket as unknown as { encrypted?: boolean }).encrypted
|
||||
? "https"
|
||||
: "http";
|
||||
}
|
||||
|
||||
const portHeader = req.headers["x-forwarded-port"];
|
||||
let port: string | undefined =
|
||||
typeof portHeader === "string"
|
||||
? portHeader.split(",")[0].trim()
|
||||
: undefined;
|
||||
|
||||
const hostHeaderRaw =
|
||||
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
|
||||
const hostHeader =
|
||||
typeof hostHeaderRaw === "string"
|
||||
? hostHeaderRaw.split(",")[0].trim()
|
||||
: String(hostHeaderRaw);
|
||||
|
||||
if (!port && hostHeader.includes(":")) {
|
||||
const parts = hostHeader.split(":");
|
||||
if (parts.length === 2 && !parts[0].includes("[")) {
|
||||
port = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
const hostWithoutPort = hostHeader.split(":")[0];
|
||||
if (port) {
|
||||
const isDefaultPort =
|
||||
(protocol === "http" && port === "80") ||
|
||||
(protocol === "https" && port === "443");
|
||||
|
||||
return isDefaultPort
|
||||
? `${protocol}://${hostWithoutPort}`
|
||||
: `${protocol}://${hostWithoutPort}:${port}`;
|
||||
}
|
||||
|
||||
return `${protocol}://${hostWithoutPort}`;
|
||||
}
|
||||
|
||||
export function getRequestOriginWithForceHTTPS(
|
||||
req: Request | IncomingMessage,
|
||||
): string {
|
||||
if (process.env.OIDC_FORCE_HTTPS === "true") {
|
||||
const origin = getRequestOrigin(req);
|
||||
return origin.replace(/^http:/, "https:");
|
||||
}
|
||||
return getRequestOrigin(req);
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import {
|
||||
sharedCredentials,
|
||||
sshCredentials,
|
||||
hostAccess,
|
||||
users,
|
||||
userRoles,
|
||||
sshData,
|
||||
hosts,
|
||||
} from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
@@ -191,15 +190,27 @@ class SharedCredentialManager {
|
||||
const cred = sharedCred[0].shared_credentials;
|
||||
|
||||
if (cred.needsReEncryption) {
|
||||
databaseLogger.warn(
|
||||
"Shared credential needs re-encryption but cannot be accessed yet",
|
||||
{
|
||||
operation: "get_shared_credential_pending",
|
||||
hostId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
await this.reEncryptSharedCredential(cred.id, userId);
|
||||
|
||||
const refreshed = await db
|
||||
.select()
|
||||
.from(sharedCredentials)
|
||||
.where(eq(sharedCredentials.id, cred.id))
|
||||
.limit(1);
|
||||
|
||||
if (refreshed.length === 0 || refreshed[0].needsReEncryption) {
|
||||
databaseLogger.warn(
|
||||
"Shared credential needs re-encryption but cannot be accessed yet",
|
||||
{
|
||||
operation: "get_shared_credential_pending",
|
||||
hostId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.decryptSharedCredential(refreshed[0], userDEK);
|
||||
}
|
||||
|
||||
return this.decryptSharedCredential(cred, userDEK);
|
||||
@@ -293,10 +304,9 @@ class SharedCredentialManager {
|
||||
credentialId: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await db
|
||||
await db
|
||||
.delete(sharedCredentials)
|
||||
.where(eq(sharedCredentials.originalCredentialId, credentialId))
|
||||
.returning({ id: sharedCredentials.id });
|
||||
.where(eq(sharedCredentials.originalCredentialId, credentialId));
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete shared credentials", error, {
|
||||
operation: "delete_shared_credentials",
|
||||
@@ -364,9 +374,9 @@ class SharedCredentialManager {
|
||||
key: cred.key
|
||||
? this.decryptField(cred.key, ownerDEK, credentialId, "key")
|
||||
: undefined,
|
||||
keyPassword: cred.key_password
|
||||
keyPassword: cred.keyPassword
|
||||
? this.decryptField(
|
||||
cred.key_password,
|
||||
cred.keyPassword,
|
||||
ownerDEK,
|
||||
credentialId,
|
||||
"key_password",
|
||||
@@ -534,7 +544,7 @@ class SharedCredentialManager {
|
||||
recordId.toString(),
|
||||
fieldName,
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
databaseLogger.warn("Field decryption failed, returning as-is", {
|
||||
operation: "decrypt_field",
|
||||
fieldName,
|
||||
@@ -589,7 +599,7 @@ class SharedCredentialManager {
|
||||
const access = await db
|
||||
.select()
|
||||
.from(hostAccess)
|
||||
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
|
||||
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
|
||||
.where(eq(hostAccess.id, cred.hostAccessId))
|
||||
.limit(1);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
import type { SQL } from "drizzle-orm";
|
||||
|
||||
type TableName =
|
||||
| "users"
|
||||
@@ -11,7 +12,7 @@ type TableName =
|
||||
|
||||
class SimpleDBOps {
|
||||
static async insert<T extends Record<string, unknown>>(
|
||||
table: SQLiteTable<any>,
|
||||
table: SQLiteTable,
|
||||
tableName: TableName,
|
||||
data: T,
|
||||
userId: string,
|
||||
@@ -109,7 +110,7 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async update<T extends Record<string, unknown>>(
|
||||
table: SQLiteTable<any>,
|
||||
table: SQLiteTable,
|
||||
tableName: TableName,
|
||||
where: unknown,
|
||||
data: Partial<T>,
|
||||
@@ -141,7 +142,7 @@ class SimpleDBOps {
|
||||
const result = await getDb()
|
||||
.update(table)
|
||||
.set(encryptedData)
|
||||
.where(where as any)
|
||||
.where(where as SQL | undefined)
|
||||
.returning();
|
||||
|
||||
DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
|
||||
@@ -157,13 +158,13 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async delete(
|
||||
table: SQLiteTable<any>,
|
||||
table: SQLiteTable,
|
||||
tableName: TableName,
|
||||
where: unknown,
|
||||
): Promise<unknown[]> {
|
||||
const result = await getDb()
|
||||
.delete(table)
|
||||
.where(where as any)
|
||||
.where(where as SQL | undefined)
|
||||
.returning();
|
||||
|
||||
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
|
||||
|
||||
@@ -1,117 +1,8 @@
|
||||
import { SocksClient } from "socks";
|
||||
import type { SocksClientOptions } from "socks";
|
||||
import net from "net";
|
||||
import { sshLogger } from "./logger.js";
|
||||
import type { ProxyNode } from "../../types/index.js";
|
||||
|
||||
export interface SOCKS5Config {
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
socks5Port?: number;
|
||||
socks5Username?: string;
|
||||
socks5Password?: string;
|
||||
socks5ProxyChain?: ProxyNode[];
|
||||
}
|
||||
|
||||
export async function createSocks5Connection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
socks5Config: SOCKS5Config,
|
||||
): Promise<net.Socket | null> {
|
||||
if (!socks5Config.useSocks5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
socks5Config.socks5ProxyChain &&
|
||||
socks5Config.socks5ProxyChain.length > 0
|
||||
) {
|
||||
return createProxyChainConnection(
|
||||
targetHost,
|
||||
targetPort,
|
||||
socks5Config.socks5ProxyChain,
|
||||
);
|
||||
}
|
||||
|
||||
if (socks5Config.socks5Host) {
|
||||
return createSingleProxyConnection(targetHost, targetPort, socks5Config);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createSingleProxyConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
socks5Config: SOCKS5Config,
|
||||
): Promise<net.Socket> {
|
||||
const socksOptions: SocksClientOptions = {
|
||||
proxy: {
|
||||
host: socks5Config.socks5Host!,
|
||||
port: socks5Config.socks5Port || 1080,
|
||||
type: 5,
|
||||
userId: socks5Config.socks5Username,
|
||||
password: socks5Config.socks5Password,
|
||||
},
|
||||
command: "connect",
|
||||
destination: {
|
||||
host: targetHost,
|
||||
port: targetPort,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await SocksClient.createConnection(socksOptions);
|
||||
|
||||
return info.socket;
|
||||
} catch (error) {
|
||||
sshLogger.error("SOCKS5 connection failed", error, {
|
||||
operation: "socks5_connect_failed",
|
||||
proxyHost: socks5Config.socks5Host,
|
||||
proxyPort: socks5Config.socks5Port || 1080,
|
||||
targetHost,
|
||||
targetPort,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProxyChainConnection(
|
||||
targetHost: string,
|
||||
targetPort: number,
|
||||
proxyChain: ProxyNode[],
|
||||
): Promise<net.Socket> {
|
||||
if (proxyChain.length === 0) {
|
||||
throw new Error("Proxy chain is empty");
|
||||
}
|
||||
|
||||
const chainPath = proxyChain.map((p) => `${p.host}:${p.port}`).join(" → ");
|
||||
try {
|
||||
const info = await SocksClient.createConnectionChain({
|
||||
proxies: proxyChain.map((p) => ({
|
||||
host: p.host,
|
||||
port: p.port,
|
||||
type: p.type,
|
||||
userId: p.username,
|
||||
password: p.password,
|
||||
timeout: 10000,
|
||||
})),
|
||||
command: "connect",
|
||||
destination: {
|
||||
host: targetHost,
|
||||
port: targetPort,
|
||||
},
|
||||
});
|
||||
return info.socket;
|
||||
} catch (error) {
|
||||
sshLogger.error("SOCKS proxy chain connection failed", error, {
|
||||
operation: "socks5_chain_connect_failed",
|
||||
chainLength: proxyChain.length,
|
||||
targetHost,
|
||||
targetPort,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export {
|
||||
createSocks5Connection,
|
||||
createProxyConnection,
|
||||
createHttpConnectConnection,
|
||||
createMixedProxyChainConnection,
|
||||
testProxyConnectivity,
|
||||
} from "./proxy-helper.js";
|
||||
export type { SOCKS5Config } from "./proxy-helper.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ssh2Pkg from "ssh2";
|
||||
import { sshLogger } from "./logger.js";
|
||||
const ssh2Utils = ssh2Pkg.utils;
|
||||
|
||||
function detectKeyTypeFromContent(keyContent: string): string {
|
||||
@@ -85,7 +84,9 @@ function detectKeyTypeFromContent(keyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - base64 decode may fail for some key formats
|
||||
}
|
||||
|
||||
if (content.length < 800) {
|
||||
return "ssh-ed25519";
|
||||
@@ -141,7 +142,9 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - base64 decode may fail for some key formats
|
||||
}
|
||||
|
||||
if (content.length < 400) {
|
||||
return "ssh-ed25519";
|
||||
@@ -243,7 +246,9 @@ export function parseSSHKey(
|
||||
|
||||
useSSH2 = true;
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - ssh2 key parsing may fail
|
||||
}
|
||||
}
|
||||
|
||||
if (!useSSH2) {
|
||||
@@ -269,7 +274,9 @@ export function parseSSHKey(
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - fallback key type detection may fail
|
||||
}
|
||||
|
||||
return {
|
||||
privateKey: privateKeyData,
|
||||
|
||||
@@ -52,7 +52,9 @@ class SystemCrypto {
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (fileError) {}
|
||||
} catch {
|
||||
// expected - env file may not exist
|
||||
}
|
||||
|
||||
await this.generateAndGuideUser();
|
||||
} catch (error) {
|
||||
@@ -78,12 +80,6 @@ class SystemCrypto {
|
||||
const envKey = process.env.DATABASE_KEY;
|
||||
if (envKey && envKey.length >= 64) {
|
||||
this.databaseKey = Buffer.from(envKey, "hex");
|
||||
const keyFingerprint = crypto
|
||||
.createHash("sha256")
|
||||
.update(this.databaseKey)
|
||||
.digest("hex")
|
||||
.substring(0, 16);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,17 +89,13 @@ class SystemCrypto {
|
||||
if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
|
||||
this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
|
||||
process.env.DATABASE_KEY = dbKeyMatch[1];
|
||||
|
||||
const keyFingerprint = crypto
|
||||
.createHash("sha256")
|
||||
.update(this.databaseKey)
|
||||
.digest("hex")
|
||||
.substring(0, 16);
|
||||
|
||||
return;
|
||||
} else {
|
||||
// expected - key not found or invalid length in env file
|
||||
}
|
||||
} catch (fileError) {}
|
||||
} catch {
|
||||
// expected - env file may not exist
|
||||
}
|
||||
|
||||
await this.generateAndGuideDatabaseKey();
|
||||
} catch (error) {
|
||||
@@ -141,7 +133,9 @@ class SystemCrypto {
|
||||
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// expected - env file may not exist
|
||||
}
|
||||
|
||||
await this.generateAndGuideInternalAuthToken();
|
||||
} catch (error) {
|
||||
@@ -178,7 +172,9 @@ class SystemCrypto {
|
||||
process.env.CREDENTIAL_SHARING_KEY = csKeyMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch (fileError) {}
|
||||
} catch {
|
||||
// expected - env file may not exist
|
||||
}
|
||||
|
||||
await this.generateAndGuideCredentialSharingKey();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Request } from "express";
|
||||
import crypto from "crypto";
|
||||
|
||||
export type DeviceType = "web" | "desktop" | "mobile";
|
||||
|
||||
@@ -22,7 +23,18 @@ export function detectPlatform(req: Request): DeviceType {
|
||||
return "mobile";
|
||||
}
|
||||
|
||||
if (userAgent.includes("Android")) {
|
||||
const isDesktopOS =
|
||||
userAgent.includes("Windows") ||
|
||||
userAgent.includes("Macintosh") ||
|
||||
userAgent.includes("Mac OS X") ||
|
||||
userAgent.includes("X11") ||
|
||||
userAgent.includes("Linux x86_64");
|
||||
|
||||
if (
|
||||
(userAgent.includes("Android") && !isDesktopOS) ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("iPad")
|
||||
) {
|
||||
return "mobile";
|
||||
}
|
||||
|
||||
@@ -237,3 +249,22 @@ function parseMacVersion(userAgent: string): string {
|
||||
}
|
||||
return "macOS";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stable device fingerprint based on device type, browser, and OS.
|
||||
* 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}`;
|
||||
}
|
||||
|
||||
return crypto.createHash("sha256").update(fingerprintString).digest("hex");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import {
|
||||
users,
|
||||
sshData,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
@@ -74,8 +74,8 @@ class UserDataExport {
|
||||
|
||||
const sshHosts = await getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.userId, userId));
|
||||
.from(hosts)
|
||||
.where(eq(hosts.userId, userId));
|
||||
const processedSshHosts =
|
||||
format === "plaintext" && userDataKey
|
||||
? sshHosts.map((host) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import {
|
||||
users,
|
||||
sshData,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
@@ -179,13 +179,13 @@ class UserDataImport {
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.userId, targetUserId),
|
||||
eq(sshData.ip, host.ip as string),
|
||||
eq(sshData.port, host.port as number),
|
||||
eq(sshData.username, host.username as string),
|
||||
eq(hosts.userId, targetUserId),
|
||||
eq(hosts.ip, host.ip as string),
|
||||
eq(hosts.port, host.port as number),
|
||||
eq(hosts.username, host.username as string),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -194,7 +194,7 @@ class UserDataImport {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newHostData: any = {
|
||||
const newHostData: Record<string, unknown> = {
|
||||
...host,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -204,7 +204,7 @@ class UserDataImport {
|
||||
newHostData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedHostData: any = newHostData;
|
||||
let processedHostData: Record<string, unknown> = newHostData;
|
||||
if (options.userDataKey) {
|
||||
processedHostData = DataCrypto.encryptRecord(
|
||||
"ssh_data",
|
||||
@@ -218,15 +218,13 @@ class UserDataImport {
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(sshData)
|
||||
.set(processedHostData as unknown as typeof sshData.$inferInsert)
|
||||
.where(eq(sshData.id, existing[0].id));
|
||||
.update(hosts)
|
||||
.set(processedHostData as unknown as typeof hosts.$inferInsert)
|
||||
.where(eq(hosts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(sshData)
|
||||
.values(
|
||||
processedHostData as unknown as typeof sshData.$inferInsert,
|
||||
);
|
||||
.insert(hosts)
|
||||
.values(processedHostData as unknown as typeof hosts.$inferInsert);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
@@ -275,7 +273,7 @@ class UserDataImport {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newCredentialData: any = {
|
||||
const newCredentialData: Record<string, unknown> = {
|
||||
...credential,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -287,7 +285,8 @@ class UserDataImport {
|
||||
newCredentialData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedCredentialData: any = newCredentialData;
|
||||
let processedCredentialData: Record<string, unknown> =
|
||||
newCredentialData;
|
||||
if (options.userDataKey) {
|
||||
processedCredentialData = DataCrypto.encryptRecord(
|
||||
"ssh_credentials",
|
||||
|
||||
@@ -37,8 +37,7 @@ const buttonVariants = cva(
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ComponentProps<"button">,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -762,6 +762,8 @@ export const DEFAULT_TERMINAL_CONFIG = {
|
||||
autoMosh: false,
|
||||
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
|
||||
sudoPasswordAutoFill: false,
|
||||
keepaliveInterval: undefined as number | undefined,
|
||||
keepaliveCountMax: undefined as number | undefined,
|
||||
};
|
||||
|
||||
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
IClipboardProvider,
|
||||
ClipboardSelectionType,
|
||||
} from "@xterm/addon-clipboard";
|
||||
|
||||
export class RobustClipboardProvider implements IClipboardProvider {
|
||||
private pendingWrite: string | null = null;
|
||||
private readonly focusHandler: () => void;
|
||||
|
||||
constructor() {
|
||||
this.focusHandler = () => {
|
||||
if (this.pendingWrite !== null) {
|
||||
const text = this.pendingWrite;
|
||||
this.pendingWrite = null;
|
||||
navigator.clipboard.writeText(text).catch(() => {
|
||||
this.pendingWrite = text;
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener("focus", this.focusHandler);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
window.removeEventListener("focus", this.focusHandler);
|
||||
this.pendingWrite = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
readText(selection: ClipboardSelectionType): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
async writeText(
|
||||
selection: ClipboardSelectionType,
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (window.electronClipboard) {
|
||||
window.electronClipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
this.pendingWrite = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,14 +113,14 @@ const PATTERNS: HighlightPattern[] = [
|
||||
function hasExistingAnsiCodes(text: string): boolean {
|
||||
const ansiCount = (
|
||||
text.match(
|
||||
/\x1b[\[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g,
|
||||
/\x1b[[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g,
|
||||
) || []
|
||||
).length;
|
||||
return ansiCount > MAX_ANSI_CODES;
|
||||
}
|
||||
|
||||
function hasIncompleteAnsiSequence(text: string): boolean {
|
||||
return /\x1b(?:\[(?:[0-9;]*)?)?$/.test(text);
|
||||
return /\x1b(?:\[(?:[0-9;?>=!]*)?)?$/.test(text);
|
||||
}
|
||||
|
||||
interface TextSegment {
|
||||
@@ -130,7 +130,7 @@ interface TextSegment {
|
||||
|
||||
function parseAnsiSegments(text: string): TextSegment[] {
|
||||
const segments: TextSegment[] = [];
|
||||
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;]*[@-~])/g;
|
||||
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;?>=!]*[@-~])/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
|
||||
+87
-6
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "2-Way",
|
||||
"threeSplit": "3-Way",
|
||||
"fourSplit": "4-Way",
|
||||
"fiveSplit": "5-Way",
|
||||
"sixSplit": "6-Way",
|
||||
"availableTabs": "Available Tabs",
|
||||
"dragTabsHint": "Drag tabs to the layout cells below to assign them",
|
||||
"layout": "Split Screen Layout",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "Warning",
|
||||
"notValidatedWarning": "URL not validated - ensure it's correct",
|
||||
"changeServer": "Change Server",
|
||||
"mustIncludeProtocol": "Server URL must start with http:// or https://"
|
||||
"mustIncludeProtocol": "Server URL must start with http:// or https://",
|
||||
"useEmbedded": "Use Local Server",
|
||||
"embeddedDesc": "Run Termix with the built-in local server (no remote server needed)",
|
||||
"embeddedConnecting": "Connecting to local server...",
|
||||
"embeddedNotReady": "Local server is not ready yet. Please wait a moment and try again."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "Version Check Error",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "Confirm",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"or": "OR",
|
||||
"ok": "OK",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "User {{username}} deleted successfully",
|
||||
"failedToDeleteUser": "Failed to delete user",
|
||||
"overrideUserInfoUrl": "Override User Info URL (not required)",
|
||||
"allowedUsers": "Allowed Users",
|
||||
"allowedUsersDescription": "Comma-separated list of allowed email patterns. Use exact emails (user@example.com), domain suffixes (@example.com), or leave empty to allow all users.",
|
||||
"failedToFetchSessions": "Failed to fetch sessions",
|
||||
"sessionRevokedSuccessfully": "Session revoked successfully",
|
||||
"failedToRevokeSession": "Failed to revoke session",
|
||||
@@ -794,6 +803,14 @@
|
||||
"oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.",
|
||||
"confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?",
|
||||
"failedToUpdatePasswordLoginStatus": "Failed to update password login status",
|
||||
"monitoringDefaults": "Monitoring Defaults",
|
||||
"monitoringDefaultsDesc": "Global default intervals for status checks and metrics collection. Per-host settings override these values.",
|
||||
"globalStatusCheckInterval": "Default Status Check Interval",
|
||||
"globalMetricsInterval": "Default Metrics Collection Interval",
|
||||
"globalSettingsSaved": "Global monitoring settings saved",
|
||||
"failedToSaveGlobalSettings": "Failed to save global monitoring settings",
|
||||
"failedToLoadGlobalSettings": "Failed to load global monitoring settings",
|
||||
"clampedToValidRange": "was adjusted to valid range",
|
||||
"sessionManagement": "Session Management",
|
||||
"loadingSessions": "Loading sessions...",
|
||||
"noActiveSessions": "No active sessions found.",
|
||||
@@ -849,6 +866,11 @@
|
||||
"noHostsInJson": "No hosts found in JSON file",
|
||||
"maxHostsAllowed": "Maximum 100 hosts allowed per import",
|
||||
"importCompleted": "Import completed: {{success}} successful, {{failed}} failed",
|
||||
"importCreated": "created",
|
||||
"importUpdated": "updated",
|
||||
"importFailedCount": "failed",
|
||||
"importSkipExisting": "Import (skip existing)",
|
||||
"importOverwriteExisting": "Import (overwrite existing)",
|
||||
"importFailed": "Import failed",
|
||||
"importError": "Import error",
|
||||
"failedToImportJson": "Failed to import JSON file",
|
||||
@@ -1027,6 +1049,9 @@
|
||||
"statusCheckEnabledDesc": "Check if the server is online or offline",
|
||||
"statusCheckInterval": "Status Check Interval",
|
||||
"statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)",
|
||||
"useGlobalStatusInterval": "Use global default",
|
||||
"useGlobalMetricsInterval": "Use global default",
|
||||
"usingGlobalDefault": "Using global default ({{value}}s)",
|
||||
"metricsEnabled": "Enable Metrics Monitoring",
|
||||
"metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics",
|
||||
"metricsInterval": "Metrics Collection Interval",
|
||||
@@ -1184,8 +1209,20 @@
|
||||
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password",
|
||||
"sudoPassword": "Sudo Password",
|
||||
"sudoPasswordDesc": "Optional password for sudo commands (useful with key authentication)",
|
||||
"keepaliveSettings": "SSH Keepalive",
|
||||
"keepaliveSettingsDesc": "Configure SSH-level keepalive behavior. Set interval to 0 to disable SSH keepalives (useful for MikroTik and other devices that ignore keepalive requests).",
|
||||
"keepaliveInterval": "Keepalive Interval (ms)",
|
||||
"keepaliveIntervalDesc": "How often to send SSH keepalive pings. Default: 30000 (30s). Set to 0 to disable.",
|
||||
"keepaliveCountMax": "Keepalive Max Failures",
|
||||
"keepaliveCountMaxDesc": "Disconnect after this many unanswered keepalives. Default: 3.",
|
||||
"socks4": "SOCKS4",
|
||||
"socks5": "SOCKS5",
|
||||
"httpConnect": "HTTP CONNECT",
|
||||
"testProxy": "Test Connection",
|
||||
"testingProxy": "Testing...",
|
||||
"proxyTestSuccess": "Proxy connection successful ({{latency}}ms)",
|
||||
"proxyTestFailed": "Proxy test failed: {{error}}",
|
||||
"connectionPath": "Connection Path",
|
||||
"executeSnippetOnConnect": "Execute a snippet when the terminal connects",
|
||||
"autoMosh": "Auto-MOSH",
|
||||
"autoMoshDesc": "Automatically run MOSH command on connect",
|
||||
@@ -1296,7 +1333,35 @@
|
||||
"showTimestamps": "Show Timestamps",
|
||||
"autoRefresh": "Auto Refresh",
|
||||
"filterLogsPlaceholder": "Filter logs...",
|
||||
"noLogsAvailable": "No logs available"
|
||||
"noLogsAvailable": "No logs available",
|
||||
"selectMode": "Select",
|
||||
"exitSelectMode": "Cancel",
|
||||
"selectedCount": "{{count}} selected",
|
||||
"bulkMonitoring": "Monitoring",
|
||||
"bulkFeatures": "Features",
|
||||
"bulkMoveFolder": "Move to Folder",
|
||||
"bulkPin": "Pin",
|
||||
"bulkUnpin": "Unpin",
|
||||
"enableAllFeatures": "Enable All Features",
|
||||
"disableAllFeatures": "Disable All Features",
|
||||
"enableStatusCheck": "Enable Status Check",
|
||||
"disableStatusCheck": "Disable Status Check",
|
||||
"enableMetrics": "Enable Metrics",
|
||||
"disableMetrics": "Disable Metrics",
|
||||
"bulkEnableTerminal": "Enable Terminal",
|
||||
"bulkDisableTerminal": "Disable Terminal",
|
||||
"bulkEnableTunnel": "Enable Tunnel",
|
||||
"bulkDisableTunnel": "Disable Tunnel",
|
||||
"bulkEnableFileManager": "Enable File Manager",
|
||||
"bulkDisableFileManager": "Disable File Manager",
|
||||
"bulkEnableDocker": "Enable Docker",
|
||||
"bulkDisableDocker": "Disable Docker",
|
||||
"bulkUpdateSuccess": "Updated {{count}} host(s) successfully",
|
||||
"bulkUpdateFailed": "Bulk update failed",
|
||||
"selectAll": "Select all",
|
||||
"deselectAll": "Deselect all",
|
||||
"useGlobalStatusDefault": "Use Global Default (Status)",
|
||||
"useGlobalMetricsDefault": "Use Global Default (Metrics)"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Split Screen",
|
||||
@@ -1304,6 +1369,8 @@
|
||||
"twoSplit": "2-Split",
|
||||
"threeSplit": "3-Split",
|
||||
"fourSplit": "4-Split",
|
||||
"fiveSplit": "5-Split",
|
||||
"sixSplit": "6-Split",
|
||||
"availableTabs": "Available Tabs",
|
||||
"dragTabsHint": "Drag tabs into the grid below to position them",
|
||||
"layout": "Layout",
|
||||
@@ -1339,6 +1406,8 @@
|
||||
"connectionClosed": "Connection closed",
|
||||
"connectionError": "Connection error: {{message}}",
|
||||
"connected": "Connected",
|
||||
"clipboardWriteFailed": "Failed to copy to clipboard. Make sure the page is served over HTTPS or localhost.",
|
||||
"clipboardReadFailed": "Failed to read from clipboard. Make sure clipboard permissions are granted.",
|
||||
"sshConnected": "SSH connection established",
|
||||
"authError": "Authentication failed: {{message}}",
|
||||
"unknownError": "Unknown error occurred",
|
||||
@@ -1393,7 +1462,9 @@
|
||||
"totpTimeout": "TOTP verification timeout. Please reconnect.",
|
||||
"passwordTimeout": "Password verification timeout. Please reconnect.",
|
||||
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
|
||||
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled."
|
||||
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled.",
|
||||
"sessionTakenOver": "Session was opened in another tab. Reconnecting...",
|
||||
"sessionAttachTimeout": "Session attachment timed out. Creating new connection..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "File Manager",
|
||||
@@ -1718,7 +1789,13 @@
|
||||
"write": "Write",
|
||||
"execute": "Execute",
|
||||
"permissionsChangedSuccessfully": "Permissions changed successfully",
|
||||
"failedToChangePermissions": "Failed to change permissions"
|
||||
"failedToChangePermissions": "Failed to change permissions",
|
||||
"name": "Name",
|
||||
"sortByName": "Name",
|
||||
"sortByDate": "Date Modified",
|
||||
"sortBySize": "Size",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending"
|
||||
},
|
||||
"tunnel": {
|
||||
"noTunnelsConfigured": "No Tunnels Configured",
|
||||
@@ -1897,7 +1974,7 @@
|
||||
"loginButton": "Login",
|
||||
"registerButton": "Register",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"rememberMe": "Remember Me",
|
||||
"rememberMe": "Remember Device for 30 Days (includes TOTP)",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"loginSuccess": "Login successful",
|
||||
@@ -2062,6 +2139,7 @@
|
||||
"processing": "Processing...",
|
||||
"pleaseWait": "Please wait...",
|
||||
"registrationDisabled": "New account registration is currently disabled by an admin. Please log in or contact an administrator.",
|
||||
"userNotAllowed": "Your account is not authorized to register. Please contact an administrator.",
|
||||
"databaseConnected": "Database connected successfully",
|
||||
"databaseConnectionFailed": "Failed to connect to the database server",
|
||||
"checkServerConnection": "Please check your server connection and try again",
|
||||
@@ -2117,7 +2195,9 @@
|
||||
"appearanceDesc": "Select the color theme for the application",
|
||||
"terminalSyntaxHighlightingDesc": "Automatically highlight commands, paths, IPs, and log levels in terminal output",
|
||||
"enableCommandPaletteShortcut": "Enable Command Palette Shortcut",
|
||||
"enableCommandPaletteShortcutDesc": "Double-tap left Shift to open the Command Palette for quick access to hosts"
|
||||
"enableCommandPaletteShortcutDesc": "Double-tap left Shift to open the Command Palette for quick access to hosts",
|
||||
"enableTerminalSessionPersistence": "Persistent Terminal Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "Maintain SSH connections when switching tabs or closing the browser (may be unstable)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "Failed to load version information"
|
||||
@@ -2153,6 +2233,7 @@
|
||||
"usernameField": "name",
|
||||
"scopes": "openid email profile",
|
||||
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
|
||||
"allowedUsers": "user@example.com, @company.com",
|
||||
"enterUsername": "Enter username to make admin",
|
||||
"searchHosts": "Search hosts by name, username, IP, folder, tags...",
|
||||
"enterPassword": "Enter your password",
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "2-rigting",
|
||||
"threeSplit": "3-rigting",
|
||||
"fourSplit": "4-rigting",
|
||||
"fiveSplit": "5-rigting",
|
||||
"sixSplit": "6-rigting",
|
||||
"availableTabs": "Beskikbare oortjies",
|
||||
"dragTabsHint": "Sleep oortjies na die uitlegselle hieronder om hulle toe te ken",
|
||||
"layout": "Gesplete skermuitleg",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "Waarskuwing",
|
||||
"notValidatedWarning": "URL nie gevalideer nie - maak seker dit is korrek",
|
||||
"changeServer": "Verander bediener",
|
||||
"mustIncludeProtocol": "Bediener-URL moet met http:// of https:// begin"
|
||||
"mustIncludeProtocol": "Bediener-URL moet met http:// of https:// begin",
|
||||
"useEmbedded": "Gebruik plaaslike bediener",
|
||||
"embeddedDesc": "Begin Termix met die ingeboude plaaslike bediener (geen afstandbediener nodig nie)",
|
||||
"embeddedConnecting": "Verbind met plaaslike bediener...",
|
||||
"embeddedNotReady": "Die plaaslike bediener is nog nie gereed nie. Wag asseblief 'n oomblik en probeer weer."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "Weergawekontrolefout",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "Bevestig",
|
||||
"yes": "Ja",
|
||||
"no": "Nee",
|
||||
"or": "OF",
|
||||
"ok": "Goed",
|
||||
"enabled": "Geaktiveer",
|
||||
"disabled": "Gestremd",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "Gebruiker {{username}} suksesvol verwyder",
|
||||
"failedToDeleteUser": "Kon nie gebruiker verwyder nie",
|
||||
"overrideUserInfoUrl": "Oorskryf Gebruikersinligting-URL (nie vereis nie)",
|
||||
"allowedUsers": "Toegelate Gebruikers",
|
||||
"allowedUsersDescription": "Komma-geskeide lys van toegelate e-pospatrone. Gebruik presiese e-posadresse (gebruiker@voorbeeld.com), domeinagtervoegsels (@voorbeeld.com), of los leeg om alle gebruikers toe te laat.",
|
||||
"failedToFetchSessions": "Kon nie sessies haal nie",
|
||||
"sessionRevokedSuccessfully": "Sessie suksesvol herroep",
|
||||
"failedToRevokeSession": "Kon nie sessie herroep nie",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "KRITIES: Wagwoordaanmelding is gedeaktiveer. As jy OIDC herstel of verkeerd konfigureer, sal jy alle toegang tot Termix verloor en jou instansie blokkeer. Gaan slegs voort as jy absoluut seker is.",
|
||||
"confirmDisableOIDCWarning": "WAARSKUWING: Jy is op die punt om OIDC te deaktiveer terwyl wagwoordaanmelding ook gedeaktiveer is. Dit sal jou Termix-instansie blokkeer en jy sal alle toegang verloor. Is jy absoluut seker jy wil voortgaan?",
|
||||
"failedToUpdatePasswordLoginStatus": "Kon nie wagwoord-aanmeldstatus opdateer nie",
|
||||
"monitoringDefaults": "Monitering van Standaardwaardes",
|
||||
"monitoringDefaultsDesc": "Globale standaardintervalle vir statuskontroles en metrieke-insameling. Instellings per gasheer ignoreer hierdie waardes.",
|
||||
"globalStatusCheckInterval": "Standaardstatuskontrole-interval",
|
||||
"globalMetricsInterval": "Standaard Metrieke Versamelingsinterval",
|
||||
"globalSettingsSaved": "Globale moniteringsinstellings gestoor",
|
||||
"failedToSaveGlobalSettings": "Kon nie globale moniteringsinstellings stoor nie",
|
||||
"failedToLoadGlobalSettings": "Kon nie globale moniteringsinstellings laai nie",
|
||||
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
|
||||
"guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.",
|
||||
"enableGuacamole": "Enable RDP/VNC/Telnet support",
|
||||
"guacdUrl": "guacd URL (host:port)",
|
||||
"guacdUrlPlaceholder": "guacd:4822",
|
||||
"guacdUrlNote": "Changes to the guacd host/port require a server restart to take effect.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "was adjusted to valid range",
|
||||
"loadingSessions": "Laai sessies...",
|
||||
"noActiveSessions": "Geen aktiewe sessies gevind nie.",
|
||||
"device": "Toestel",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "Geen gashere in JSON-lêer gevind nie",
|
||||
"maxHostsAllowed": "Maksimum 100 gashere toegelaat per invoer",
|
||||
"importCompleted": "Invoer voltooi: {{success}} suksesvol, {{failed}} misluk",
|
||||
"importCreated": "geskep",
|
||||
"importUpdated": "opgedateer",
|
||||
"importFailedCount": "misluk",
|
||||
"importSkipExisting": "Invoer (slaan bestaande oor)",
|
||||
"importOverwriteExisting": "Invoer (oorskryf bestaande)",
|
||||
"importFailed": "Invoer het misluk",
|
||||
"importError": "Invoerfout",
|
||||
"failedToImportJson": "Kon nie JSON-lêer invoer nie",
|
||||
"connectionDetails": "Verbindingsbesonderhede",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "Organisasie",
|
||||
"ipAddress": "IP-adres of gasheernaam",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "Hawe",
|
||||
"name": "Naam",
|
||||
"username": "Gebruikersnaam",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "Vouer",
|
||||
"tags": "Etikette",
|
||||
"pin": "Speld vas",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "Tonnel",
|
||||
"fileManager": "Lêerbestuurder",
|
||||
"serverStats": "Bedienerstatistieke",
|
||||
"status": "Status",
|
||||
"statistics": "Statistiek",
|
||||
"hostViewer": "Gasheerkyker",
|
||||
"enableServerStats": "Aktiveer bedienerstatistieke",
|
||||
"enableServerStatsDesc": "Aktiveer/deaktiveer bedienerstatistiekversameling vir hierdie gasheer",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "Maak Lêerbestuurder oop",
|
||||
"openTunnels": "Oop Tonnels",
|
||||
"openServerDetails": "Maak bedienerbesonderhede oop",
|
||||
"statistics": "Statistiek",
|
||||
"enabledWidgets": "Geaktiveerde Widgets",
|
||||
"openServerStats": "Maak bedienerstatistieke oop",
|
||||
"enabledWidgetsDesc": "Kies watter statistiek-widgets vir hierdie gasheer vertoon moet word",
|
||||
@@ -1017,10 +1059,18 @@
|
||||
"statusCheckEnabledDesc": "Kontroleer of die bediener aanlyn of vanlyn is",
|
||||
"statusCheckInterval": "Statuskontrole-interval",
|
||||
"statusCheckIntervalDesc": "Hoe gereeld om te kyk of die gasheer aanlyn is (5s - 1u)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "Gebruik globale verstekwaarde",
|
||||
"useGlobalMetricsInterval": "Gebruik globale verstekwaarde",
|
||||
"usingGlobalDefault": "Gebruik globale verstekwaarde ({{value}}s)",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "Aktiveer Metrieke Monitering",
|
||||
"metricsEnabledDesc": "Versamel SVE-, RAM-, skyf- en ander stelselstatistieke",
|
||||
"metricsInterval": "Metrieke-insamelingsinterval",
|
||||
"metricsIntervalDesc": "Hoe gereeld om bedienerstatistieke in te samel (5s - 1u)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "sekondes",
|
||||
"intervalMinutes": "minute",
|
||||
"intervalValidation": "Moniteringsintervalle moet tussen 5 sekondes en 1 uur (3600 sekondes) wees.",
|
||||
@@ -1174,8 +1224,20 @@
|
||||
"sudoPasswordAutoFillDesc": "Bied outomaties aan om SSH-wagwoord in te voeg wanneer sudo vir wagwoord vra",
|
||||
"sudoPassword": "Sudo-wagwoord",
|
||||
"sudoPasswordDesc": "Opsionele wagwoord vir sudo-opdragte (nuttig met sleutelverifikasie)",
|
||||
"keepaliveSettings": "SSH Keepalive",
|
||||
"keepaliveSettingsDesc": "Konfigureer SSH-vlak keepalive-gedrag. Stel interval op 0 om SSH keepalives te deaktiveer (nuttig vir MikroTik en ander toestelle wat keepalive-versoeke ignoreer).",
|
||||
"keepaliveInterval": "Lewendige interval (ms)",
|
||||
"keepaliveIntervalDesc": "Hoe gereeld om SSH keepalive pings te stuur. Standaard: 30000 (30s). Stel na 0 om te deaktiveer.",
|
||||
"keepaliveCountMax": "Hou Maksimum Mislukkings",
|
||||
"keepaliveCountMaxDesc": "Ontkoppel na soveel onbeantwoorde lewende boodskappe. Standaard: 3.",
|
||||
"socks4": "SOKKIES4",
|
||||
"socks5": "SOKKIES5",
|
||||
"httpConnect": "HTTP-VERBINDING",
|
||||
"testProxy": "Toetsverbinding",
|
||||
"testingProxy": "Toetsing...",
|
||||
"proxyTestSuccess": "Proxy-verbinding suksesvol ({{latency}}ms)",
|
||||
"proxyTestFailed": "Proxy-toets het misluk: {{error}}",
|
||||
"connectionPath": "Verbindingspad",
|
||||
"executeSnippetOnConnect": "Voer 'n brokkie uit wanneer die terminaal verbind",
|
||||
"autoMosh": "Outomatiese MOSH",
|
||||
"autoMoshDesc": "Voer outomaties die MOSH-opdrag uit tydens verbinding",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "Kopieer Tonnel URL",
|
||||
"copyServerStatsUrl": "Kopieer bedienerstatistieke-URL",
|
||||
"copyDockerUrl": "Kopieer Docker URL",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "Kopieer URL om hierdie program in volskermmodus oop te maak",
|
||||
"notEnabled": "Docker is nie vir hierdie gasheer geaktiveer nie. Aktiveer dit in Gasheerinstellings om Docker-funksies te gebruik.",
|
||||
"validating": "Valideer Docker...",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "Wys Tydstempels",
|
||||
"autoRefresh": "Outomatiese Herlaai",
|
||||
"filterLogsPlaceholder": "Filtreer logboeke...",
|
||||
"noLogsAvailable": "Geen logboeke beskikbaar nie"
|
||||
"noLogsAvailable": "Geen logboeke beskikbaar nie",
|
||||
"selectMode": "Kies",
|
||||
"exitSelectMode": "Kanselleer",
|
||||
"selectedCount": "{{count}} gekies",
|
||||
"bulkMonitoring": "Monitering",
|
||||
"bulkFeatures": "Kenmerke",
|
||||
"bulkMoveFolder": "Skuif na vouer",
|
||||
"bulkPin": "Speld vas",
|
||||
"bulkUnpin": "Ontspeld",
|
||||
"enableAllFeatures": "Aktiveer alle funksies",
|
||||
"disableAllFeatures": "Deaktiveer alle funksies",
|
||||
"enableStatusCheck": "Aktiveer Statuskontrole",
|
||||
"disableStatusCheck": "Deaktiveer Statuskontrole",
|
||||
"enableMetrics": "Aktiveer Metrieke",
|
||||
"disableMetrics": "Deaktiveer Metrieke",
|
||||
"bulkEnableTerminal": "Aktiveer Terminaal",
|
||||
"bulkDisableTerminal": "Deaktiveer terminaal",
|
||||
"bulkEnableTunnel": "Aktiveer Tonnel",
|
||||
"bulkDisableTunnel": "Deaktiveer Tonnel",
|
||||
"bulkEnableFileManager": "Aktiveer Lêerbestuurder",
|
||||
"bulkDisableFileManager": "Deaktiveer Lêerbestuurder",
|
||||
"bulkEnableDocker": "Aktiveer Docker",
|
||||
"bulkDisableDocker": "Deaktiveer Docker",
|
||||
"bulkUpdateSuccess": "{{count}} gasheer(s) suksesvol opgedateer",
|
||||
"bulkUpdateFailed": "Grootmaatopdatering het misluk",
|
||||
"selectAll": "Kies alles",
|
||||
"deselectAll": "Deselekteer alles",
|
||||
"useGlobalStatusDefault": "Gebruik globale verstekwaarde (status)",
|
||||
"useGlobalMetricsDefault": "Gebruik globale verstekwaarde (metrieke)",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminaal",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "Verbinding gesluit",
|
||||
"connectionError": "Verbindingsfout: {{message}}",
|
||||
"connected": "Verbonde",
|
||||
"clipboardWriteFailed": "Kon nie na knipbord kopieer nie. Maak seker dat die bladsy oor HTTPS of localhost bedien word.",
|
||||
"clipboardReadFailed": "Kon nie vanaf knipbord lees nie. Maak seker dat knipbordtoestemmings toegestaan is.",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "SSH-verbinding gevestig",
|
||||
"authError": "Verifikasie het misluk: {{message}}",
|
||||
"unknownError": "Onbekende fout het voorgekom",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "TOTP-verifikasie het uitgeloop. Koppel asseblief weer aan.",
|
||||
"passwordTimeout": "Wagwoordverifikasie het uitgeloop. Koppel asseblief weer aan.",
|
||||
"connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.",
|
||||
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer."
|
||||
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer.",
|
||||
"sessionTakenOver": "Sessie is in 'n ander oortjie oopgemaak. Herverbind tans...",
|
||||
"sessionAttachTimeout": "Sessie-aanhegsel het uitgetel. Skep nuwe verbinding..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Lêerbestuurder",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "Skryf",
|
||||
"execute": "Voer uit",
|
||||
"permissionsChangedSuccessfully": "Toestemmings suksesvol verander",
|
||||
"failedToChangePermissions": "Kon nie toestemmings verander nie"
|
||||
"failedToChangePermissions": "Kon nie toestemmings verander nie",
|
||||
"name": "Naam",
|
||||
"sortByName": "Naam",
|
||||
"sortByDate": "Datum Gewysig",
|
||||
"sortBySize": "Grootte",
|
||||
"ascending": "Stygend",
|
||||
"descending": "Dalend"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "Aanmeld",
|
||||
"registerButton": "Registreer",
|
||||
"forgotPassword": "Wagwoord vergeet?",
|
||||
"rememberMe": "Onthou My",
|
||||
"rememberMe": "Onthou toestel vir 30 dae (sluit TOTP in)",
|
||||
"noAccount": "Het jy nie 'n rekening nie?",
|
||||
"hasAccount": "Het jy reeds 'n rekening?",
|
||||
"loginSuccess": "Aanmelding suksesvol",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "Verwerk...",
|
||||
"pleaseWait": "Wag asseblief...",
|
||||
"registrationDisabled": "Nuwe rekeningregistrasie is tans deur 'n administrateur gedeaktiveer. Meld asseblief aan of kontak 'n administrateur.",
|
||||
"userNotAllowed": "Jou rekening is nie gemagtig om te registreer nie. Kontak asseblief 'n administrateur.",
|
||||
"databaseConnected": "Databasis suksesvol gekoppel",
|
||||
"databaseConnectionFailed": "Kon nie aan die databasisbediener koppel nie",
|
||||
"checkServerConnection": "Kontroleer asseblief u bedienerverbinding en probeer weer",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "Kies die kleurtema vir die toepassing",
|
||||
"terminalSyntaxHighlightingDesc": "Merk outomaties opdragte, paaie, IP's en logvlakke in terminaaluitvoer",
|
||||
"enableCommandPaletteShortcut": "Aktiveer die opdragpalet-kortpad",
|
||||
"enableCommandPaletteShortcutDesc": "Dubbeltik links Shift om die opdragpalet oop te maak vir vinnige toegang tot gashere"
|
||||
"enableCommandPaletteShortcutDesc": "Dubbeltik links Shift om die opdragpalet oop te maak vir vinnige toegang tot gashere",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "Handhaaf SSH-verbindings wanneer jy oortjies wissel of die blaaier toemaak (dit kan onstabiel wees)."
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "Kon nie weergawe-inligting laai nie"
|
||||
@@ -2114,6 +2309,7 @@
|
||||
"usernameField": "naam",
|
||||
"scopes": "openid e-posprofiel",
|
||||
"userinfoUrl": "https://jou-verskaffer.com/aansoek/o/gebruikersinfo/",
|
||||
"allowedUsers": "gebruiker@voorbeeld.com, @maatskappy.com",
|
||||
"enterUsername": "Voer gebruikersnaam in om admin te word",
|
||||
"searchHosts": "Soek gashere volgens naam, gebruikersnaam, IP, vouer, etikette...",
|
||||
"enterPassword": "Voer jou wagwoord in",
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "طريقتان",
|
||||
"threeSplit": "3-طريق",
|
||||
"fourSplit": "4-طريق",
|
||||
"fiveSplit": "5-الطريق",
|
||||
"sixSplit": "6-طريق",
|
||||
"availableTabs": "علامات التبويب المتاحة",
|
||||
"dragTabsHint": "اسحب علامات التبويب إلى خلايا التخطيط أدناه لتعيينها",
|
||||
"layout": "تقسيم تخطيط الشاشة",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "تحذير",
|
||||
"notValidatedWarning": "لم يتم التحقق من صحة الرابط - تأكد من صحته",
|
||||
"changeServer": "تغيير الخادم",
|
||||
"mustIncludeProtocol": "عنوان URL للخادم يجب أن يبدأ مع http:// أو https://"
|
||||
"mustIncludeProtocol": "عنوان URL للخادم يجب أن يبدأ مع http:// أو https://",
|
||||
"useEmbedded": "استخدام الخادم المحلي",
|
||||
"embeddedDesc": "تشغيل ترميكس مع الخادم المحلي المدمج (لا حاجة للخادم البعيد)",
|
||||
"embeddedConnecting": "جاري الاتصال بالخادم المحلي...",
|
||||
"embeddedNotReady": "الخادم المحلي غير جاهز بعد. الرجاء الانتظار لحظة وحاول مرة أخرى."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "خطأ في التحقق من الإصدار",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "تأكيد",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"or": "أو",
|
||||
"ok": "حسناً",
|
||||
"enabled": "تمكين",
|
||||
"disabled": "معطل",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "تم حذف المستخدم {{username}} بنجاح",
|
||||
"failedToDeleteUser": "فشل في حذف المستخدم",
|
||||
"overrideUserInfoUrl": "تجاوز رابط معلومات المستخدم (غير مطلوب)",
|
||||
"allowedUsers": "المستخدمين المسموح بهم",
|
||||
"allowedUsersDescription": "قائمة مفصولة بفواصل بأنماط البريد الإلكتروني المسموح بها. استخدم رسائل البريد الإلكتروني بالضبط (user@example.com)، المجال لاحقة (@example.com)، أو اترك فارغًا للسماح لجميع المستخدمين.",
|
||||
"failedToFetchSessions": "فشل في جلب الجلسات",
|
||||
"sessionRevokedSuccessfully": "تم إلغاء الجلسة بنجاح",
|
||||
"failedToRevokeSession": "فشل في إلغاء الجلسة",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "إنشاء: تم تعطيل تسجيل الدخول لكلمة المرور. إذا قمت بإعادة تعيين OIDC أو إساءة إعداده، فستفقد جميع الصلاحيات إلى Termix وستلغي مثالك. المضي قدما فقط إذا كنت متأكدا تماما.",
|
||||
"confirmDisableOIDCWarning": "تحذير: أنت على وشك تعطيل OIDC بينما يتم تعطيل تسجيل الدخول لكلمة المرور أيضا. سيؤدي هذا إلى طوب مثيل الـ Termix الخاص بك وسوف تفقد كل الوصول. هل أنت متأكد تماما من أنك تريد المتابعة؟",
|
||||
"failedToUpdatePasswordLoginStatus": "فشل تحديث حالة تسجيل الدخول لكلمة المرور",
|
||||
"monitoringDefaults": "مراقبة الافتراضات",
|
||||
"monitoringDefaultsDesc": "الفترات الافتراضية العالمية للتحقق من الحالة وجمع المقاييس. تتجاوز إعدادات كل مضيف هذه القيم.",
|
||||
"globalStatusCheckInterval": "الفاصل الزمني الافتراضي للتحقق من الحالة",
|
||||
"globalMetricsInterval": "الفاصل الزمني الافتراضي لمجموعة القياسات",
|
||||
"globalSettingsSaved": "تم حفظ إعدادات المراقبة العالمية",
|
||||
"failedToSaveGlobalSettings": "فشل في حفظ إعدادات المراقبة العالمية",
|
||||
"failedToLoadGlobalSettings": "فشل تحميل إعدادات المراقبة العالمية",
|
||||
"guacamoleIntegration": "تكامل سطح المكتب عن بعد (غواكامول)",
|
||||
"guacamoleIntegrationDesc": "تمكين اتصالات RDP و VNC و Telnet عبر guacd. يتطلب مثيل Gacd ليتم تشغيله.",
|
||||
"enableGuacamole": "تمكين دعم RDP/VNC/Telnet",
|
||||
"guacdUrl": "رابط guacd (المضيف:port)",
|
||||
"guacdUrlPlaceholder": "guacd:4822",
|
||||
"guacdUrlNote": "تتطلب التغييرات على مضيف/منفذ guacd إعادة تشغيل الخادم لتفعيلها.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "تم تعديله إلى نطاق صحيح",
|
||||
"loadingSessions": "جاري تحميل الجلسات...",
|
||||
"noActiveSessions": "لم يتم العثور على جلسات نشطة.",
|
||||
"device": "الجهاز",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "لم يتم العثور على مضيف في ملف JSON",
|
||||
"maxHostsAllowed": "الحد الأقصى المسموح به 100 مضيف لكل استيراد",
|
||||
"importCompleted": "اكتمل الاستيراد: {{success}} ناجح, {{failed}} فشل",
|
||||
"importCreated": "أنشئ",
|
||||
"importUpdated": "تم التحديث",
|
||||
"importFailedCount": "فشل",
|
||||
"importSkipExisting": "استيراد (تخطي قائم)",
|
||||
"importOverwriteExisting": "الاستيراد (الكتابة فوق القائمة)",
|
||||
"importFailed": "فشل الاستيراد",
|
||||
"importError": "خطأ في الاستيراد",
|
||||
"failedToImportJson": "فشل استيراد ملف JSON",
|
||||
"connectionDetails": "تفاصيل الاتصال",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "المنظمة",
|
||||
"ipAddress": "عنوان IP أو اسم المضيف",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "المنفذ",
|
||||
"name": "الاسم",
|
||||
"username": "اسم المستخدم",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "مجلد",
|
||||
"tags": "الوسوم",
|
||||
"pin": "تثبيت",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "نفق",
|
||||
"fileManager": "مدير الملفات",
|
||||
"serverStats": "إحصائيات الخادم",
|
||||
"status": "Status",
|
||||
"statistics": "الإحصائيات",
|
||||
"hostViewer": "عارض المضيف",
|
||||
"enableServerStats": "تمكين إحصائيات الخادم",
|
||||
"enableServerStatsDesc": "تمكين/تعطيل مجموعة إحصائيات الخادم لهذا المضيف",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "فتح مدير الملفات",
|
||||
"openTunnels": "فتح الأنفاق",
|
||||
"openServerDetails": "فتح تفاصيل الخادم",
|
||||
"statistics": "الإحصائيات",
|
||||
"enabledWidgets": "تمكين الودجت",
|
||||
"openServerStats": "فتح إحصائيات الخادم",
|
||||
"enabledWidgetsDesc": "حدد الاحصائيات التي سيتم عرضها لهذا المضيف",
|
||||
@@ -1017,10 +1059,18 @@
|
||||
"statusCheckEnabledDesc": "تحقق مما إذا كان الخادم متصلاً أو غير متصل",
|
||||
"statusCheckInterval": "فترة التحقق من الحالة",
|
||||
"statusCheckIntervalDesc": "كم عدد المرات للتحقق مما إذا كان المضيف متصل بالإنترنت (5s - 1ساعة)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "استخدام الافتراضي العام",
|
||||
"useGlobalMetricsInterval": "استخدام الافتراضي العام",
|
||||
"usingGlobalDefault": "استخدام الافتراضي العالمي ({{value}})",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "تمكين مراقبة القياسات",
|
||||
"metricsEnabledDesc": "جمع إحصائيات المعالجة، ذاكرة الوصول العشوائي، القرص وغيرها من إحصائيات النظام",
|
||||
"metricsInterval": "الفاصل الزمني لمجموعة القياسات",
|
||||
"metricsIntervalDesc": "كم عدد المرات لجمع إحصائيات الخادم (5s - 1ساعة)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "ثواني",
|
||||
"intervalMinutes": "دقائق",
|
||||
"intervalValidation": "يجب أن تكون فترات الرصد بين 5 ثوان وساعة واحدة (3600 ثانية)",
|
||||
@@ -1174,8 +1224,20 @@
|
||||
"sudoPasswordAutoFillDesc": "عرض تلقائي لإدراج كلمة مرور SSH عندما يطلب sudo لكلمة المرور",
|
||||
"sudoPassword": "كلمة مرور سودو",
|
||||
"sudoPasswordDesc": "كلمة المرور الاختيارية لأوامر sudo (مفيدة مع مصادقة المفتاح)",
|
||||
"keepaliveSettings": "حفظ SSH",
|
||||
"keepaliveSettingsDesc": "تهيئة السلوك على مستوى SSHH. تعيين الفاصل الزمني إلى 0 لتعطيل الحفاظ على SSH (مفيد لMikroTik والأجهزة الأخرى التي تتجاهل طلبات البقاء على قيد الحياة).",
|
||||
"keepaliveInterval": "الفاصل الزمني للحفاظ على (مللي ثانية)",
|
||||
"keepaliveIntervalDesc": "كم مرة إرسال صفحات SSH الاحتفاظ بالحياة. الافتراضي: 30000 (30). تعيين إلى 0 لتعطيل.",
|
||||
"keepaliveCountMax": "الحفاظ على الحد الأقصى للفشل",
|
||||
"keepaliveCountMaxDesc": "قطع الاتصال بعد هذا العديد من الابقاء بدون إجابة. الافتراضي: 3.",
|
||||
"socks4": "SOCKS4",
|
||||
"socks5": "SOCKS5",
|
||||
"httpConnect": "اتصال HTTP",
|
||||
"testProxy": "اختبار الاتصال",
|
||||
"testingProxy": "اختبار...",
|
||||
"proxyTestSuccess": "تم بنجاح اتصال البروكسي ({{latency}}ms)",
|
||||
"proxyTestFailed": "فشل اختبار البروكسي: {{error}}",
|
||||
"connectionPath": "مسار الاتصال",
|
||||
"executeSnippetOnConnect": "تنفيذ كتلة الكود عندما تتصل المحطة الطرفية",
|
||||
"autoMosh": "وضع تلقائي",
|
||||
"autoMoshDesc": "تشغيل أمر MOSH تلقائياً عند الاتصال",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "نسخ رابط النفق",
|
||||
"copyServerStatsUrl": "نسخ رابط إحصائيات الخادم",
|
||||
"copyDockerUrl": "نسخ رابط Docker",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "نسخ عنوان URL لفتح هذا التطبيق في وضع ملء الشاشة",
|
||||
"notEnabled": "Docker غير مفعل لهذا المضيف. قم بتمكينه في إعدادات المضيف لاستخدام ميزات Docker.",
|
||||
"validating": "التحقق من دوكر...",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "إظهار الطوابع الزمنية",
|
||||
"autoRefresh": "تحديث تلقائي",
|
||||
"filterLogsPlaceholder": "تصفية السجلات...",
|
||||
"noLogsAvailable": "لا توجد سجلات متاحة"
|
||||
"noLogsAvailable": "لا توجد سجلات متاحة",
|
||||
"selectMode": "حدد",
|
||||
"exitSelectMode": "إلغاء",
|
||||
"selectedCount": "تم اختيار {{count}}",
|
||||
"bulkMonitoring": "الرصد",
|
||||
"bulkFeatures": "الميزات",
|
||||
"bulkMoveFolder": "نقل إلى مجلد",
|
||||
"bulkPin": "تثبيت",
|
||||
"bulkUnpin": "Unpin",
|
||||
"enableAllFeatures": "تمكين جميع الميزات",
|
||||
"disableAllFeatures": "تعطيل جميع الميزات",
|
||||
"enableStatusCheck": "تمكين التحقق من الحالة",
|
||||
"disableStatusCheck": "تعطيل التحقق من الحالة",
|
||||
"enableMetrics": "تمكين القياسات",
|
||||
"disableMetrics": "تعطيل القياسات",
|
||||
"bulkEnableTerminal": "تمكين المحطة الطرفية",
|
||||
"bulkDisableTerminal": "تعطيل المحطة الطرفية",
|
||||
"bulkEnableTunnel": "تمكين النفق",
|
||||
"bulkDisableTunnel": "تعطيل النفق",
|
||||
"bulkEnableFileManager": "تمكين مدير الملفات",
|
||||
"bulkDisableFileManager": "تعطيل مدير الملفات",
|
||||
"bulkEnableDocker": "تمكين Docker",
|
||||
"bulkDisableDocker": "تعطيل المرفأ",
|
||||
"bulkUpdateSuccess": "تم تحديث مضيف {{count}} بنجاح",
|
||||
"bulkUpdateFailed": "فشل التحديث الشامل",
|
||||
"selectAll": "حدد الكل",
|
||||
"deselectAll": "إلغاء تحديد الكل",
|
||||
"useGlobalStatusDefault": "استخدام الافتراضي العام (الحالة)",
|
||||
"useGlobalMetricsDefault": "استخدام الافتراضي العام (القياسات)",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "المحطة",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "تم إغلاق الاتصال",
|
||||
"connectionError": "خطأ في الاتصال: {{message}}",
|
||||
"connected": "متصل",
|
||||
"clipboardWriteFailed": "فشل النسخ إلى الحافظة. تأكد من خدمة الصفحة عبر HTTPS أو localhost.",
|
||||
"clipboardReadFailed": "فشل في القراءة من الحافظة. تأكد من منح أذونات الحافظة.",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "تم إنشاء اتصال SSH",
|
||||
"authError": "فشل المصادقة: {{message}}",
|
||||
"unknownError": "حدث خطأ غير معروف",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "انتهت مهلة التحقق من TOTP. الرجاء إعادة الاتصال.",
|
||||
"passwordTimeout": "انتهت مهلة التحقق من كلمة المرور. الرجاء إعادة الاتصال.",
|
||||
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة.",
|
||||
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال."
|
||||
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال.",
|
||||
"sessionTakenOver": "تم فتح الجلسة في علامة تبويب أخرى. إعادة الاتصال...",
|
||||
"sessionAttachTimeout": "انتهت مهلة مرفق الجلسة. إنشاء اتصال جديد..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "مدير الملفات",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "كتابة",
|
||||
"execute": "تنفيذ",
|
||||
"permissionsChangedSuccessfully": "تم تغيير الأذونات بنجاح",
|
||||
"failedToChangePermissions": "فشل في تغيير الأذونات"
|
||||
"failedToChangePermissions": "فشل في تغيير الأذونات",
|
||||
"name": "الاسم",
|
||||
"sortByName": "الاسم",
|
||||
"sortByDate": "تاريخ التعديل",
|
||||
"sortBySize": "الحجم",
|
||||
"ascending": "تصاعدي",
|
||||
"descending": "تنازلي"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "تسجيل الدخول",
|
||||
"registerButton": "تسجيل",
|
||||
"forgotPassword": "نسيت كلمة المرور؟",
|
||||
"rememberMe": "تذكر لي",
|
||||
"rememberMe": "تذكر الجهاز لمدة 30 يوما (يشمل TOTP)",
|
||||
"noAccount": "ليس لديك حساب؟",
|
||||
"hasAccount": "لديك حساب بالفعل؟",
|
||||
"loginSuccess": "تم تسجيل الدخول بنجاح",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "معالجة...",
|
||||
"pleaseWait": "الرجاء الانتظار...",
|
||||
"registrationDisabled": "تسجيل الحساب الجديد معطل حاليًا من قبل المسؤول. الرجاء تسجيل الدخول أو الاتصال بالمسؤول.",
|
||||
"userNotAllowed": "حسابك غير مصرح له بالتسجيل. الرجاء الاتصال بالمسؤول.",
|
||||
"databaseConnected": "تم توصيل قاعدة البيانات بنجاح",
|
||||
"databaseConnectionFailed": "فشل الاتصال بخادم قاعدة البيانات",
|
||||
"checkServerConnection": "الرجاء التحقق من اتصال الخادم الخاص بك وحاول مرة أخرى",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "حدد سمة اللون للتطبيق",
|
||||
"terminalSyntaxHighlightingDesc": "تسليط الضوء تلقائياً على الأوامر والمسارات وIP ومستويات السجل في المخرجات الطرفية",
|
||||
"enableCommandPaletteShortcut": "تمكين اختصار قفل لوحة الأوامر",
|
||||
"enableCommandPaletteShortcutDesc": "انقر نقرًا مزدوجًا على نوبات اليسار لفتح لوحة القيادة للوصول السريع إلى المضيفين"
|
||||
"enableCommandPaletteShortcutDesc": "انقر نقرًا مزدوجًا على نوبات اليسار لفتح لوحة القيادة للوصول السريع إلى المضيفين",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "الحفاظ على اتصالات SSH عند تبديل علامات التبويب أو إغلاق المتصفح (قد يكون غير مستقر)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "فشل تحميل معلومات الإصدار"
|
||||
@@ -2114,6 +2309,7 @@
|
||||
"usernameField": "اسم",
|
||||
"scopes": "ملف البريد الإلكتروني المفتوح",
|
||||
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
|
||||
"allowedUsers": "user@example.com, @company.com",
|
||||
"enterUsername": "أدخل اسم المستخدم لجعل المشرف",
|
||||
"searchHosts": "البحث عن المضيفين حسب الاسم، اسم المستخدم، IP ، المجلد، العلامات...",
|
||||
"enterPassword": "أدخل كلمة المرور",
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "2-посочно",
|
||||
"threeSplit": "3-посочен",
|
||||
"fourSplit": "4-посочен",
|
||||
"fiveSplit": "5-посочен",
|
||||
"sixSplit": "6-посочен",
|
||||
"availableTabs": "Налични раздели",
|
||||
"dragTabsHint": "Плъзнете раздели към клетките на оформлението по-долу, за да ги присвоите",
|
||||
"layout": "Разделен екран",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "Предупреждение",
|
||||
"notValidatedWarning": "URL адресът не е валидиран - уверете се, че е правилен",
|
||||
"changeServer": "Промяна на сървъра",
|
||||
"mustIncludeProtocol": "URL адресът на сървъра трябва да започва с http:// или https://"
|
||||
"mustIncludeProtocol": "URL адресът на сървъра трябва да започва с http:// или https://",
|
||||
"useEmbedded": "Използване на локален сървър",
|
||||
"embeddedDesc": "Стартирайте Termix с вградения локален сървър (не е необходим отдалечен сървър)",
|
||||
"embeddedConnecting": "Свързване с локален сървър...",
|
||||
"embeddedNotReady": "Локалният сървър все още не е готов. Моля, изчакайте малко и опитайте отново."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "Грешка при проверка на версията",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "Потвърди",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
"or": "ИЛИ",
|
||||
"ok": "Добре",
|
||||
"enabled": "Активирано",
|
||||
"disabled": "За хора с увреждания",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "Потребител {{username}} е изтрит успешно",
|
||||
"failedToDeleteUser": "Изтриването на потребителя не бе успешно",
|
||||
"overrideUserInfoUrl": "Замяна на URL адреса с информация за потребителя (не е задължително)",
|
||||
"allowedUsers": "Разрешени потребители",
|
||||
"allowedUsersDescription": "Списък с разрешени имейл шаблони, разделени със запетаи. Използвайте точни имейли (user@example.com), суфикси на домейн (@example.com) или оставете празно, за да разрешите всички потребители.",
|
||||
"failedToFetchSessions": "Извличането на сесиите не бе успешно",
|
||||
"sessionRevokedSuccessfully": "Сесията е успешно отменена",
|
||||
"failedToRevokeSession": "Неуспешно отменяне на сесията",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "КРИТИЧНО: Входът с парола е деактивиран. Ако нулирате или конфигурирате неправилно OIDC, ще загубите целия достъп до Termix и ще блокирате вашия екземпляр. Продължете само ако сте абсолютно сигурни.",
|
||||
"confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: На път сте да деактивирате OIDC, докато входът с парола също е деактивиран. Това ще блокира вашия Termix екземпляр и ще загубите всякакъв достъп. Абсолютно ли сте сигурни, че искате да продължите?",
|
||||
"failedToUpdatePasswordLoginStatus": "Актуализирането на състоянието на вход с парола не бе успешно",
|
||||
"monitoringDefaults": "Мониторинг на настройки по подразбиране",
|
||||
"monitoringDefaultsDesc": "Глобални интервали по подразбиране за проверки на състоянието и събиране на показатели. Настройките за всеки хост отменят тези стойности.",
|
||||
"globalStatusCheckInterval": "Интервал за проверка на състоянието по подразбиране",
|
||||
"globalMetricsInterval": "Интервал за събиране на показатели по подразбиране",
|
||||
"globalSettingsSaved": "Глобалните настройки за наблюдение са запазени",
|
||||
"failedToSaveGlobalSettings": "Запазването на настройките за глобално наблюдение не бе успешно",
|
||||
"failedToLoadGlobalSettings": "Зареждането на настройките за глобално наблюдение не бе успешно",
|
||||
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
|
||||
"guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.",
|
||||
"enableGuacamole": "Enable RDP/VNC/Telnet support",
|
||||
"guacdUrl": "guacd URL (host:port)",
|
||||
"guacdUrlPlaceholder": "guacd:4822",
|
||||
"guacdUrlNote": "Changes to the guacd host/port require a server restart to take effect.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "was adjusted to valid range",
|
||||
"loadingSessions": "Зареждане на сесиите...",
|
||||
"noActiveSessions": "Няма намерени активни сесии.",
|
||||
"device": "Устройство",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "Не са намерени хостове във JSON файла",
|
||||
"maxHostsAllowed": "Максимално 100 хоста са разрешени на импортиране",
|
||||
"importCompleted": "Импортирането завършено: {{success}} успешно, {{failed}} неуспешно",
|
||||
"importCreated": "създаден",
|
||||
"importUpdated": "актуализиран",
|
||||
"importFailedCount": "неуспешен",
|
||||
"importSkipExisting": "Импортиране (пропускане на съществуващото)",
|
||||
"importOverwriteExisting": "Импортиране (презаписване на съществуващо)",
|
||||
"importFailed": "Импортирането не бе успешно",
|
||||
"importError": "Грешка при импортиране",
|
||||
"failedToImportJson": "Импортирането на JSON файл не бе успешно",
|
||||
"connectionDetails": "Детайли за връзката",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "Организация",
|
||||
"ipAddress": "IP адрес или име на хост",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "Порт",
|
||||
"name": "Име",
|
||||
"username": "Потребителско име",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "Папка",
|
||||
"tags": "Етикети",
|
||||
"pin": "Закачи",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "Тунел",
|
||||
"fileManager": "Файлов мениджър",
|
||||
"serverStats": "Статистика на сървъра",
|
||||
"status": "Status",
|
||||
"statistics": "Статистика",
|
||||
"hostViewer": "Преглед на хоста",
|
||||
"enableServerStats": "Активиране на статистиката на сървъра",
|
||||
"enableServerStatsDesc": "Активиране/деактивиране на събирането на статистика за сървъра за този хост",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "Отворете файловия мениджър",
|
||||
"openTunnels": "Отворени тунели",
|
||||
"openServerDetails": "Отворете подробности за сървъра",
|
||||
"statistics": "Статистика",
|
||||
"enabledWidgets": "Активирани джаджи",
|
||||
"openServerStats": "Отворете статистиката на сървъра",
|
||||
"enabledWidgetsDesc": "Изберете кои статистически уиджети да се показват за този хост",
|
||||
@@ -1017,10 +1059,18 @@
|
||||
"statusCheckEnabledDesc": "Проверете дали сървърът е онлайн или офлайн",
|
||||
"statusCheckInterval": "Интервал за проверка на състоянието",
|
||||
"statusCheckIntervalDesc": "Колко често да проверявам дали хостът е онлайн (5 секунди - 1 час)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "Използване на глобално по подразбиране",
|
||||
"useGlobalMetricsInterval": "Използване на глобално по подразбиране",
|
||||
"usingGlobalDefault": "Използване на глобалното по подразбиране ({{value}}s)",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "Активиране на мониторинг на показатели",
|
||||
"metricsEnabledDesc": "Събиране на статистически данни за процесора, RAM, диска и други системни данни",
|
||||
"metricsInterval": "Интервал на събиране на показатели",
|
||||
"metricsIntervalDesc": "Колко често да се събира статистика за сървъра (5 секунди - 1 час)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "секунди",
|
||||
"intervalMinutes": "минути",
|
||||
"intervalValidation": "Интервалите за наблюдение трябва да бъдат между 5 секунди и 1 час (3600 секунди)",
|
||||
@@ -1174,8 +1224,20 @@
|
||||
"sudoPasswordAutoFillDesc": "Автоматично предлагане за въвеждане на SSH парола, когато sudo поиска парола",
|
||||
"sudoPassword": "Парола за Sudo",
|
||||
"sudoPasswordDesc": "Незадължителна парола за sudo команди (полезно при удостоверяване с ключ)",
|
||||
"keepaliveSettings": "SSH поддържане на активност",
|
||||
"keepaliveSettingsDesc": "Конфигурирайте поведението за keepalive на ниво SSH. Задайте интервал на 0, за да деактивирате SSH keepalive (полезно за MikroTik и други устройства, които игнорират заявките за keepalive).",
|
||||
"keepaliveInterval": "Интервал на поддържане на активността (ms)",
|
||||
"keepaliveIntervalDesc": "Колко често да се изпращат SSH keepalive ping-ове. По подразбиране: 30000 (30 секунди). Задайте на 0, за да деактивирате.",
|
||||
"keepaliveCountMax": "Максимални неуспехи при поддържане на активността",
|
||||
"keepaliveCountMaxDesc": "Прекъсване след толкова много неотговорени keepalive съобщения. По подразбиране: 3.",
|
||||
"socks4": "ЧОРАПИ4",
|
||||
"socks5": "ЧОРАПИ5",
|
||||
"httpConnect": "HTTP ВРЪЗКА",
|
||||
"testProxy": "Тест на връзката",
|
||||
"testingProxy": "Тестване...",
|
||||
"proxyTestSuccess": "Прокси връзката е успешна ({{latency}}мс)",
|
||||
"proxyTestFailed": "Тестът на прокси сървъра не бе успешен: {{error}}",
|
||||
"connectionPath": "Път на свързване",
|
||||
"executeSnippetOnConnect": "Изпълнете фрагмент, когато терминалът се свърже",
|
||||
"autoMosh": "Auto-MOSH",
|
||||
"autoMoshDesc": "Автоматично изпълнение на MOSH команда при свързване",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "Копиране на URL адреса на тунела",
|
||||
"copyServerStatsUrl": "Копиране на URL адреса за статистика на сървъра",
|
||||
"copyDockerUrl": "Копиране на URL адреса на Docker",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "Копирайте URL адреса, за да отворите това приложение в режим на цял екран",
|
||||
"notEnabled": "Docker не е активиран за този хост. Активирайте го в настройките на хоста, за да използвате функциите на Docker.",
|
||||
"validating": "Валидиране на Docker...",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "Показване на времеви отпечатъци",
|
||||
"autoRefresh": "Автоматично опресняване",
|
||||
"filterLogsPlaceholder": "Филтриране на лог файлове...",
|
||||
"noLogsAvailable": "Няма налични лог файлове"
|
||||
"noLogsAvailable": "Няма налични лог файлове",
|
||||
"selectMode": "Изберете",
|
||||
"exitSelectMode": "Отказ",
|
||||
"selectedCount": "{{count}} избрано",
|
||||
"bulkMonitoring": "Мониторинг",
|
||||
"bulkFeatures": "Характеристики",
|
||||
"bulkMoveFolder": "Преместване в папка",
|
||||
"bulkPin": "Закачи",
|
||||
"bulkUnpin": "Откачи",
|
||||
"enableAllFeatures": "Активиране на всички функции",
|
||||
"disableAllFeatures": "Деактивиране на всички функции",
|
||||
"enableStatusCheck": "Активиране на проверка на състоянието",
|
||||
"disableStatusCheck": "Деактивиране на проверката на състоянието",
|
||||
"enableMetrics": "Активиране на показатели",
|
||||
"disableMetrics": "Деактивиране на показателите",
|
||||
"bulkEnableTerminal": "Активиране на терминала",
|
||||
"bulkDisableTerminal": "Деактивиране на терминала",
|
||||
"bulkEnableTunnel": "Активиране на тунел",
|
||||
"bulkDisableTunnel": "Деактивиране на тунела",
|
||||
"bulkEnableFileManager": "Активиране на файловия мениджър",
|
||||
"bulkDisableFileManager": "Деактивиране на файловия мениджър",
|
||||
"bulkEnableDocker": "Активиране на Докер",
|
||||
"bulkDisableDocker": "Деактивиране на Докер",
|
||||
"bulkUpdateSuccess": "{{count}} хост(ове) са актуализирани успешно",
|
||||
"bulkUpdateFailed": "Груповата актуализация не бе успешна",
|
||||
"selectAll": "Избери всички",
|
||||
"deselectAll": "Премахване на избора от всички",
|
||||
"useGlobalStatusDefault": "Използване на глобално по подразбиране (състояние)",
|
||||
"useGlobalMetricsDefault": "Използване на глобални настройки по подразбиране (метрики)",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Терминал",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "Връзката е затворена",
|
||||
"connectionError": "Грешка при свързване: {{message}}",
|
||||
"connected": "Свързан",
|
||||
"clipboardWriteFailed": "Копирането в клипборда не бе успешно. Уверете се, че страницата се предоставя през HTTPS или localhost.",
|
||||
"clipboardReadFailed": "Четенето от клипборда не бе успешно. Уверете се, че са предоставени разрешения за достъп до клипборда.",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "SSH връзка е установена",
|
||||
"authError": "Удостоверяването не бе успешно: {{message}}",
|
||||
"unknownError": "Възникна неизвестна грешка",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "Времето за изчакване за проверка на TOTP изтече. Моля, свържете се отново.",
|
||||
"passwordTimeout": "Времето за изчакване за потвърждение на паролата изтече. Моля, свържете се отново.",
|
||||
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.",
|
||||
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена."
|
||||
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена.",
|
||||
"sessionTakenOver": "Сесията беше отворена в друг раздел. Повторно свързване...",
|
||||
"sessionAttachTimeout": "Времето за изчакване на прикачения файл към сесията изтече. Създаване на нова връзка..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Файлов мениджър",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "Пишете",
|
||||
"execute": "Изпълнение",
|
||||
"permissionsChangedSuccessfully": "Разрешенията са променени успешно",
|
||||
"failedToChangePermissions": "Промяната на разрешенията не бе успешна"
|
||||
"failedToChangePermissions": "Промяната на разрешенията не бе успешна",
|
||||
"name": "Име",
|
||||
"sortByName": "Име",
|
||||
"sortByDate": "Дата на промяна",
|
||||
"sortBySize": "Размер",
|
||||
"ascending": "Възходящ",
|
||||
"descending": "Низходящо"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "Вход",
|
||||
"registerButton": "Регистрация",
|
||||
"forgotPassword": "Забравена парола?",
|
||||
"rememberMe": "Запомни ме",
|
||||
"rememberMe": "Запомни устройството за 30 дни (включва TOTP)",
|
||||
"noAccount": "Нямате акаунт?",
|
||||
"hasAccount": "Вече имате акаунт?",
|
||||
"loginSuccess": "Влизането е успешно",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "Обработва се...",
|
||||
"pleaseWait": "Моля, изчакайте...",
|
||||
"registrationDisabled": "Регистрацията на нов акаунт в момента е деактивирана от администратор. Моля, влезте или се свържете с администратор.",
|
||||
"userNotAllowed": "Вашият акаунт не е оторизиран за регистрация. Моля, свържете се с администратор.",
|
||||
"databaseConnected": "Базата данни е свързана успешно",
|
||||
"databaseConnectionFailed": "Неуспешно свързване със сървъра на базата данни",
|
||||
"checkServerConnection": "Моля, проверете връзката със сървъра си и опитайте отново",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "Изберете цветовата тема за приложението",
|
||||
"terminalSyntaxHighlightingDesc": "Автоматично маркиране на команди, пътища, IP адреси и нива на лог файлове в изхода на терминала",
|
||||
"enableCommandPaletteShortcut": "Активиране на пряк път за палитра с команди",
|
||||
"enableCommandPaletteShortcutDesc": "Докоснете двукратно левия Shift, за да отворите палитрата с команди за бърз достъп до хостове"
|
||||
"enableCommandPaletteShortcutDesc": "Докоснете двукратно левия Shift, за да отворите палитрата с команди за бърз достъп до хостове",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "Поддържане на SSH връзки при превключване на раздели или затваряне на браузъра (може да е нестабилно)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "Зареждането на информация за версията не бе успешно"
|
||||
@@ -2114,6 +2309,7 @@
|
||||
"usernameField": "име",
|
||||
"scopes": "имейл профил с отворен ID",
|
||||
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
|
||||
"allowedUsers": "потребител@пример.com, @компания.com",
|
||||
"enterUsername": "Въведете потребителско име, за да го направите администратор",
|
||||
"searchHosts": "Търсене на хостове по име, потребителско име, IP адрес, папка, тагове...",
|
||||
"enterPassword": "Въведете паролата си",
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "2-উপায়",
|
||||
"threeSplit": "3-উপায়",
|
||||
"fourSplit": "৪-উপায়",
|
||||
"fiveSplit": "৫-উপায়",
|
||||
"sixSplit": "৬-উপায়",
|
||||
"availableTabs": "উপলব্ধ ট্যাবগুলি",
|
||||
"dragTabsHint": "ট্যাবগুলিকে নিচের লেআউট সেলগুলিতে টেনে আনুন যাতে সেগুলি অ্যাসাইন করা যায়।",
|
||||
"layout": "স্প্লিট স্ক্রিন লেআউট",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "সতর্কতা",
|
||||
"notValidatedWarning": "URL যাচাই করা হয়নি - নিশ্চিত করুন যে এটি সঠিক",
|
||||
"changeServer": "সার্ভার পরিবর্তন করুন",
|
||||
"mustIncludeProtocol": "সার্ভার URL টি http:// অথবা https:// দিয়ে শুরু হতে হবে"
|
||||
"mustIncludeProtocol": "সার্ভার URL টি http:// অথবা https:// দিয়ে শুরু হতে হবে",
|
||||
"useEmbedded": "স্থানীয় সার্ভার ব্যবহার করুন",
|
||||
"embeddedDesc": "অন্তর্নির্মিত স্থানীয় সার্ভার দিয়ে টার্মিক্স চালান (কোনও দূরবর্তী সার্ভারের প্রয়োজন নেই)",
|
||||
"embeddedConnecting": "স্থানীয় সার্ভারের সাথে সংযোগ স্থাপন করা হচ্ছে...",
|
||||
"embeddedNotReady": "স্থানীয় সার্ভার এখনও প্রস্তুত নয়। দয়া করে কিছুক্ষণ অপেক্ষা করুন এবং আবার চেষ্টা করুন।"
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "সংস্করণ পরীক্ষা ত্রুটি",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "নিশ্চিত করুন",
|
||||
"yes": "হাঁ",
|
||||
"no": "না",
|
||||
"or": "অথবা",
|
||||
"ok": "ঠিক আছে",
|
||||
"enabled": "সক্ষম করা হয়েছে",
|
||||
"disabled": "অক্ষম",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "ব্যবহারকারী {{username}} সফলভাবে মুছে ফেলা হয়েছে",
|
||||
"failedToDeleteUser": "ব্যবহারকারী মুছে ফেলা যায়নি",
|
||||
"overrideUserInfoUrl": "ব্যবহারকারীর তথ্য URL ওভাররাইড করুন (প্রয়োজনীয় নয়)",
|
||||
"allowedUsers": "অনুমোদিত ব্যবহারকারীরা",
|
||||
"allowedUsersDescription": "অনুমোদিত ইমেল প্যাটার্নের কমা দ্বারা পৃথক তালিকা। সঠিক ইমেল (user@example.com), ডোমেন প্রত্যয় (@example.com) ব্যবহার করুন, অথবা সমস্ত ব্যবহারকারীকে অনুমতি দিতে খালি রাখুন।",
|
||||
"failedToFetchSessions": "সেশনগুলি আনা যায়নি",
|
||||
"sessionRevokedSuccessfully": "সেশন সফলভাবে প্রত্যাহার করা হয়েছে",
|
||||
"failedToRevokeSession": "সেশন প্রত্যাহার করতে ব্যর্থ হয়েছে",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "গুরুত্বপূর্ণ: পাসওয়ার্ড লগইন অক্ষম করা আছে। আপনি যদি OIDC রিসেট করেন বা ভুলভাবে কনফিগার করেন, তাহলে আপনি Termix-এর সমস্ত অ্যাক্সেস হারাবেন এবং আপনার ইনস্ট্যান্স ব্রিক হয়ে যাবে। আপনি যদি সম্পূর্ণরূপে নিশ্চিত হন তবেই এগিয়ে যান।",
|
||||
"confirmDisableOIDCWarning": "সতর্কতা: আপনি OIDC নিষ্ক্রিয় করতে চলেছেন, যখন পাসওয়ার্ড লগইনও নিষ্ক্রিয় থাকবে। এটি আপনার Termix ইন্সট্যান্সকে ব্রিক করবে এবং আপনি সমস্ত অ্যাক্সেস হারাবেন। আপনি কি নিশ্চিত যে আপনি এগিয়ে যেতে চান?",
|
||||
"failedToUpdatePasswordLoginStatus": "পাসওয়ার্ড লগইন স্ট্যাটাস আপডেট করা যায়নি",
|
||||
"monitoringDefaults": "ডিফল্ট পর্যবেক্ষণ",
|
||||
"monitoringDefaultsDesc": "স্থিতি পরীক্ষা এবং মেট্রিক্স সংগ্রহের জন্য বিশ্বব্যাপী ডিফল্ট ব্যবধান। প্রতি-হোস্ট সেটিংস এই মানগুলিকে ওভাররাইড করে।",
|
||||
"globalStatusCheckInterval": "ডিফল্ট স্থিতি পরীক্ষা ব্যবধান",
|
||||
"globalMetricsInterval": "ডিফল্ট মেট্রিক্স সংগ্রহের ব্যবধান",
|
||||
"globalSettingsSaved": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করা হয়েছে",
|
||||
"failedToSaveGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে",
|
||||
"failedToLoadGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস লোড করা যায়নি",
|
||||
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
|
||||
"guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.",
|
||||
"enableGuacamole": "Enable RDP/VNC/Telnet support",
|
||||
"guacdUrl": "guacd URL (host:port)",
|
||||
"guacdUrlPlaceholder": "guacd:4822",
|
||||
"guacdUrlNote": "Changes to the guacd host/port require a server restart to take effect.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "was adjusted to valid range",
|
||||
"loadingSessions": "সেশন লোড হচ্ছে...",
|
||||
"noActiveSessions": "কোন সক্রিয় সেশন পাওয়া যায়নি।",
|
||||
"device": "যন্ত্র",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "JSON ফাইলে কোনও হোস্ট পাওয়া যায়নি",
|
||||
"maxHostsAllowed": "প্রতি আমদানিতে সর্বোচ্চ ১০০টি হোস্ট অনুমোদিত",
|
||||
"importCompleted": "আমদানি সম্পন্ন হয়েছে: {{success}} সফল, {{failed}} ব্যর্থ",
|
||||
"importCreated": "তৈরি",
|
||||
"importUpdated": "আপডেট করা হয়েছে",
|
||||
"importFailedCount": "ব্যর্থ",
|
||||
"importSkipExisting": "আমদানি করুন (বিদ্যমান এড়িয়ে যান)",
|
||||
"importOverwriteExisting": "আমদানি করুন (বিদ্যমানগুলিকে ওভাররাইট করুন)",
|
||||
"importFailed": "আমদানি ব্যর্থ হয়েছে",
|
||||
"importError": "আমদানি ত্রুটি",
|
||||
"failedToImportJson": "JSON ফাইল আমদানি করা যায়নি",
|
||||
"connectionDetails": "সংযোগের বিবরণ",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "সংগঠন",
|
||||
"ipAddress": "আইপি ঠিকানা অথবা হোস্টনেম",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "বন্দর",
|
||||
"name": "নাম",
|
||||
"username": "ব্যবহারকারীর নাম",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "ফোল্ডার",
|
||||
"tags": "ট্যাগ",
|
||||
"pin": "পিন",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "টানেল",
|
||||
"fileManager": "ফাইল ম্যানেজার",
|
||||
"serverStats": "সার্ভার পরিসংখ্যান",
|
||||
"status": "Status",
|
||||
"statistics": "পরিসংখ্যান",
|
||||
"hostViewer": "হোস্ট ভিউয়ার",
|
||||
"enableServerStats": "সার্ভার পরিসংখ্যান সক্ষম করুন",
|
||||
"enableServerStatsDesc": "এই হোস্টের জন্য সার্ভার পরিসংখ্যান সংগ্রহ সক্ষম/অক্ষম করুন",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "ফাইল ম্যানেজার খুলুন",
|
||||
"openTunnels": "খোলা টানেল",
|
||||
"openServerDetails": "সার্ভারের বিবরণ খুলুন",
|
||||
"statistics": "পরিসংখ্যান",
|
||||
"enabledWidgets": "সক্রিয় উইজেট",
|
||||
"openServerStats": "ওপেন সার্ভার পরিসংখ্যান",
|
||||
"enabledWidgetsDesc": "এই হোস্টের জন্য কোন পরিসংখ্যান উইজেটগুলি প্রদর্শন করা হবে তা নির্বাচন করুন।",
|
||||
@@ -1017,10 +1059,18 @@
|
||||
"statusCheckEnabledDesc": "সার্ভারটি অনলাইন নাকি অফলাইন তা পরীক্ষা করুন।",
|
||||
"statusCheckInterval": "স্থিতি পরীক্ষা ব্যবধান",
|
||||
"statusCheckIntervalDesc": "হোস্ট অনলাইন আছে কিনা তা কতবার পরীক্ষা করতে হবে (৫ সেকেন্ড - ১ ঘন্টা)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
|
||||
"useGlobalMetricsInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
|
||||
"usingGlobalDefault": "গ্লোবাল ডিফল্ট ব্যবহার করে ({{value}}s)",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "মেট্রিক্স মনিটরিং সক্ষম করুন",
|
||||
"metricsEnabledDesc": "CPU, RAM, ডিস্ক এবং অন্যান্য সিস্টেম পরিসংখ্যান সংগ্রহ করুন",
|
||||
"metricsInterval": "মেট্রিক্স সংগ্রহের ব্যবধান",
|
||||
"metricsIntervalDesc": "সার্ভারের পরিসংখ্যান কত ঘন ঘন সংগ্রহ করতে হবে (৫ সেকেন্ড - ১ ঘন্টা)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "সেকেন্ড",
|
||||
"intervalMinutes": "মিনিট",
|
||||
"intervalValidation": "পর্যবেক্ষণের ব্যবধান ৫ সেকেন্ড থেকে ১ ঘন্টা (৩৬০০ সেকেন্ড) এর মধ্যে হতে হবে।",
|
||||
@@ -1174,8 +1224,20 @@
|
||||
"sudoPasswordAutoFillDesc": "sudo পাসওয়ার্ডের জন্য অনুরোধ করলে স্বয়ংক্রিয়ভাবে SSH পাসওয়ার্ড সন্নিবেশ করার প্রস্তাব দেয়",
|
||||
"sudoPassword": "সুডো পাসওয়ার্ড",
|
||||
"sudoPasswordDesc": "sudo কমান্ডের জন্য ঐচ্ছিক পাসওয়ার্ড (কী প্রমাণীকরণের সাথে কার্যকর)",
|
||||
"keepaliveSettings": "SSH Keepalive সম্পর্কে",
|
||||
"keepaliveSettingsDesc": "SSH-স্তরের keepalive আচরণ কনফিগার করুন। SSH keepalives নিষ্ক্রিয় করতে ব্যবধান 0 তে সেট করুন (MikroTik এবং অন্যান্য ডিভাইসের জন্য দরকারী যা keepalive অনুরোধ উপেক্ষা করে)।",
|
||||
"keepaliveInterval": "কিপলাইভ ব্যবধান (মিলিসেকেন্ড)",
|
||||
"keepaliveIntervalDesc": "কত ঘন ঘন SSH keepalive পিং পাঠাতে হবে। ডিফল্ট: 30000 (30s)। অক্ষম করতে 0 তে সেট করুন।",
|
||||
"keepaliveCountMax": "Keepalive সর্বোচ্চ ব্যর্থতা",
|
||||
"keepaliveCountMaxDesc": "এই অনেক উত্তর না দেওয়া কিপলাইভের পরে সংযোগ বিচ্ছিন্ন করুন। ডিফল্ট: 3।",
|
||||
"socks4": "মোজা ৪",
|
||||
"socks5": "মোজা৫",
|
||||
"httpConnect": "HTTP সংযোগ",
|
||||
"testProxy": "সংযোগ পরীক্ষা করুন",
|
||||
"testingProxy": "পরীক্ষা করা হচ্ছে...",
|
||||
"proxyTestSuccess": "প্রক্সি সংযোগ সফল হয়েছে ({{latency}}মিলিসেকেন্ড)",
|
||||
"proxyTestFailed": "প্রক্সি পরীক্ষা ব্যর্থ হয়েছে: {{error}}",
|
||||
"connectionPath": "সংযোগ পথ",
|
||||
"executeSnippetOnConnect": "টার্মিনাল সংযোগ করলে একটি স্নিপেট কার্যকর করুন",
|
||||
"autoMosh": "অটো-মোশ",
|
||||
"autoMoshDesc": "কানেক্টে স্বয়ংক্রিয়ভাবে MOSH কমান্ড চালান",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "টানেলের URL কপি করুন",
|
||||
"copyServerStatsUrl": "সার্ভার পরিসংখ্যান URL কপি করুন",
|
||||
"copyDockerUrl": "ডকার ইউআরএল কপি করুন",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "এই অ্যাপটি পূর্ণ-স্ক্রিন মোডে খুলতে URL কপি করুন",
|
||||
"notEnabled": "এই হোস্টের জন্য ডকার সক্ষম করা নেই। ডকার বৈশিষ্ট্যগুলি ব্যবহার করতে হোস্ট সেটিংসে এটি সক্ষম করুন।",
|
||||
"validating": "ডকার যাচাই করা হচ্ছে...",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "টাইমস্ট্যাম্প দেখান",
|
||||
"autoRefresh": "অটো রিফ্রেশ",
|
||||
"filterLogsPlaceholder": "ফিল্টার লগ...",
|
||||
"noLogsAvailable": "কোনও লগ উপলব্ধ নেই"
|
||||
"noLogsAvailable": "কোনও লগ উপলব্ধ নেই",
|
||||
"selectMode": "নির্বাচন করুন",
|
||||
"exitSelectMode": "বাতিল করুন",
|
||||
"selectedCount": "{{count}} নির্বাচিত",
|
||||
"bulkMonitoring": "পর্যবেক্ষণ",
|
||||
"bulkFeatures": "ফিচার",
|
||||
"bulkMoveFolder": "ফোল্ডারে সরান",
|
||||
"bulkPin": "পিন",
|
||||
"bulkUnpin": "আনপিন করুন",
|
||||
"enableAllFeatures": "সকল বৈশিষ্ট্য সক্ষম করুন",
|
||||
"disableAllFeatures": "সমস্ত বৈশিষ্ট্য অক্ষম করুন",
|
||||
"enableStatusCheck": "স্থিতি পরীক্ষা সক্ষম করুন",
|
||||
"disableStatusCheck": "স্থিতি পরীক্ষা অক্ষম করুন",
|
||||
"enableMetrics": "মেট্রিক্স সক্ষম করুন",
|
||||
"disableMetrics": "মেট্রিক্স অক্ষম করুন",
|
||||
"bulkEnableTerminal": "টার্মিনাল সক্ষম করুন",
|
||||
"bulkDisableTerminal": "টার্মিনাল অক্ষম করুন",
|
||||
"bulkEnableTunnel": "টানেল সক্ষম করুন",
|
||||
"bulkDisableTunnel": "টানেল অক্ষম করুন",
|
||||
"bulkEnableFileManager": "ফাইল ম্যানেজার সক্ষম করুন",
|
||||
"bulkDisableFileManager": "ফাইল ম্যানেজার অক্ষম করুন",
|
||||
"bulkEnableDocker": "ডকার সক্ষম করুন",
|
||||
"bulkDisableDocker": "ডকার অক্ষম করুন",
|
||||
"bulkUpdateSuccess": "{{count}} হোস্ট(গুলি) সফলভাবে আপডেট করা হয়েছে",
|
||||
"bulkUpdateFailed": "বাল্ক আপডেট ব্যর্থ হয়েছে",
|
||||
"selectAll": "সবগুলো নির্বাচন করুন",
|
||||
"deselectAll": "সবগুলো অনির্বাচন করুন",
|
||||
"useGlobalStatusDefault": "গ্লোবাল ডিফল্ট (স্থিতি) ব্যবহার করুন",
|
||||
"useGlobalMetricsDefault": "গ্লোবাল ডিফল্ট (মেট্রিক্স) ব্যবহার করুন",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "টার্মিনাল",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "সংযোগ বন্ধ আছে",
|
||||
"connectionError": "সংযোগ ত্রুটি: {{message}}",
|
||||
"connected": "সংযুক্ত",
|
||||
"clipboardWriteFailed": "ক্লিপবোর্ডে কপি করা যায়নি। নিশ্চিত করুন যে পৃষ্ঠাটি HTTPS অথবা localhost এর মাধ্যমে পরিবেশিত হচ্ছে।",
|
||||
"clipboardReadFailed": "ক্লিপবোর্ড থেকে পড়া যায়নি। ক্লিপবোর্ডের অনুমতি মঞ্জুর করা হয়েছে কিনা তা নিশ্চিত করুন।",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "SSH সংযোগ স্থাপন করা হয়েছে",
|
||||
"authError": "প্রমাণীকরণ ব্যর্থ হয়েছে: {{message}}",
|
||||
"unknownError": "অজানা ত্রুটি ঘটেছে",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "TOTP যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
|
||||
"passwordTimeout": "পাসওয়ার্ড যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
|
||||
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।",
|
||||
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।"
|
||||
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।",
|
||||
"sessionTakenOver": "অন্য ট্যাবে সেশন খোলা হয়েছে। পুনরায় সংযোগ করা হচ্ছে...",
|
||||
"sessionAttachTimeout": "সেশন সংযুক্তির সময় শেষ। নতুন সংযোগ তৈরি করা হচ্ছে..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "ফাইল ম্যানেজার",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "লিখুন",
|
||||
"execute": "সম্পাদন করুন",
|
||||
"permissionsChangedSuccessfully": "অনুমতিগুলি সফলভাবে পরিবর্তন করা হয়েছে",
|
||||
"failedToChangePermissions": "অনুমতি পরিবর্তন করা যায়নি"
|
||||
"failedToChangePermissions": "অনুমতি পরিবর্তন করা যায়নি",
|
||||
"name": "নাম",
|
||||
"sortByName": "নাম",
|
||||
"sortByDate": "তারিখ পরিবর্তন করা হয়েছে",
|
||||
"sortBySize": "আকার",
|
||||
"ascending": "আরোহী",
|
||||
"descending": "অবরোহী"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "লগইন",
|
||||
"registerButton": "নিবন্ধন",
|
||||
"forgotPassword": "পাসওয়ার্ড ভুলে গেছেন?",
|
||||
"rememberMe": "আমাকে মনে রেখো",
|
||||
"rememberMe": "ডিভাইসটি ৩০ দিনের জন্য মনে রাখবেন (TOTP সহ)",
|
||||
"noAccount": "কোন অ্যাকাউন্ট নেই?",
|
||||
"hasAccount": "ইতিমধ্যে একটি অ্যাকাউন্ট আছে?",
|
||||
"loginSuccess": "লগইন সফল হয়েছে",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "প্রক্রিয়াজাতকরণ...",
|
||||
"pleaseWait": "অনুগ্রহ করে অপেক্ষা করুন...",
|
||||
"registrationDisabled": "একজন প্রশাসক বর্তমানে নতুন অ্যাকাউন্ট নিবন্ধন বন্ধ করে রেখেছেন। অনুগ্রহ করে লগ ইন করুন অথবা একজন প্রশাসকের সাথে যোগাযোগ করুন।",
|
||||
"userNotAllowed": "আপনার অ্যাকাউন্টটি নিবন্ধনের জন্য অনুমোদিত নয়। অনুগ্রহ করে একজন প্রশাসকের সাথে যোগাযোগ করুন।",
|
||||
"databaseConnected": "ডাটাবেস সফলভাবে সংযুক্ত হয়েছে",
|
||||
"databaseConnectionFailed": "ডাটাবেস সার্ভারের সাথে সংযোগ করতে ব্যর্থ হয়েছে।",
|
||||
"checkServerConnection": "আপনার সার্ভার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন।",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "অ্যাপ্লিকেশনের জন্য রঙের থিম নির্বাচন করুন",
|
||||
"terminalSyntaxHighlightingDesc": "টার্মিনাল আউটপুটে স্বয়ংক্রিয়ভাবে কমান্ড, পাথ, আইপি এবং লগ লেভেল হাইলাইট করুন",
|
||||
"enableCommandPaletteShortcut": "কমান্ড প্যালেট শর্টকাট সক্ষম করুন",
|
||||
"enableCommandPaletteShortcutDesc": "হোস্টগুলিতে দ্রুত অ্যাক্সেসের জন্য কমান্ড প্যালেট খুলতে বাম দিকে Shift-এ দুবার ট্যাপ করুন।"
|
||||
"enableCommandPaletteShortcutDesc": "হোস্টগুলিতে দ্রুত অ্যাক্সেসের জন্য কমান্ড প্যালেট খুলতে বাম দিকে Shift-এ দুবার ট্যাপ করুন।",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "ট্যাব স্যুইচ করার সময় বা ব্রাউজার বন্ধ করার সময় SSH সংযোগ বজায় রাখুন (অস্থির হতে পারে)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "সংস্করণের তথ্য লোড করা যায়নি"
|
||||
@@ -2114,6 +2309,7 @@
|
||||
"usernameField": "নাম",
|
||||
"scopes": "ওপেনআইডি ইমেল প্রোফাইল",
|
||||
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
|
||||
"allowedUsers": "user@example.com, @company.com",
|
||||
"enterUsername": "অ্যাডমিন করতে ব্যবহারকারীর নাম লিখুন",
|
||||
"searchHosts": "নাম, ব্যবহারকারীর নাম, আইপি, ফোল্ডার, ট্যাগ দ্বারা হোস্ট অনুসন্ধান করুন...",
|
||||
"enterPassword": "আপনার পাসওয়ার্ড লিখুন",
|
||||
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "Bidireccional",
|
||||
"threeSplit": "3 vies",
|
||||
"fourSplit": "4 vies",
|
||||
"fiveSplit": "5 vies",
|
||||
"sixSplit": "6 vies",
|
||||
"availableTabs": "Pestanyes disponibles",
|
||||
"dragTabsHint": "Arrossegueu pestanyes a les cel·les de disseny següents per assignar-les",
|
||||
"layout": "Disseny de pantalla dividida",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "Avís",
|
||||
"notValidatedWarning": "URL no validada: assegureu-vos que sigui correcta",
|
||||
"changeServer": "Canviar servidor",
|
||||
"mustIncludeProtocol": "L'URL del servidor ha de començar per http:// o https://"
|
||||
"mustIncludeProtocol": "L'URL del servidor ha de començar per http:// o https://",
|
||||
"useEmbedded": "Utilitza el servidor local",
|
||||
"embeddedDesc": "Executeu Termix amb el servidor local integrat (no cal cap servidor remot)",
|
||||
"embeddedConnecting": "Connectant al servidor local...",
|
||||
"embeddedNotReady": "El servidor local encara no està a punt. Espereu un moment i torneu-ho a intentar."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "Error de comprovació de versió",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "Confirma",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"or": "O",
|
||||
"ok": "D'acord",
|
||||
"enabled": "Activat",
|
||||
"disabled": "Discapacitat",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "L'usuari {{username}} s'ha eliminat correctament",
|
||||
"failedToDeleteUser": "No s'ha pogut suprimir l'usuari",
|
||||
"overrideUserInfoUrl": "Substitueix l'URL d'informació de l'usuari (no obligatori)",
|
||||
"allowedUsers": "Usuaris permesos",
|
||||
"allowedUsersDescription": "Llista de patrons de correu electrònic permesos separats per comes. Utilitzeu correus electrònics exactes (usuari@exemple.com), sufixos de domini (@exemple.com) o deixeu-ho en blanc per permetre tots els usuaris.",
|
||||
"failedToFetchSessions": "No s'han pogut recuperar les sessions",
|
||||
"sessionRevokedSuccessfully": "La sessió s'ha revocat correctament",
|
||||
"failedToRevokeSession": "No s'ha pogut revocar la sessió",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "CRÍTIC: L'inici de sessió amb contrasenya està desactivat. Si restableixes o configures incorrectament l'OIDC, perdràs tot l'accés a Termix i bloquejaràs la teva instància. Només continua si n'estàs absolutament segur.",
|
||||
"confirmDisableOIDCWarning": "AVÍS: Esteu a punt de desactivar OIDC mentre l'inici de sessió amb contrasenya també està desactivat. Això bloquejarà la vostra instància de Termix i perdreu tot l'accés. Esteu completament segur que voleu continuar?",
|
||||
"failedToUpdatePasswordLoginStatus": "No s'ha pogut actualitzar l'estat d'inici de sessió de la contrasenya",
|
||||
"monitoringDefaults": "Monitorització dels valors predeterminats",
|
||||
"monitoringDefaultsDesc": "Intervals globals predeterminats per a les comprovacions d'estat i la recopilació de mètriques. La configuració per host substitueix aquests valors.",
|
||||
"globalStatusCheckInterval": "Interval de comprovació d'estat per defecte",
|
||||
"globalMetricsInterval": "Interval de recopilació de mètriques per defecte",
|
||||
"globalSettingsSaved": "S'ha desat la configuració de monitorització global",
|
||||
"failedToSaveGlobalSettings": "No s'ha pogut desar la configuració de monitorització global",
|
||||
"failedToLoadGlobalSettings": "No s'ha pogut carregar la configuració de monitorització global",
|
||||
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
|
||||
"guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.",
|
||||
"enableGuacamole": "Enable RDP/VNC/Telnet support",
|
||||
"guacdUrl": "guacd URL (host:port)",
|
||||
"guacdUrlPlaceholder": "guacd:4822",
|
||||
"guacdUrlNote": "Changes to the guacd host/port require a server restart to take effect.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "was adjusted to valid range",
|
||||
"loadingSessions": "S'estan carregant les sessions...",
|
||||
"noActiveSessions": "No s'han trobat sessions actives.",
|
||||
"device": "Dispositiu",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "No s'han trobat hosts al fitxer JSON",
|
||||
"maxHostsAllowed": "Màxim de 100 amfitrions permesos per importació",
|
||||
"importCompleted": "Importació completada: {{success}} correcta, {{failed}} fallida",
|
||||
"importCreated": "creat",
|
||||
"importUpdated": "actualitzat",
|
||||
"importFailedCount": "fallat",
|
||||
"importSkipExisting": "Importa (omet els existents)",
|
||||
"importOverwriteExisting": "Importa (sobreescriu l'existent)",
|
||||
"importFailed": "La importació ha fallat",
|
||||
"importError": "Error d'importació",
|
||||
"failedToImportJson": "No s'ha pogut importar el fitxer JSON",
|
||||
"connectionDetails": "Detalls de la connexió",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "Organització",
|
||||
"ipAddress": "Adreça IP o nom d'amfitrió",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "Port",
|
||||
"name": "Nom",
|
||||
"username": "Nom d'usuari",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "Carpeta",
|
||||
"tags": "Etiquetes",
|
||||
"pin": "Fixar",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "Túnel",
|
||||
"fileManager": "Gestor de fitxers",
|
||||
"serverStats": "Estadístiques del servidor",
|
||||
"status": "Status",
|
||||
"statistics": "Estadístiques",
|
||||
"hostViewer": "Visualitzador de l'amfitrió",
|
||||
"enableServerStats": "Habilita les estadístiques del servidor",
|
||||
"enableServerStatsDesc": "Activa/desactiva la recopilació d'estadístiques del servidor per a aquest amfitrió",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "Obre el gestor de fitxers",
|
||||
"openTunnels": "Túnels oberts",
|
||||
"openServerDetails": "Obre els detalls del servidor",
|
||||
"statistics": "Estadístiques",
|
||||
"enabledWidgets": "Widgets activats",
|
||||
"openServerStats": "Estadístiques del servidor obert",
|
||||
"enabledWidgetsDesc": "Seleccioneu els widgets d'estadístiques que voleu mostrar per a aquest amfitrió",
|
||||
@@ -1017,10 +1059,18 @@
|
||||
"statusCheckEnabledDesc": "Comprova si el servidor està en línia o fora de línia",
|
||||
"statusCheckInterval": "Interval de comprovació d'estat",
|
||||
"statusCheckIntervalDesc": "Amb quina freqüència cal comprovar si l'amfitrió està en línia (5 segons - 1 hora)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "Utilitza el valor predeterminat global",
|
||||
"useGlobalMetricsInterval": "Utilitza el valor predeterminat global",
|
||||
"usingGlobalDefault": "Utilitzant el valor per defecte global ({{value}}s)",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "Activa la supervisió de mètriques",
|
||||
"metricsEnabledDesc": "Recopila estadístiques de CPU, RAM, disc i altres estadístiques del sistema",
|
||||
"metricsInterval": "Interval de recopilació de mètriques",
|
||||
"metricsIntervalDesc": "Amb quina freqüència s'han de recopilar estadístiques del servidor (5 segons - 1 hora)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "segons",
|
||||
"intervalMinutes": "minuts",
|
||||
"intervalValidation": "Els intervals de monitorització han d'estar entre 5 segons i 1 hora (3600 segons)",
|
||||
@@ -1174,8 +1224,20 @@
|
||||
"sudoPasswordAutoFillDesc": "Ofereix automàticament la inserció d'una contrasenya SSH quan sudo demani la contrasenya",
|
||||
"sudoPassword": "Contrasenya de Sudo",
|
||||
"sudoPasswordDesc": "Contrasenya opcional per a les ordres sudo (útil amb l'autenticació de clau)",
|
||||
"keepaliveSettings": "SSH Keeping Alive",
|
||||
"keepaliveSettingsDesc": "Configura el comportament de keepalive a nivell SSH. Estableix l'interval a 0 per desactivar els keepalive SSH (útil per a MikroTik i altres dispositius que ignoren les sol·licituds de keepalive).",
|
||||
"keepaliveInterval": "Interval de manteniment (ms)",
|
||||
"keepaliveIntervalDesc": "Freqüència d'enviament de pings de keepalive SSH. Per defecte: 30000 (30 s). Establiu-ho a 0 per desactivar-ho.",
|
||||
"keepaliveCountMax": "Errors màxims de KeepAlive",
|
||||
"keepaliveCountMaxDesc": "Desconnecta després d'aquest nombre de keepalives sense resposta. Per defecte: 3.",
|
||||
"socks4": "MITJONS4",
|
||||
"socks5": "MITJONS5",
|
||||
"httpConnect": "CONNEXIÓ HTTP",
|
||||
"testProxy": "Prova de connexió",
|
||||
"testingProxy": "Proves...",
|
||||
"proxyTestSuccess": "Connexió de proxy correcta ({{latency}}ms)",
|
||||
"proxyTestFailed": "La prova del servidor intermediari ha fallat: {{error}}",
|
||||
"connectionPath": "Ruta de connexió",
|
||||
"executeSnippetOnConnect": "Executa un fragment quan el terminal es connecti",
|
||||
"autoMosh": "Auto-MOSH",
|
||||
"autoMoshDesc": "Executa automàticament l'ordre MOSH en connectar-se",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "Copia l'URL del túnel",
|
||||
"copyServerStatsUrl": "Copia l'URL de les estadístiques del servidor",
|
||||
"copyDockerUrl": "Copia l'URL de Docker",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "Copia l'URL per obrir aquesta aplicació en mode de pantalla completa",
|
||||
"notEnabled": "Docker no està habilitat per a aquest amfitrió. Activeu-lo a la configuració de l'amfitrió per utilitzar les funcions de Docker.",
|
||||
"validating": "Validant Docker...",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "Mostra les marques de temps",
|
||||
"autoRefresh": "Actualització automàtica",
|
||||
"filterLogsPlaceholder": "Filtra els registres...",
|
||||
"noLogsAvailable": "No hi ha registres disponibles"
|
||||
"noLogsAvailable": "No hi ha registres disponibles",
|
||||
"selectMode": "Selecciona",
|
||||
"exitSelectMode": "Cancel·la",
|
||||
"selectedCount": "{{count}} seleccionat",
|
||||
"bulkMonitoring": "Monitorització",
|
||||
"bulkFeatures": "Característiques",
|
||||
"bulkMoveFolder": "Mou a la carpeta",
|
||||
"bulkPin": "Fixar",
|
||||
"bulkUnpin": "Desfixar",
|
||||
"enableAllFeatures": "Activa totes les funcions",
|
||||
"disableAllFeatures": "Desactiva totes les funcions",
|
||||
"enableStatusCheck": "Activa la comprovació d'estat",
|
||||
"disableStatusCheck": "Desactiva la comprovació d'estat",
|
||||
"enableMetrics": "Activa les mètriques",
|
||||
"disableMetrics": "Desactiva les mètriques",
|
||||
"bulkEnableTerminal": "Habilita el terminal",
|
||||
"bulkDisableTerminal": "Desactiva el terminal",
|
||||
"bulkEnableTunnel": "Habilita el túnel",
|
||||
"bulkDisableTunnel": "Desactiva el túnel",
|
||||
"bulkEnableFileManager": "Habilita el gestor de fitxers",
|
||||
"bulkDisableFileManager": "Desactiva el gestor de fitxers",
|
||||
"bulkEnableDocker": "Activa Docker",
|
||||
"bulkDisableDocker": "Desactiva Docker",
|
||||
"bulkUpdateSuccess": "S'han actualitzat correctament els hosts {{count}}",
|
||||
"bulkUpdateFailed": "L'actualització massiva ha fallat",
|
||||
"selectAll": "Selecciona-ho tot",
|
||||
"deselectAll": "Desselecciona-ho tot",
|
||||
"useGlobalStatusDefault": "Utilitza el valor per defecte global (estat)",
|
||||
"useGlobalMetricsDefault": "Utilitza el valor predeterminat global (mètriques)",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "Connexió tancada",
|
||||
"connectionError": "Error de connexió: {{message}}",
|
||||
"connected": "Connectat",
|
||||
"clipboardWriteFailed": "No s'ha pogut copiar al porta-retalls. Assegureu-vos que la pàgina es serveix per HTTPS o localhost.",
|
||||
"clipboardReadFailed": "No s'ha pogut llegir del porta-retalls. Assegureu-vos que els permisos del porta-retalls estiguin concedits.",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "Connexió SSH establerta",
|
||||
"authError": "L'autenticació ha fallat: {{message}}",
|
||||
"unknownError": "S'ha produït un error desconegut",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "Temps d'espera de verificació TOTP superat. Si us plau, torneu a connectar-vos.",
|
||||
"passwordTimeout": "Temps d'espera de verificació de la contrasenya. Si us plau, torneu a connectar-vos.",
|
||||
"connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa.",
|
||||
"hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada."
|
||||
"hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada.",
|
||||
"sessionTakenOver": "La sessió s'ha obert en una altra pestanya. S'està tornant a connectar...",
|
||||
"sessionAttachTimeout": "S'ha esgotat el temps d'espera per al fitxer adjunt de la sessió. S'està creant una connexió nova..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Gestor de fitxers",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "Escriure",
|
||||
"execute": "Executar",
|
||||
"permissionsChangedSuccessfully": "Els permisos s'han canviat correctament",
|
||||
"failedToChangePermissions": "No s'han pogut canviar els permisos"
|
||||
"failedToChangePermissions": "No s'han pogut canviar els permisos",
|
||||
"name": "Nom",
|
||||
"sortByName": "Nom",
|
||||
"sortByDate": "Data de modificació",
|
||||
"sortBySize": "Mida",
|
||||
"ascending": "Ascendent",
|
||||
"descending": "Descendent"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "Inicia la sessió",
|
||||
"registerButton": "Registra't",
|
||||
"forgotPassword": "Has oblidat la contrasenya?",
|
||||
"rememberMe": "Recorda'm",
|
||||
"rememberMe": "Recorda el dispositiu durant 30 dies (inclou TOTP)",
|
||||
"noAccount": "No tens un compte?",
|
||||
"hasAccount": "Ja tens un compte?",
|
||||
"loginSuccess": "Inici de sessió correcte",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "Processant...",
|
||||
"pleaseWait": "Si us plau, espereu...",
|
||||
"registrationDisabled": "El registre de comptes nous està desactivat actualment per un administrador. Si us plau, inicia la sessió o contacta amb un administrador.",
|
||||
"userNotAllowed": "El vostre compte no està autoritzat per registrar-se. Poseu-vos en contacte amb un administrador.",
|
||||
"databaseConnected": "La base de dades s'ha connectat correctament",
|
||||
"databaseConnectionFailed": "No s'ha pogut connectar al servidor de la base de dades",
|
||||
"checkServerConnection": "Si us plau, comproveu la connexió del servidor i torneu-ho a intentar.",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "Seleccioneu el tema de color per a l'aplicació",
|
||||
"terminalSyntaxHighlightingDesc": "Ressaltar automàticament les ordres, les rutes, les IP i els nivells de registre a la sortida del terminal",
|
||||
"enableCommandPaletteShortcut": "Activa la drecera de la paleta d'ordres",
|
||||
"enableCommandPaletteShortcutDesc": "Toqueu dues vegades la tecla Majúscules esquerra per obrir la paleta d'ordres i accedir ràpidament als amfitrions."
|
||||
"enableCommandPaletteShortcutDesc": "Toqueu dues vegades la tecla Majúscules esquerra per obrir la paleta d'ordres i accedir ràpidament als amfitrions.",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "Mantingueu les connexions SSH en canviar de pestanya o tancar el navegador (pot ser inestable)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "No s'ha pogut carregar la informació de la versió"
|
||||
@@ -2114,6 +2309,7 @@
|
||||
"usernameField": "nom",
|
||||
"scopes": "perfil de correu electrònic d'OpenID",
|
||||
"userinfoUrl": "https://el-teu-proveïdor.com/aplicació/o/info-usuari/",
|
||||
"allowedUsers": "usuari@exemple.com, @empresa.com",
|
||||
"enterUsername": "Introdueix el nom d'usuari per fer-lo administrador",
|
||||
"searchHosts": "Cerca hosts per nom, nom d'usuari, IP, carpeta, etiquetes...",
|
||||
"enterPassword": "Introdueix la teva contrasenya",
|
||||
|
||||
+374
-178
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"credentials": {
|
||||
"credentials": "Pověření",
|
||||
"credentials": "Přihlašovací údaje",
|
||||
"credentialsViewer": "Prohlížeč přihlašovacích údajů",
|
||||
"manageYourSSHCredentials": "Bezpečná správa přihlašovacích údajů SSH",
|
||||
"addCredential": "Přidat pověření",
|
||||
"createCredential": "Vytvořit pověření",
|
||||
"editCredential": "Upravit pověření",
|
||||
"viewCredential": "Zobrazit pověření",
|
||||
"duplicateCredential": "Duplikovat pověření",
|
||||
"deleteCredential": "Odstranit pověření",
|
||||
"updateCredential": "Aktualizovat pověření",
|
||||
"credentialName": "Název pověření",
|
||||
"credentialDescription": "L 343, 22.12.2009, s. 1).",
|
||||
"manageYourSSHCredentials": "Spravujte své přihlašovací údaje SSH bezpečně",
|
||||
"addCredential": "Přidat přihlašovací údaje",
|
||||
"createCredential": "Vytvořit přihlašovací údaje",
|
||||
"editCredential": "Upravit přihlašovací údaje",
|
||||
"viewCredential": "Zobrazit přihlašovací údaje",
|
||||
"duplicateCredential": "Duplikovat přihlašovací údaje",
|
||||
"deleteCredential": "Odstranit přihlašovací údaje",
|
||||
"updateCredential": "Aktualizovat přihlašovací údaje",
|
||||
"credentialName": "Název přihlašovacích údajů",
|
||||
"credentialDescription": "Popis",
|
||||
"username": "Uživatelské jméno",
|
||||
"searchCredentials": "Hledat přihlašovací údaje...",
|
||||
"selectFolder": "Vybrat složku",
|
||||
"searchCredentials": "Hledat přihlašovací údaje…",
|
||||
"selectFolder": "Vyberte složku",
|
||||
"selectAuthType": "Vyberte typ ověření",
|
||||
"allFolders": "Všechny složky",
|
||||
"allAuthTypes": "Všechny typy ověření",
|
||||
@@ -23,38 +23,38 @@
|
||||
"keyBased": "Na základě klíče",
|
||||
"passwordBased": "Na základě hesla",
|
||||
"folders": "Složky",
|
||||
"noCredentialsMatchFilters": "Žádné přihlašovací údaje neodpovídají vašim filtrům",
|
||||
"noCredentialsYet": "Dosud nebyla vytvořena žádná pověření",
|
||||
"createFirstCredential": "Vytvořte své první pověření",
|
||||
"noCredentialsMatchFilters": "Vašim filtrům neodpovídají žádné přihlašovací údaje",
|
||||
"noCredentialsYet": "Zatím nebyly vytvořeny žádné přihlašovací údaje",
|
||||
"createFirstCredential": "Vytvořte své první přihlašovací údaje",
|
||||
"failedToFetchCredentials": "Nepodařilo se načíst přihlašovací údaje",
|
||||
"credentialDeletedSuccessfully": "Pověření bylo úspěšně odstraněno",
|
||||
"failedToDeleteCredential": "Nepodařilo se odstranit pověření",
|
||||
"confirmDeleteCredential": "Jste si jisti, že chcete odstranit pověření \"{{name}}\"?",
|
||||
"credentialCreatedSuccessfully": "Pověření bylo úspěšně vytvořeno",
|
||||
"credentialUpdatedSuccessfully": "Útvar byl úspěšně aktualizován",
|
||||
"failedToSaveCredential": "Nepodařilo se uložit pověření",
|
||||
"failedToFetchCredentialDetails": "Nepodařilo se načíst detaily pověření",
|
||||
"failedToFetchHostsUsing": "Nepodařilo se načíst hostitele pomocí tohoto pověření",
|
||||
"loadingCredentials": "Načítání pověření...",
|
||||
"credentialDeletedSuccessfully": "Přihlašovací údaje byly úspěšně odstraněny",
|
||||
"failedToDeleteCredential": "Nepodařilo se odstranit přihlašovací údaje",
|
||||
"confirmDeleteCredential": "Opravdu chcete odstranit přihlašovací údaje „{{name}}“?",
|
||||
"credentialCreatedSuccessfully": "Přihlašovací údaje byly úspěšně vytvořeny",
|
||||
"credentialUpdatedSuccessfully": "Přihlašovací údaje byly úspěšně aktualizovány",
|
||||
"failedToSaveCredential": "Nepodařilo se uložit přihlašovací údaje",
|
||||
"failedToFetchCredentialDetails": "Nepodařilo se načíst podrobnosti o přihlašovacích údajích",
|
||||
"failedToFetchHostsUsing": "Nepodařilo se načíst hostitele pomocí těchto přihlašovacích údajů",
|
||||
"loadingCredentials": "Načítání přihlašovacích údajů…",
|
||||
"retry": "Opakovat",
|
||||
"noCredentials": "Žádné přihlašovací údaje",
|
||||
"noCredentialsMessage": "Zatím jste nepřidali žádné přihlašovací údaje. Chcete-li začít, klikněte na \"Přidat přihlašovací údaje\".",
|
||||
"sshCredentials": "SSH pověření",
|
||||
"credentialsCount": "{{count}} pověření",
|
||||
"refresh": "Aktualizovat",
|
||||
"passwordRequired": "Heslo je vyžadováno",
|
||||
"sshKeyRequired": "SSH klíč je povinný",
|
||||
"credentialAddedSuccessfully": "Pověření \"{{name}}\" bylo úspěšně přidáno",
|
||||
"savingCredential": "Ukládání přihlašovacích údajů...",
|
||||
"updatingCredential": "Aktualizace pověření...",
|
||||
"general": "Obecná ustanovení",
|
||||
"description": "L 343, 22.12.2009, s. 1).",
|
||||
"noCredentialsMessage": "Zatím jste nepřidali žádné přihlašovací údaje. Začněte kliknutím na „Přidat přihlašovací údaje“.",
|
||||
"sshCredentials": "Přihlašovací údaje SSH",
|
||||
"credentialsCount": "{{count}} přihlašovacích údajů",
|
||||
"refresh": "Obnovit",
|
||||
"passwordRequired": "Je vyžadováno heslo",
|
||||
"sshKeyRequired": "Je vyžadován klíč SSH",
|
||||
"credentialAddedSuccessfully": "Přihlašovací údaje „{{name}}“ byly úspěšně přidány",
|
||||
"savingCredential": "Ukládání přihlašovacích údajů…",
|
||||
"updatingCredential": "Aktualizace přihlašovacích údajů…",
|
||||
"general": "Obecné",
|
||||
"description": "Popis",
|
||||
"folder": "Složka",
|
||||
"tags": "Štítky",
|
||||
"addTagsSpaceToAdd": "Přidat štítky (stiskněte mezeru, chcete-li přidat)",
|
||||
"addTagsSpaceToAdd": "Přidat štítky (pro přidání stiskněte mezerník)",
|
||||
"password": "Heslo",
|
||||
"key": "Klíč",
|
||||
"sshPrivateKey": "SSH soukromý klíč",
|
||||
"sshPrivateKey": "Soukromý klíč SSH",
|
||||
"upload": "Nahrát",
|
||||
"updateKey": "Aktualizovat klíč",
|
||||
"keyPassword": "Heslo klíče",
|
||||
@@ -66,104 +66,104 @@
|
||||
"authentication": "Ověření",
|
||||
"organization": "Organizace",
|
||||
"basicInformation": "Základní informace",
|
||||
"basicInformationDescription": "Zadejte základní informace pro toto pověření",
|
||||
"basicInformationDescription": "Zadejte základní informace pro tyto přihlašovací údaje",
|
||||
"authenticationMethod": "Metoda ověřování",
|
||||
"authenticationMethodDescription": "Vyberte si, jak chcete ověřit pomocí SSH serverů",
|
||||
"organizationDescription": "Uspořádat vaše přihlašovací údaje se složkami a štítky",
|
||||
"enterCredentialName": "Zadejte název pověření",
|
||||
"authenticationMethodDescription": "Zvolte, jak se chcete ověřovat u serverů SSH",
|
||||
"organizationDescription": "Uspořádejte si přihlašovací údaje pomocí složek a štítků",
|
||||
"enterCredentialName": "Zadejte název pro přihlašovací údaje",
|
||||
"enterCredentialDescription": "Zadejte popis (volitelné)",
|
||||
"enterUsername": "Zadejte uživatelské jméno",
|
||||
"nameIsRequired": "Je vyžadován název pověření",
|
||||
"usernameIsRequired": "Uživatelské jméno je povinné",
|
||||
"nameIsRequired": "Je vyžadován název přihlašovach údajů",
|
||||
"usernameIsRequired": "Je vyžadováno uživatelské jméno",
|
||||
"authenticationType": "Typ ověření",
|
||||
"passwordAuthDescription": "Použít ověření hesla",
|
||||
"sshKeyAuthDescription": "Použít ověření klíče SSH",
|
||||
"passwordIsRequired": "Heslo je vyžadováno",
|
||||
"sshKeyIsRequired": "SSH klíč je povinný",
|
||||
"passwordAuthDescription": "Použít ověřování heslem",
|
||||
"sshKeyAuthDescription": "Použít ověřování klíčem SSH",
|
||||
"passwordIsRequired": "Je vyžadováno heslo",
|
||||
"sshKeyIsRequired": "Je vyžadován klíč SSH",
|
||||
"sshKeyType": "Typ klíče SSH",
|
||||
"privateKey": "Soukromý klíč",
|
||||
"enterPassword": "Zadejte heslo",
|
||||
"enterPrivateKey": "Zadejte soukromý klíč",
|
||||
"keyPassphrase": "Heslo klíče",
|
||||
"enterKeyPassphrase": "Zadejte heslo klíče (volitelné)",
|
||||
"keyPassphraseOptional": "Volitelné: ponechte prázdné, pokud váš klíč nemá žádné přístupové heslo",
|
||||
"leaveEmptyToKeepCurrent": "Ponechte prázdné pro zachování aktuální hodnoty",
|
||||
"uploadKeyFile": "Nahrát klíčový soubor",
|
||||
"generateKeyPairButton": "Generovat párování klíče",
|
||||
"generateKeyPair": "Generovat párování klíče",
|
||||
"generateKeyPairDescription": "Vygenerujte novou dvojici klíčů SSH. Pokud chcete klíč chránit pomocí přístupové fráze, zadejte jej nejprve do pole Klíčové heslo.",
|
||||
"deploySSHKey": "Nasadit SSH klíč",
|
||||
"keyPassphraseOptional": "Volitelné: pokud váš klíč nemá heslo, ponechte prázdné",
|
||||
"leaveEmptyToKeepCurrent": "Pro zachování aktuální hodnoty ponechte prázdné",
|
||||
"uploadKeyFile": "Nahrát soubor klíče",
|
||||
"generateKeyPairButton": "Generovat pár klíčů",
|
||||
"generateKeyPair": "Generovat pár klíčů",
|
||||
"generateKeyPairDescription": "Vygenerujte nový pár klíčů SSH. Pokud chcete klíč chránit heslem, zadejte ho nejprve do pole Heslo klíče níže.",
|
||||
"deploySSHKey": "Nasadit klíč SSH",
|
||||
"deploySSHKeyDescription": "Nasadit veřejný klíč na cílový server",
|
||||
"sourceCredential": "Zdrojové autority",
|
||||
"targetHost": "Hostitel cíle",
|
||||
"sourceCredential": "Zdrojové přihlašovací údaje",
|
||||
"targetHost": "Cílový hostitel",
|
||||
"deploymentProcess": "Proces nasazení",
|
||||
"deploymentProcessDescription": "Toto bezpečně přidá veřejný klíč do souboru ~/.ssh/authorized_keys bez přepsání existujících klíčů. Operace je vratná.",
|
||||
"chooseHostToDeploy": "Vyberte hostitele pro nasazení...",
|
||||
"deploying": "Nasazování...",
|
||||
"deploymentProcessDescription": "Toto bezpečně přidá veřejný klíč do souboru ~/.ssh/authorized_keys cílového hostitele bez přepsání existujících klíčů. Operace je vratná.",
|
||||
"chooseHostToDeploy": "Zvolte hostitele pro nasazení…",
|
||||
"deploying": "Nasazování…",
|
||||
"copyDeployCommand": "Kopírovat příkaz pro nasazení",
|
||||
"copyDeployCommandDescription": "Zkopírujte příkaz shell do manuálního přidání veřejného klíče do souboru autorized_keys. Užitečné, pokud je heslo vypnuto.",
|
||||
"copyDeployCommandDescription": "Zkopírujte příkaz shellu pro ruční přidání veřejného klíče do souboru authorized_keys hostitele. Užitečné, když je zakázáno ověřování heslem.",
|
||||
"deployCommandCopied": "Příkaz nasazení zkopírován do schránky",
|
||||
"manualDeployInfo": "Vložte tento příkaz do terminálu cílového hostitele (např. Proxmox konzola, IPMI nebo fyzický přístup) pro přidání SSH klíče.",
|
||||
"manualDeployInfo": "Vložte tento příkaz do terminálu cílového hostitele (např. konzole Proxmox, IPMI nebo fyzického přístupu) pro přidání klíče SSH.",
|
||||
"name": "Název",
|
||||
"noHostsAvailable": "Nejsou k dispozici žádní hostitelé",
|
||||
"noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání",
|
||||
"sshKeyGenerationNotImplemented": "Vytváření SSH klíče přichází brzy",
|
||||
"connectionTestingNotImplemented": "Testovací funkce připojení přichází brzy",
|
||||
"noHostsAvailable": "Nejsou dostupní žádní hostitelé",
|
||||
"noHostsMatchSearch": "Vašemu hledání neodpovídají žádní hostitelé",
|
||||
"sshKeyGenerationNotImplemented": "Funkce generování klíčů SSH bude brzy dostupná",
|
||||
"connectionTestingNotImplemented": "Funkce testování připojení bude brzy dostupná",
|
||||
"testConnection": "Testovat připojení",
|
||||
"selectOrCreateFolder": "Vyberte nebo vytvořte složku",
|
||||
"noFolder": "Žádná složka",
|
||||
"orCreateNewFolder": "Nebo vytvořit novou složku",
|
||||
"orCreateNewFolder": "Nebo vytvořte novou složku",
|
||||
"addTag": "Přidat štítek",
|
||||
"saving": "Ukládání...",
|
||||
"credentialId": "ID pověření",
|
||||
"saving": "Ukládání…",
|
||||
"credentialId": "ID přihlašovacích údajů",
|
||||
"overview": "Přehled",
|
||||
"security": "Zabezpečení",
|
||||
"usage": "Využití",
|
||||
"securityDetails": "Detaily zabezpečení",
|
||||
"securityDetailsDescription": "Zobrazit šifrované přihlašovací informace",
|
||||
"credentialSecured": "Zabezpečení důvěryhodnosti",
|
||||
"securityDetails": "Podrobnosti zabezpečení",
|
||||
"securityDetailsDescription": "Zobrazit zašifrované informace o přihlašovacích údajích",
|
||||
"credentialSecured": "Zabezpečené přihlašovací údaje",
|
||||
"credentialSecuredDescription": "Všechna citlivá data jsou šifrována pomocí AES-256",
|
||||
"passwordAuthentication": "Ověření hesla",
|
||||
"keyAuthentication": "Ověření klíče",
|
||||
"passwordAuthentication": "Ověřování heslem",
|
||||
"keyAuthentication": "Ověřování klíčem",
|
||||
"securityReminder": "Upozornění zabezpečení",
|
||||
"securityReminderText": "Nikdy nesdílejte Vaše přihlašovací údaje. Všechna data jsou v odpočinku šifrována.",
|
||||
"hostsUsingCredential": "Hostitelé používající tento klíč",
|
||||
"noHostsUsingCredential": "Žádné hostitele v současné době používají toto pověření",
|
||||
"securityReminderText": "Nikdy nesdílejte své přihlašovací údaje. Všechna data jsou v klidovém stavu šifrována.",
|
||||
"hostsUsingCredential": "Hostitelé používající tyto přihlašovací údaje",
|
||||
"noHostsUsingCredential": "Tyto přihlašovací údaje nepoužívají aktuálně žádní hostitelé",
|
||||
"timesUsed": "Četnost použití",
|
||||
"lastUsed": "Naposledy použito",
|
||||
"connectedHosts": "Připojení hostitelé",
|
||||
"created": "Vytvořeno",
|
||||
"lastModified": "Poslední změna",
|
||||
"lastModified": "Naposledy upraveno",
|
||||
"usageStatistics": "Statistiky využití",
|
||||
"copiedToClipboard": "{{field}} zkopírován do schránky",
|
||||
"copiedToClipboard": "{{field}} zkopírováno do schránky",
|
||||
"failedToCopy": "Kopírování do schránky se nezdařilo",
|
||||
"sshKey": "SSH klíč",
|
||||
"createCredentialDescription": "Vytvořit nové SSH pověření pro bezpečný přístup",
|
||||
"editCredentialDescription": "Aktualizovat důvěryhodné informace",
|
||||
"sshKey": "Klíč SSH",
|
||||
"createCredentialDescription": "Vytvořit nové přihlašovací údaje SSH pro bezpečný přístup",
|
||||
"editCredentialDescription": "Aktualizovat informace o přihlašovacích údajích",
|
||||
"listView": "Seznam",
|
||||
"folderView": "Složky",
|
||||
"unknownCredential": "Neznámý",
|
||||
"confirmRemoveFromFolder": "Opravdu chcete odstranit \"{{name}}\" ze složky \"{{folder}}\"? pověření bude přesunuto do \"Uncategorized\".",
|
||||
"removedFromFolder": "Pověření \"{{name}}\" bylo úspěšně odstraněno ze složky",
|
||||
"failedToRemoveFromFolder": "Nepodařilo se odstranit pověření ze složky",
|
||||
"folderRenamed": "Složka \"{{oldName}}\" byla úspěšně přejmenována na \"{{newName}}\"",
|
||||
"failedToRenameFolder": "Složku se nepodařilo přejmenovat",
|
||||
"movedToFolder": "Pověření \"{{name}}\" bylo úspěšně přesunuto do \"{{folder}}\"",
|
||||
"failedToMoveToFolder": "Nepodařilo se přesunout pověření do složky",
|
||||
"confirmRemoveFromFolder": "Opravdu chcete odebrat „{{name}}“ ze složky „{{folder}}“? Přihlašovací údaje budou přesunuty do složky „Nezařazené“.",
|
||||
"removedFromFolder": "Přihlašovací údaje „{{name}}“ byly úspěšně odebrány ze složky",
|
||||
"failedToRemoveFromFolder": "Nepodařilo se odebrat přihlašovací údaje ze složky",
|
||||
"folderRenamed": "Složka „{{oldName}}“ byla úspěšně přejmenována na „{{newName}}“",
|
||||
"failedToRenameFolder": "Nepodařilo se přejmenovat složku",
|
||||
"movedToFolder": "Pověření „{{name}}“ bylo úspěšně přesunuto do „{{folder}}“",
|
||||
"failedToMoveToFolder": "Nepodařilo se přesunout přihlašovací údaje do složky",
|
||||
"sshPublicKey": "Veřejný klíč SSH",
|
||||
"publicKeyNote": "Veřejný klíč je volitelný, ale doporučuje se pro ověření klíče",
|
||||
"publicKeyUploaded": "Veřejný klíč byl nahrán",
|
||||
"uploadPublicKey": "Nahrát veřejný klíč",
|
||||
"uploadPrivateKeyFile": "Nahrát soubor soukromého klíče",
|
||||
"uploadPublicKeyFile": "Nahrát soubor s veřejným klíčem",
|
||||
"uploadPublicKeyFile": "Nahrát soubor veřejného klíče",
|
||||
"privateKeyRequiredForGeneration": "Pro generování veřejného klíče je vyžadován soukromý klíč",
|
||||
"failedToGeneratePublicKey": "Nepodařilo se vygenerovat veřejný klíč",
|
||||
"generatePublicKey": "Generovat ze soukromého klíče",
|
||||
"publicKeyGeneratedSuccessfully": "Veřejný klíč byl úspěšně vygenerován",
|
||||
"detectedKeyType": "Zjištěný typ klíče",
|
||||
"detectingKeyType": "zjišťuji...",
|
||||
"optional": "Nepovinné",
|
||||
"generateKeyPairNew": "Generovat nové páry",
|
||||
"detectingKeyType": "zjišťování…",
|
||||
"optional": "Volitelné",
|
||||
"generateKeyPairNew": "Generovat nový pár klíčů",
|
||||
"generateEd25519": "Generovat Ed25519",
|
||||
"generateECDSA": "Generovat ECDSA",
|
||||
"generateRSA": "Generovat RSA",
|
||||
@@ -173,20 +173,20 @@
|
||||
"keyTypeDsa": "DSA (SSH)",
|
||||
"keyTypeRsaSha256": "RSA-SHA2-256",
|
||||
"keyTypeRsaSha512": "RSA-SHA2-512",
|
||||
"keyPairGeneratedSuccessfully": "{{keyType}} klíče byly úspěšně generovány",
|
||||
"keyPairGeneratedSuccessfully": "Pár klíčů {{keyType}} byl úspěšně vygenerován",
|
||||
"failedToGenerateKeyPair": "Nepodařilo se vygenerovat pár klíčů",
|
||||
"generateKeyPairNote": "Generovat nový pár SSH klíčů přímo. Tím se nahradí všechny existující klíče ve formuláři.",
|
||||
"generateKeyPairNote": "Generovat nový pár klíčů SSH přímo. Tím se nahradí všechny existující klíče ve formuláři.",
|
||||
"invalidKey": "Neplatný klíč",
|
||||
"detectionError": "Chyba detekce",
|
||||
"removing": "Odstranění:",
|
||||
"clickToEditCredential": "Kliknutím upravíte pověření",
|
||||
"detectionError": "Chyba zjišťování",
|
||||
"removing": "Odebírání:",
|
||||
"clickToEditCredential": "Kliknutím upravíte přihlašovací údaje",
|
||||
"dragToMoveBetweenFolders": "Přetažením přesunout mezi složkami",
|
||||
"keyBasedOnlyForDeployment": "Lze použít pouze přístupové údaje založené na klíči SSH",
|
||||
"publicKeyRequiredForDeployment": "Veřejný klíč je potřebný pro nasazení",
|
||||
"keyBasedOnlyForDeployment": "Lze nasadit pouze přihlašovací údaje založené na klíčích SSH",
|
||||
"publicKeyRequiredForDeployment": "Pro nasazení je vyžadován veřejný klíč",
|
||||
"selectTargetHost": "Vyberte prosím cílového hostitele",
|
||||
"keyDeployedSuccessfully": "SSH klíč byl úspěšně nasazen",
|
||||
"deploymentFailed": "Nasazení selhalo",
|
||||
"failedToDeployKey": "Nepodařilo se použít SSH klíč",
|
||||
"keyDeployedSuccessfully": "Klíč SSH byl úspěšně nasazen",
|
||||
"deploymentFailed": "Nasazení se nezdařilo",
|
||||
"failedToDeployKey": "Nepodařilo se nasadit klíč SSH",
|
||||
"clickToRenameFolder": "Klikněte pro přejmenování složky",
|
||||
"renameFolder": "Přejmenovat složku",
|
||||
"idLabel": "ID:"
|
||||
@@ -195,16 +195,16 @@
|
||||
"title": "Rychlé připojení",
|
||||
"description": "Připojit přímo k relaci terminálu nebo správce souborů bez uložení konfigurace hostitele",
|
||||
"ipAddress": "IP adresa nebo název hostitele",
|
||||
"port": "Přístav",
|
||||
"port": "Port",
|
||||
"username": "Uživatelské jméno",
|
||||
"password": "Heslo",
|
||||
"key": "SSH soukromý klíč",
|
||||
"keyPassword": "Klíčové heslo (volitelné)",
|
||||
"key": "Soukromý klíč SSH",
|
||||
"keyPassword": "Heslo klíče (volitelné)",
|
||||
"keyType": "Typ klíče",
|
||||
"uploadFile": "Nahrát soubor",
|
||||
"pasteKey": "Vložit klíč",
|
||||
"credential": "Pověření",
|
||||
"overrideUsername": "Přepsat pověření uživatelské jméno",
|
||||
"credential": "Přihlašovací údaj",
|
||||
"overrideUsername": "Přepsat uživatelské jméno pro přihlašovací údaje",
|
||||
"overrideUsernameDesc": "Použít jiné uživatelské jméno, než jaké je uloženo v pověření",
|
||||
"connectTerminal": "Připojit k terminálu",
|
||||
"connectFileManager": "Připojit ke správci souborů",
|
||||
@@ -238,20 +238,20 @@
|
||||
"commandsWillBeSent": "Příkazy budou odeslány do vybraných terminálů {{count}}.",
|
||||
"settings": "Nastavení",
|
||||
"enableRightClickCopyPaste": "Povolit kopírování/vkládání kliknutím pravým tlačítkem",
|
||||
"shareIdeas": "Máte nápady na to, co by mělo přijít dále pro ssh nástroje? Sdílejte je na",
|
||||
"shareIdeas": "Máte nápady, co by mělo být dalším vývojem nástrojů SSH? Sdílejte je na",
|
||||
"scripts": {
|
||||
"inputPlaceholder": "Např. systémové příkazy, Docker skripty"
|
||||
}
|
||||
},
|
||||
"snippets": {
|
||||
"title": "Výstřižky bloků",
|
||||
"new": "Nový snippet",
|
||||
"create": "Vytvořit snippet",
|
||||
"edit": "Upravit snippet",
|
||||
"title": "Úryvky",
|
||||
"new": "Nový úryvek",
|
||||
"create": "Vytvořit úryvek",
|
||||
"edit": "Upravit úryvek",
|
||||
"run": "Spustit",
|
||||
"empty": "Zatím žádné textové bloky",
|
||||
"emptyHint": "Vytvořit snippet pro uložení běžně používaných příkazů",
|
||||
"searchSnippets": "Hledat snippety...",
|
||||
"empty": "Zatím žádné úryvky",
|
||||
"emptyHint": "Vytvořit úryvek pro uložení běžně používaných příkazů",
|
||||
"searchSnippets": "Hledat úryvky...",
|
||||
"name": "Název",
|
||||
"description": "L 343, 22.12.2009, s. 1).",
|
||||
"content": "Příkaz",
|
||||
@@ -260,29 +260,29 @@
|
||||
"contentPlaceholder": "Např. sudo systemctl restart nginx",
|
||||
"nameRequired": "Název je povinný",
|
||||
"contentRequired": "Příkaz je povinný",
|
||||
"createDescription": "Vytvořit nový snippet příkazu pro rychlé spuštění",
|
||||
"editDescription": "Upravit tento snippet příkazu",
|
||||
"deleteConfirmTitle": "Odstranit snippet",
|
||||
"createDescription": "Vytvořit nový úryvek příkazu pro rychlé spuštění",
|
||||
"editDescription": "Upravit tento úryvek příkazu",
|
||||
"deleteConfirmTitle": "Odstranit úryvek",
|
||||
"deleteConfirmDescription": "Jste si jisti, že chcete odstranit \"{{name}}\"?",
|
||||
"createSuccess": "Výstřižek byl úspěšně vytvořen",
|
||||
"createSuccess": "Úryvek byl úspěšně vytvořen",
|
||||
"updateSuccess": "Úryvek byl úspěšně aktualizován",
|
||||
"deleteSuccess": "Textový blok byl úspěšně odstraněn",
|
||||
"createFailed": "Nepodařilo se vytvořit snippet",
|
||||
"updateFailed": "Nepodařilo se aktualizovat snippet",
|
||||
"deleteFailed": "Nepodařilo se odstranit snippet",
|
||||
"failedToFetch": "Nepodařilo se načíst snippety",
|
||||
"deleteSuccess": "Úryvek byl úspěšně odstraněn",
|
||||
"createFailed": "Nepodařilo se vytvořit úryvek",
|
||||
"updateFailed": "Nepodařilo se aktualizovat úryvek",
|
||||
"deleteFailed": "Nepodařilo se odstranit úryvek",
|
||||
"failedToFetch": "Nepodařilo se načíst úryvky",
|
||||
"executeSuccess": "Probíhá: {{name}}",
|
||||
"copySuccess": "Zkopírováno \"{{name}}\" do schránky",
|
||||
"runTooltip": "Spustit tento snippet v terminálu",
|
||||
"copyTooltip": "Zkopírovat snippet do schránky",
|
||||
"runTooltip": "Spustit tento úryvek v terminálu",
|
||||
"copyTooltip": "Zkopírovat úryvek do schránky",
|
||||
"editTooltip": "Upravit tento úryvek",
|
||||
"deleteTooltip": "Odstranit tento úryvek",
|
||||
"newFolder": "Nová složka",
|
||||
"reorderSameFolder": "Lze změnit pořadí snippetů ve stejné složce",
|
||||
"reorderSuccess": "Úryvky úspěšně seřazeny",
|
||||
"reorderFailed": "Nepodařilo se změnit pořadí snippetů",
|
||||
"deleteFolderConfirm": "Odstranit složku \"{{name}}\"? Všechny textové bloky budou přesunuty do nekategorizované.",
|
||||
"deleteFolderSuccess": "Složka byla úspěšně smazána",
|
||||
"reorderSameFolder": "Lze změnit pořadí úryvků pouze ve stejné složce",
|
||||
"reorderSuccess": "Pořadí úryvků bylo úspěšně změněno",
|
||||
"reorderFailed": "Nepodařilo se změnit pořadí úryvků",
|
||||
"deleteFolderConfirm": "Odstranit složku \"{{name}}\"? Všechny úryvky budou přesunuty do Nezařazené.",
|
||||
"deleteFolderSuccess": "Složka byla úspěšně odstraněna",
|
||||
"deleteFolderFailed": "Nepodařilo se odstranit složku",
|
||||
"updateFolderSuccess": "Složka byla úspěšně aktualizována",
|
||||
"createFolderSuccess": "Složka byla úspěšně vytvořena",
|
||||
@@ -293,7 +293,7 @@
|
||||
"executeOnCurrent": "Spustit na aktuálním terminálu (klepnutím vyberte více možností)",
|
||||
"folder": "Složka",
|
||||
"selectFolder": "Vyberte složku nebo ponechte prázdné",
|
||||
"noFolder": "Žádná složka (nekategorizovaná)",
|
||||
"noFolder": "Žádná složka (Nezařazené)",
|
||||
"folderName": "Název složky",
|
||||
"folderNameRequired": "Název složky je povinný",
|
||||
"folderColor": "Barva složky",
|
||||
@@ -302,10 +302,10 @@
|
||||
"updateFolder": "Aktualizovat složku",
|
||||
"createFolder": "Vytvořit složku",
|
||||
"editFolder": "Upravit složku",
|
||||
"editFolderDescription": "Přizpůsobit složku snippetu",
|
||||
"editFolderDescription": "Přizpůsobit složku úryvku",
|
||||
"createFolderDescription": "Uspořádat úryvky do složek",
|
||||
"confirmExecution": "Spustit \"{{name}}\"?",
|
||||
"confirmExecutionDesc": "Jste si jisti, že chcete spustit tento snippet?"
|
||||
"confirmExecutionDesc": "Opravdu chcete spustit tento úryvek?"
|
||||
},
|
||||
"commandHistory": {
|
||||
"title": "Historie",
|
||||
@@ -331,6 +331,8 @@
|
||||
"twoSplit": "2-Way",
|
||||
"threeSplit": "3-Way",
|
||||
"fourSplit": "4 cesty",
|
||||
"fiveSplit": "5-Way",
|
||||
"sixSplit": "6-Way",
|
||||
"availableTabs": "Dostupné panely",
|
||||
"dragTabsHint": "Přetáhněte záložky do polí níže uvedeného rozvržení pro jejich přiřazení",
|
||||
"layout": "Rozdělit rozložení obrazovky",
|
||||
@@ -372,7 +374,11 @@
|
||||
"warning": "Varování",
|
||||
"notValidatedWarning": "URL není ověřena - ujistěte se, že je to správné",
|
||||
"changeServer": "Změnit server",
|
||||
"mustIncludeProtocol": "Adresa URL serveru musí začínat http:// nebo https:////"
|
||||
"mustIncludeProtocol": "Adresa URL serveru musí začínat http:// nebo https:////",
|
||||
"useEmbedded": "Použít místní server",
|
||||
"embeddedDesc": "Spustit Termix s vestavěným lokálním serverem (nepotřebujete vzdálený server)",
|
||||
"embeddedConnecting": "Připojování k lokálnímu serveru...",
|
||||
"embeddedNotReady": "Místní server není ještě připravený. Počkejte prosím chvíli a zkuste to znovu."
|
||||
},
|
||||
"versionCheck": {
|
||||
"error": "Chyba kontroly verze",
|
||||
@@ -471,6 +477,7 @@
|
||||
"confirm": "Potvrdit",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
"or": "NEBO",
|
||||
"ok": "OK",
|
||||
"enabled": "Povoleno",
|
||||
"disabled": "Zakázáno",
|
||||
@@ -520,13 +527,13 @@
|
||||
"serverStats": "Statistiky serveru",
|
||||
"admin": "Admin",
|
||||
"userProfile": "Profil uživatele",
|
||||
"tools": "Nástroje a nářadí",
|
||||
"snippets": "Výstřižky bloků",
|
||||
"tools": "Nástroje",
|
||||
"snippets": "Úryvky",
|
||||
"newTab": "New Tab",
|
||||
"splitScreen": "Rozdělit obrazovku",
|
||||
"closeTab": "Zavřít kartu",
|
||||
"sshManager": "SSH správce",
|
||||
"hostManager": "Manažer serveru",
|
||||
"sshManager": "Správce SSH",
|
||||
"hostManager": "Správce hostitelů",
|
||||
"cannotSplitTab": "Nelze rozdělit tuto kartu",
|
||||
"tabNavigation": "Tab Navigation",
|
||||
"hostTabTitle": "{{username}}@{{ip}}:{{port}}",
|
||||
@@ -601,6 +608,8 @@
|
||||
"userDeletedSuccessfully": "Uživatel {{username}} byl úspěšně smazán",
|
||||
"failedToDeleteUser": "Odstranění uživatele se nezdařilo",
|
||||
"overrideUserInfoUrl": "Přepsat URL informace o uživateli (nepovinné)",
|
||||
"allowedUsers": "Povolení uživatelé",
|
||||
"allowedUsersDescription": "Seznam povolených e-mailových vzorů oddělených čárkou. Použijte přesné e-maily (user@example.com), přípony domény (@example.com), nebo nechte prázdné pro povolení všem uživatelům.",
|
||||
"failedToFetchSessions": "Nepodařilo se načíst relace",
|
||||
"sessionRevokedSuccessfully": "Relace byla úspěšně zrušena",
|
||||
"failedToRevokeSession": "Nepodařilo se zrušit relaci",
|
||||
@@ -792,6 +801,23 @@
|
||||
"oidcRequiredWarning": "CRITICAL: Přihlášení k heslu je zakázáno. Pokud resetujete nebo špatně nastavíte OIDC, ztratíte přístup k Termix a blokovat vaši instanci. Pokračujte pouze v případě, že jste naprosto jistí.",
|
||||
"confirmDisableOIDCWarning": "VAROVÁNÍ: Chystáte se zakázat OIDC, zatímco je také zakázáno přihlášení heslem. Tohle blokuje vaši instanci Termix a ztratíte veškerý přístup. Jste si jisti, že chcete pokračovat?",
|
||||
"failedToUpdatePasswordLoginStatus": "Nepodařilo se aktualizovat stav přihlášení k heslu",
|
||||
"monitoringDefaults": "Výchozí nastavení sledování",
|
||||
"monitoringDefaultsDesc": "Globální výchozí intervaly pro kontrolu stavu a sběr metrik. Nastavení hostitele tyto hodnoty přepsá.",
|
||||
"globalStatusCheckInterval": "Interval kontroly stavu",
|
||||
"globalMetricsInterval": "Výchozí interval sběru metrik",
|
||||
"globalSettingsSaved": "Globální nastavení monitorování uloženo",
|
||||
"failedToSaveGlobalSettings": "Nepodařilo se uložit globální nastavení monitorování",
|
||||
"failedToLoadGlobalSettings": "Nepodařilo se načíst globální nastavení monitorování",
|
||||
"guacamoleIntegration": "Integrace vzdálené plochy (Guacamole)",
|
||||
"guacamoleIntegrationDesc": "Povolit připojení RDP, VNC a Telnet pomocí guacdu. Vyžaduje spuštění instance guacd.",
|
||||
"enableGuacamole": "Povolit podporu RDP/VNC/Telnet",
|
||||
"guacdUrl": "guacd URL (host:port)",
|
||||
"guacdUrlPlaceholder": "guakd:4822",
|
||||
"guacdUrlNote": "Změny hostitele/portu guacd vyžadují restartování serveru.",
|
||||
"guacamoleSettingsSaved": "Guacamole settings saved",
|
||||
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
|
||||
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
|
||||
"clampedToValidRange": "byl upraven na platný rozsah",
|
||||
"loadingSessions": "Načítání relací...",
|
||||
"noActiveSessions": "Nebyly nalezeny žádné aktivní relace.",
|
||||
"device": "Zařízení",
|
||||
@@ -814,7 +840,7 @@
|
||||
"assignNewRole": "Přiřadit novou roli"
|
||||
},
|
||||
"hosts": {
|
||||
"title": "Manažer serveru",
|
||||
"title": "Správce hostitelů",
|
||||
"hosts": "Hostitelé",
|
||||
"sshHosts": "SSH hostitelé",
|
||||
"noHosts": "Žádné SSH hostitele",
|
||||
@@ -829,7 +855,7 @@
|
||||
"importing": "Importování...",
|
||||
"importJsonTitle": "Importovat SSH hostitele z JSON",
|
||||
"importJsonDesc": "Nahrajte soubor JSON pro hromadný import více SSH hostitelů (max. 100).",
|
||||
"downloadSample": "Stáhnout ukázku",
|
||||
"downloadSample": "Stáhnout vzorek",
|
||||
"formatGuide": "Průvodce formátováním",
|
||||
"exportCredentialWarning": "Varování: Hostitel \"{{name}}\" používá ověření důvěryhodnosti. Exportovaný soubor nebude obsahovat přihlašovací údaje a bude muset být po importu ručně přenakonfigurován. Chcete pokračovat?",
|
||||
"exportSensitiveDataWarning": "Varování: Hostitel \"{{name}}\" obsahuje citlivé ověřovací údaje (heslo/SSH klíč). Exportovaný soubor bude obsahovat tato data do prostého textu. Udržujte soubor bezpečný a po jeho použití. Chcete pokračovat?",
|
||||
@@ -841,15 +867,30 @@
|
||||
"noHostsInJson": "Nebyly nalezeny žádné hostitele v souboru JSON",
|
||||
"maxHostsAllowed": "Maximálně 100 hostitelů na import",
|
||||
"importCompleted": "Import dokončen: {{success}} úspěšný, {{failed}} selhalo",
|
||||
"importCreated": "vytvořeno",
|
||||
"importUpdated": "aktualizováno",
|
||||
"importFailedCount": "selhalo",
|
||||
"importSkipExisting": "Import (existující přeskočení)",
|
||||
"importOverwriteExisting": "Import (přepsat)",
|
||||
"importFailed": "Import se nezdařil",
|
||||
"importError": "Chyba importu",
|
||||
"failedToImportJson": "Nepodařilo se importovat soubor JSON",
|
||||
"connectionDetails": "Detaily připojení",
|
||||
"connectionType": "Connection Type",
|
||||
"ssh": "SSH",
|
||||
"rdp": "RDP",
|
||||
"vnc": "VNC",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"guacamoleSettings": "Remote Desktop Settings",
|
||||
"organization": "Organizace",
|
||||
"ipAddress": "IP adresa nebo název hostitele",
|
||||
"port": "Přístav",
|
||||
"ipRequired": "IP address is required",
|
||||
"portRequired": "Port is required",
|
||||
"port": "Port",
|
||||
"name": "Název",
|
||||
"username": "Uživatelské jméno",
|
||||
"usernameRequired": "Username is required (SSH/Telnet only)",
|
||||
"folder": "Složka",
|
||||
"tags": "Štítky",
|
||||
"pin": "Připnout",
|
||||
@@ -885,7 +926,7 @@
|
||||
"remove": "Odebrat",
|
||||
"sourcePort": "Zdrojový port",
|
||||
"sourcePortDesc": " (Odběr se týká podrobností o aktuálním připojení v obecné kartě)",
|
||||
"endpointPort": "Přístav koncového bodu",
|
||||
"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í",
|
||||
@@ -958,6 +999,8 @@
|
||||
"tunnel": "Tunel",
|
||||
"fileManager": "Správce souborů",
|
||||
"serverStats": "Statistiky serveru",
|
||||
"status": "Status",
|
||||
"statistics": "Statistiky",
|
||||
"hostViewer": "Prohlížeč hostitelů",
|
||||
"enableServerStats": "Povolit statistiky serveru",
|
||||
"enableServerStatsDesc": "Povolit/zakázat sběr statistik serveru pro tohoto hostitele",
|
||||
@@ -1007,7 +1050,6 @@
|
||||
"openFileManager": "Otevřít správce souborů",
|
||||
"openTunnels": "Otevřené tunely",
|
||||
"openServerDetails": "Detaily otevřeného serveru",
|
||||
"statistics": "Statistiky",
|
||||
"enabledWidgets": "Povolené widgety",
|
||||
"openServerStats": "Otevřené statistiky serveru",
|
||||
"enabledWidgetsDesc": "Vyberte, které widgety statistiky se zobrazí pro tohoto hostitele",
|
||||
@@ -1017,15 +1059,23 @@
|
||||
"statusCheckEnabledDesc": "Zkontrolujte, zda je server online nebo offline",
|
||||
"statusCheckInterval": "Interval kontroly stavu",
|
||||
"statusCheckIntervalDesc": "Jak často kontrolovat, zda je hostitel online (5s - 1h)",
|
||||
"statusChecks": "Status Checks",
|
||||
"disableTcpPing": "Disable TCP Ping",
|
||||
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
|
||||
"useGlobalStatusInterval": "Použít globální výchozí nastavení",
|
||||
"useGlobalMetricsInterval": "Použít globální výchozí nastavení",
|
||||
"usingGlobalDefault": "Použití globálního výchozího ({{value}}s)",
|
||||
"metricsCollection": "Metrics Collection",
|
||||
"metricsEnabled": "Povolit sledování metriky",
|
||||
"metricsEnabledDesc": "Sbírat CPU, RAM, disky a další systémové statistiky",
|
||||
"metricsInterval": "Interval sběru metrik",
|
||||
"metricsIntervalDesc": "Jak často shromažďovat statistiky serveru (5s - 1h)",
|
||||
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
|
||||
"intervalSeconds": "sekundy",
|
||||
"intervalMinutes": "minuty",
|
||||
"intervalValidation": "Monitorovací intervaly musí být mezi 5 sekundami a 1 hodinou (3600 sekund)",
|
||||
"monitoringDisabled": "Monitorování serveru je pro tohoto hostitele zakázáno",
|
||||
"enableMonitoring": "Povolit monitorování v Host Manager → Záložka Statistiky",
|
||||
"enableMonitoring": "Povolit monitorování ve Správci hostitelů → záložka Statistiky",
|
||||
"monitoringDisabledBadge": "Monitorování vypnuto",
|
||||
"statusMonitoring": "Stav",
|
||||
"metricsMonitoring": "Metrika",
|
||||
@@ -1088,9 +1138,9 @@
|
||||
"backspaceModeNormal": "Normální (DEL)",
|
||||
"backspaceModeControlH": "Ovládání H (^H)",
|
||||
"backspaceModeDesc": "Chování tlačítka Backspace pro kompatibilitu",
|
||||
"startupSnippet": "Spustit snippet",
|
||||
"startupSnippet": "Spustit úryvek",
|
||||
"selectSnippet": "Vybrat úryvek",
|
||||
"searchSnippets": "Hledat snippety...",
|
||||
"searchSnippets": "Hledat úryvky...",
|
||||
"snippetNone": "Nic",
|
||||
"noneAuthTitle": "Klávesnice – interaktivní ověření",
|
||||
"noneAuthDescription": "Tato metoda ověřování bude při připojování k serveru SSH používat klávesnice-interaktivní ověřování.",
|
||||
@@ -1113,7 +1163,7 @@
|
||||
"enableSocks5": "Povolit SOCKS5 Proxy",
|
||||
"enableSocks5Description": "Použít SOCKS5 proxy pro toto SSH připojení",
|
||||
"socks5Host": "Proxy server",
|
||||
"socks5Port": "Port proxy serveru",
|
||||
"socks5Port": "Port serveru proxy",
|
||||
"socks5Username": "Proxy uživatelské jméno",
|
||||
"socks5Password": "Proxy heslo",
|
||||
"socks5UsernameOptional": "Volitelné: ponechte prázdné, pokud proxy nevyžaduje ověření",
|
||||
@@ -1150,11 +1200,11 @@
|
||||
"proxyNode": "Proxy uzel",
|
||||
"proxyType": "Typ proxy",
|
||||
"quickActions": "Rychlé akce",
|
||||
"quickActionsDescription": "Rychlé akce vám umožní vytvořit vlastní tlačítka, která na tomto serveru spustí snippety SSH. Tato tlačítka se zobrazí v horní části statistiky serveru pro rychlý přístup.",
|
||||
"quickActionsDescription": "Rychlé akce vám umožní vytvořit vlastní tlačítka, která na tomto serveru spustí úryvky SSH. Tato tlačítka se zobrazí v horní části statistiky serveru pro rychlý přístup.",
|
||||
"quickActionsList": "Seznam rychlých akcí",
|
||||
"addQuickAction": "Přidat rychlou akci",
|
||||
"quickActionName": "Název akce",
|
||||
"noSnippetFound": "Úryvek nenalezen",
|
||||
"noSnippetFound": "Nebyl nalezen žádný úryvek",
|
||||
"quickActionsOrder": "Tlačítka pro rychlé akce se zobrazí v pořadí uvedeném výše na stránce Statistiky serveru",
|
||||
"sidebarCustomization": "Přizpůsobení postranního panelu",
|
||||
"sidebarCustomizationDesc": "Vyberte, které akce se zobrazí jako rychlá tlačítka v postranním panelu. Akce nezobrazené jako tlačítka se zobrazí v rozbalovacím menu.",
|
||||
@@ -1174,9 +1224,21 @@
|
||||
"sudoPasswordAutoFillDesc": "Automaticky nabídne vložení hesla SSH při žádání o heslo",
|
||||
"sudoPassword": "Sudo heslo",
|
||||
"sudoPasswordDesc": "Volitelné heslo pro sudo příkazy (užitečné s ověřením klíče)",
|
||||
"keepaliveSettings": "SSH uchová",
|
||||
"keepaliveSettingsDesc": "Konfigurace udržovacího chování SSH-úrovně. Nastavte interval 0 pro vypnutí SSH zachování (užitečné pro MikroTik a další zařízení, která ignorují udržující požadavky).",
|
||||
"keepaliveInterval": "Interval Keepalive (ms)",
|
||||
"keepaliveIntervalDesc": "Jak často odesílat SSH udržující pingy. Výchozí: 30000 (30s). Nastavte na 0 pro vypnutí.",
|
||||
"keepaliveCountMax": "Ponechat maximální počet selhání",
|
||||
"keepaliveCountMaxDesc": "Odpojte se po tomto počtu nezodpovězených držení. Výchozí: 3.",
|
||||
"socks4": "SOCKS4",
|
||||
"socks5": "SOCKS5",
|
||||
"executeSnippetOnConnect": "Spustit snippet, když se terminál připojí",
|
||||
"httpConnect": "HTTP PŘIPOJIT",
|
||||
"testProxy": "Testovat připojení",
|
||||
"testingProxy": "Testování...",
|
||||
"proxyTestSuccess": "Proxy připojení úspěšné ({{latency}}ms)",
|
||||
"proxyTestFailed": "Proxy test selhal: {{error}}",
|
||||
"connectionPath": "Cesta k připojení",
|
||||
"executeSnippetOnConnect": "Spustit úryvek, když se terminál připojí",
|
||||
"autoMosh": "Automatické MOSH",
|
||||
"autoMoshDesc": "Automaticky spustit MOSH příkaz při připojení",
|
||||
"moshCommand": "Příkaz MOSH",
|
||||
@@ -1196,6 +1258,7 @@
|
||||
"copyTunnelUrl": "Kopírovat URL tunelu",
|
||||
"copyServerStatsUrl": "Kopírovat URL statistiky serveru",
|
||||
"copyDockerUrl": "Kopírovat URL Dockeru",
|
||||
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
|
||||
"fullScreenUrlTooltip": "Kopírovat URL pro otevření této aplikace v režimu celé obrazovky",
|
||||
"notEnabled": "Docker není pro tohoto hostitele povolen. Povolte jej v nastavení hostitele pro použití funkcí Dockeru.",
|
||||
"validating": "Ověřování Dockeru...",
|
||||
@@ -1237,7 +1300,7 @@
|
||||
"failedToFetchStats": "Nepodařilo se načíst statistiky",
|
||||
"noContainersFound": "Nebyly nalezeny žádné kontejnery",
|
||||
"noContainersFoundHint": "Začněte vytvořením kontejnerů na vašem serveru",
|
||||
"searchPlaceholder": "Hledat podle jména, obrázku, nebo ID...",
|
||||
"searchPlaceholder": "Hledat podle jména, obrázku, nebo ID…",
|
||||
"filterByStatusPlaceholder": "Filtrovat podle stavu",
|
||||
"allContainersCount": "Vše ({{count}})",
|
||||
"statusCount": "{{status}} ({{count}})",
|
||||
@@ -1256,7 +1319,7 @@
|
||||
"failedToRemoveContainer": "Odstranění kontejneru selhalo: {{error}}",
|
||||
"image": "Obrázek:",
|
||||
"idLabel": "ID:",
|
||||
"ports": "Přístavy:",
|
||||
"ports": "Porty:",
|
||||
"noPorts": "Nic",
|
||||
"created": "Vytvořeno:",
|
||||
"start": "Začít",
|
||||
@@ -1284,7 +1347,125 @@
|
||||
"showTimestamps": "Zobrazit časová razítka",
|
||||
"autoRefresh": "Automatické obnovení",
|
||||
"filterLogsPlaceholder": "Filtrovat logy...",
|
||||
"noLogsAvailable": "Žádné logy nejsou k dispozici"
|
||||
"noLogsAvailable": "Žádné logy nejsou k dispozici",
|
||||
"selectMode": "Vybrat",
|
||||
"exitSelectMode": "Zrušit",
|
||||
"selectedCount": "{{count}} vybrán",
|
||||
"bulkMonitoring": "Monitorování",
|
||||
"bulkFeatures": "Vlastnosti",
|
||||
"bulkMoveFolder": "Přesunout do složky",
|
||||
"bulkPin": "Připnout",
|
||||
"bulkUnpin": "Unpin",
|
||||
"enableAllFeatures": "Povolit všechny funkce",
|
||||
"disableAllFeatures": "Zakázat všechny funkce",
|
||||
"enableStatusCheck": "Povolit kontrolu stavu",
|
||||
"disableStatusCheck": "Zakázat kontrolu stavu",
|
||||
"enableMetrics": "Povolit metriku",
|
||||
"disableMetrics": "Zakázat metriku",
|
||||
"bulkEnableTerminal": "Povolit terminál",
|
||||
"bulkDisableTerminal": "Zakázat terminál",
|
||||
"bulkEnableTunnel": "Povolit tunel",
|
||||
"bulkDisableTunnel": "Zakázat tunel",
|
||||
"bulkEnableFileManager": "Povolit správce souborů",
|
||||
"bulkDisableFileManager": "Zakázat správce souborů",
|
||||
"bulkEnableDocker": "Povolit Docker",
|
||||
"bulkDisableDocker": "Zakázat Docker",
|
||||
"bulkUpdateSuccess": "Úspěšně aktualizováno {{count}} hostitele",
|
||||
"bulkUpdateFailed": "Hromadná aktualizace selhala",
|
||||
"selectAll": "Vybrat vše",
|
||||
"deselectAll": "Zrušit výběr všech",
|
||||
"useGlobalStatusDefault": "Použít globální výchozí (stav)",
|
||||
"useGlobalMetricsDefault": "Použít globální výchozí (Metrics)",
|
||||
"remoteDesktopSettings": "Remote Desktop Settings",
|
||||
"domain": "Domain",
|
||||
"securityMode": "Security Mode",
|
||||
"ignoreCert": "Ignore Certificate",
|
||||
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
|
||||
"displaySettings": "Display Settings",
|
||||
"colorDepth": "Color Depth",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"dpi": "DPI",
|
||||
"resizeMethod": "Resize Method",
|
||||
"forceLossless": "Force Lossless",
|
||||
"audioSettings": "Audio Settings",
|
||||
"disableAudio": "Disable Audio",
|
||||
"enableAudioInput": "Enable Audio Input",
|
||||
"rdpPerformance": "Performance",
|
||||
"enableWallpaper": "Enable Wallpaper",
|
||||
"enableTheming": "Enable Theming",
|
||||
"enableFontSmoothing": "Enable Font Smoothing",
|
||||
"enableFullWindowDrag": "Enable Full Window Drag",
|
||||
"enableDesktopComposition": "Enable Desktop Composition",
|
||||
"enableMenuAnimations": "Enable Menu Animations",
|
||||
"disableBitmapCaching": "Disable Bitmap Caching",
|
||||
"disableOffscreenCaching": "Disable Offscreen Caching",
|
||||
"disableGlyphCaching": "Disable Glyph Caching",
|
||||
"enableGfx": "Enable GFX",
|
||||
"deviceRedirection": "Device Redirection",
|
||||
"enablePrinting": "Enable Printing",
|
||||
"enableDrive": "Enable Drive Redirection",
|
||||
"driveName": "Drive Name",
|
||||
"drivePath": "Drive Path",
|
||||
"createDrivePath": "Create Drive Path",
|
||||
"disableDownload": "Disable Download",
|
||||
"disableUpload": "Disable Upload",
|
||||
"enableTouch": "Enable Touch",
|
||||
"rdpSession": "Session",
|
||||
"clientName": "Client Name",
|
||||
"consoleSession": "Console Session",
|
||||
"initialProgram": "Initial Program",
|
||||
"serverLayout": "Server Keyboard Layout",
|
||||
"timezone": "Timezone",
|
||||
"gatewaySettings": "Gateway",
|
||||
"gatewayHostname": "Gateway Hostname",
|
||||
"gatewayPort": "Gateway Port",
|
||||
"gatewayUsername": "Gateway Username",
|
||||
"gatewayPassword": "Gateway Password",
|
||||
"gatewayDomain": "Gateway Domain",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppProgram": "Remote Application",
|
||||
"remoteAppDir": "Remote App Directory",
|
||||
"remoteAppArgs": "Remote App Arguments",
|
||||
"clipboardSettings": "Clipboard",
|
||||
"normalizeClipboard": "Normalize Clipboard",
|
||||
"disableCopy": "Disable Copy",
|
||||
"disablePaste": "Disable Paste",
|
||||
"vncSettings": "VNC Settings",
|
||||
"cursorMode": "Cursor Mode",
|
||||
"swapRedBlue": "Swap Red/Blue",
|
||||
"readOnly": "Read Only",
|
||||
"recordingSettings": "Recording",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"createRecordingPath": "Create Recording Path",
|
||||
"excludeOutput": "Exclude Output",
|
||||
"excludeMouse": "Exclude Mouse",
|
||||
"includeKeys": "Include Keys",
|
||||
"wakeOnLan": "Wake-on-LAN",
|
||||
"sendWolPacket": "Send WoL Packet",
|
||||
"wolMacAddr": "MAC Address",
|
||||
"wolBroadcastAddr": "Broadcast Address",
|
||||
"wolUdpPort": "UDP Port",
|
||||
"wolWaitTime": "Wait Time (seconds)",
|
||||
"connectionSettings": "Connection Settings",
|
||||
"rdpOnly": "RDP only",
|
||||
"vncOnly": "VNC only",
|
||||
"telnetTerminalSettings": "Terminal Settings",
|
||||
"terminalType": "Terminal Type",
|
||||
"guacFontName": "Font Name",
|
||||
"guacFontSize": "Font Size",
|
||||
"guacColorScheme": "Color Scheme",
|
||||
"guacBackspaceKey": "Backspace Key"
|
||||
},
|
||||
"guacamole": {
|
||||
"connecting": "Connecting to {{type}} session...",
|
||||
"rdpConnecting": "Connecting to RDP server...",
|
||||
"vncConnecting": "Connecting to VNC server...",
|
||||
"telnetConnecting": "Connecting to Telnet server...",
|
||||
"connectionError": "Connection error",
|
||||
"connectionFailed": "Connection failed",
|
||||
"failedToConnect": "Failed to get connection token"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminál",
|
||||
@@ -1306,6 +1487,9 @@
|
||||
"connectionClosed": "Připojení bylo uzavřeno",
|
||||
"connectionError": "Chyba připojení: {{message}}",
|
||||
"connected": "Připojeno",
|
||||
"clipboardWriteFailed": "Kopírování do schránky se nezdařilo. Ujistěte se, že stránka je vedena přes HTTPS nebo localhost.",
|
||||
"clipboardReadFailed": "Nepodařilo se přečíst ze schránky. Ujistěte se, že máte oprávnění na schránku.",
|
||||
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
|
||||
"sshConnected": "SSH připojení navázáno",
|
||||
"authError": "Ověření se nezdařilo: {{message}}",
|
||||
"unknownError": "Došlo k neznámé chybě",
|
||||
@@ -1360,7 +1544,9 @@
|
||||
"totpTimeout": "Časový limit ověřování TOTP. Připojte se znovu.",
|
||||
"passwordTimeout": "Časový limit ověření hesla vypršel. Připojte se znovu.",
|
||||
"connectionRejected": "Připojení odmítnuto serverem. Zkontrolujte prosím ověření a nastavení sítě.",
|
||||
"hostKeyRejected": "Ověření SSH hostitele bylo zamítnuto. Připojení bylo zrušeno."
|
||||
"hostKeyRejected": "Ověření SSH hostitele bylo zamítnuto. Připojení bylo zrušeno.",
|
||||
"sessionTakenOver": "Relace byla otevřena v jiné kartě. Znovu připojuji...",
|
||||
"sessionAttachTimeout": "Vypršel časový limit přílohy relace. Vytváření nového připojení..."
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Správce souborů",
|
||||
@@ -1502,7 +1688,7 @@
|
||||
"cannotReadFile": "Soubor nelze přečíst",
|
||||
"noSshSessionId": "Žádné SSH ID relace není k dispozici",
|
||||
"noFilePath": "Není dostupná cesta k souboru",
|
||||
"noCurrentHost": "Aktuální hostitel není k dispozici",
|
||||
"noCurrentHost": "Aktuálně není dostupný žádný hostitel",
|
||||
"fileSavedSuccessfully": "Soubor byl úspěšně uložen",
|
||||
"saveTimeout": "Vypršel časový limit uložení. Soubor mohl být úspěšně uložen, ale dokončení operace trvalo příliš dlouho. Potvrzte protokol Dockeru.",
|
||||
"failedToSaveFile": "Nepodařilo se uložit soubor",
|
||||
@@ -1670,7 +1856,13 @@
|
||||
"write": "Napsat",
|
||||
"execute": "Spustit",
|
||||
"permissionsChangedSuccessfully": "Oprávnění úspěšně změněna",
|
||||
"failedToChangePermissions": "Změna oprávnění se nezdařila"
|
||||
"failedToChangePermissions": "Změna oprávnění se nezdařila",
|
||||
"name": "Název",
|
||||
"sortByName": "Název",
|
||||
"sortByDate": "Datum úpravy",
|
||||
"sortBySize": "Velikost",
|
||||
"ascending": "Vzestupně",
|
||||
"descending": "sestupně"
|
||||
},
|
||||
"tunnel": {
|
||||
"log": {
|
||||
@@ -1707,7 +1899,7 @@
|
||||
"connect": "Připojit",
|
||||
"disconnect": "Odpojit",
|
||||
"cancel": "Zrušit",
|
||||
"port": "Přístav",
|
||||
"port": "Port",
|
||||
"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",
|
||||
@@ -1786,7 +1978,7 @@
|
||||
"totpInvalidCode": "Neplatný ověřovací kód",
|
||||
"totpCancelled": "Metrika byla zrušena",
|
||||
"authenticationFailed": "Ověření se nezdařilo",
|
||||
"noneAuthNotSupported": "Statistiky serveru nepodporují 'none' typ autentizace.",
|
||||
"noneAuthNotSupported": "Statistiky serveru nepodporují typ ověřování 'žádné'.",
|
||||
"load": "Načíst",
|
||||
"editLayout": "Upravit rozvržení",
|
||||
"cancelEdit": "Zrušit",
|
||||
@@ -1820,13 +2012,13 @@
|
||||
"quickActionFailed": "{{name}} selhal",
|
||||
"quickActionError": "Provedení {{name}} se nezdařilo",
|
||||
"ports": {
|
||||
"title": "Přístavy poslech",
|
||||
"title": "Naslouchající porty",
|
||||
"protocol": "Protocol",
|
||||
"port": "Přístav",
|
||||
"port": "Port",
|
||||
"address": "Adresa",
|
||||
"state": "Stát",
|
||||
"process": "Proces",
|
||||
"noData": "Žádná data z portů pro poslech"
|
||||
"noData": "Žádná data naslouchajících portů"
|
||||
},
|
||||
"firewall": {
|
||||
"title": "Firewall",
|
||||
@@ -1839,7 +2031,7 @@
|
||||
"noData": "Nejsou k dispozici žádná data firewallu",
|
||||
"action": "Akce",
|
||||
"protocol": "Proto",
|
||||
"port": "Přístav",
|
||||
"port": "Port",
|
||||
"source": "Zdroj",
|
||||
"accept": "PŘIJMOUT",
|
||||
"drop": "DROP",
|
||||
@@ -1858,7 +2050,7 @@
|
||||
"loginButton": "Přihlásit se",
|
||||
"registerButton": "Registrovat se",
|
||||
"forgotPassword": "Zapomněli jste heslo?",
|
||||
"rememberMe": "Pamatovat si mě",
|
||||
"rememberMe": "Zapamatovat zařízení na 30 dní (včetně TOTP)",
|
||||
"noAccount": "Nemáte účet?",
|
||||
"hasAccount": "Již máte účet?",
|
||||
"loginSuccess": "Přihlášení bylo úspěšné",
|
||||
@@ -2023,6 +2215,7 @@
|
||||
"processing": "Zpracovávám...",
|
||||
"pleaseWait": "Počkejte prosím...",
|
||||
"registrationDisabled": "Registrace nového účtu je v současné době zakázána administrátorem. Přihlaste se nebo kontaktujte správce.",
|
||||
"userNotAllowed": "Váš účet nemá oprávnění k registraci. Obraťte se na správce.",
|
||||
"databaseConnected": "Databáze byla úspěšně připojena",
|
||||
"databaseConnectionFailed": "Nepodařilo se připojit k databázovému serveru",
|
||||
"checkServerConnection": "Zkontrolujte připojení k serveru a zkuste to znovu",
|
||||
@@ -2051,8 +2244,8 @@
|
||||
"fileColorCodingDesc": "Barevné soubory podle typu: složky (červené), soubory (modrá), symbolické odkazy (zelené)",
|
||||
"commandAutocomplete": "Automatické dokončování příkazu",
|
||||
"commandAutocompleteDesc": "Povolit návrhy automatického dokončování tlačítek záložek pro terminálové příkazy na základě vaší historie příkazů",
|
||||
"defaultSnippetFoldersCollapsed": "Sbalit složky snippet ve výchozím nastavení",
|
||||
"defaultSnippetFoldersCollapsedDesc": "Pokud je povoleno, všechny složky snippetů se sbalí, když otevřete záložku snippetů",
|
||||
"defaultSnippetFoldersCollapsed": "Sbalit složky úryvků ve výchozím nastavení",
|
||||
"defaultSnippetFoldersCollapsedDesc": "Když je povoleno, všechny složky úryvků se sbalí, když otevřete záložku úryvků",
|
||||
"terminalSyntaxHighlighting": "Zvýraznění syntaxe terminálu",
|
||||
"showHostTags": "Zobrazit štítky hostitele",
|
||||
"showHostTagsDesc": "Zobrazit štítky pod každým hostitelem v postranním panelu. Zakažte pro skrytí všech štítků.",
|
||||
@@ -2062,9 +2255,9 @@
|
||||
"fileManagerSettings": "Správce souborů",
|
||||
"terminalSettings": "Terminál",
|
||||
"hostSidebarSettings": "Hostitel & postranní panel",
|
||||
"snippetsSettings": "Výstřižky bloků",
|
||||
"confirmSnippetExecution": "Potvrdit spuštění snippetu",
|
||||
"confirmSnippetExecutionDesc": "Zobrazit potvrzovací dialog před spuštěním snippetů",
|
||||
"snippetsSettings": "Úryvky",
|
||||
"confirmSnippetExecution": "Potvrdit spuštění úryvku",
|
||||
"confirmSnippetExecutionDesc": "Zobrazit potvrzovací dialogové okno před spuštěním úryvků",
|
||||
"updateSettings": "Aktualizace",
|
||||
"disableUpdateCheck": "Zakázat kontrolu aktualizací",
|
||||
"disableUpdateCheckDesc": "Zastavit kontrolu nových verzí na startu a řídicím panelu. Sníží síťové požadavky.",
|
||||
@@ -2078,7 +2271,9 @@
|
||||
"appearanceDesc": "Vyberte barevný motiv pro aplikaci",
|
||||
"terminalSyntaxHighlightingDesc": "Automaticky zvýraznit příkazy, cesty, IP a logovací úrovně v terminálu",
|
||||
"enableCommandPaletteShortcut": "Povolit zástupce pro Palety",
|
||||
"enableCommandPaletteShortcutDesc": "Dvojitým poklepáním vlevo otevřít Příkazovou paletu pro rychlý přístup k hostům"
|
||||
"enableCommandPaletteShortcutDesc": "Dvojitým poklepáním vlevo otevřít Příkazovou paletu pro rychlý přístup k hostům",
|
||||
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
|
||||
"enableTerminalSessionPersistenceDesc": "Udržovat SSH připojení při přepínání karet nebo zavření prohlížeče (může být nestabilní)"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "Nepodařilo se načíst informace o verzi"
|
||||
@@ -2114,11 +2309,12 @@
|
||||
"usernameField": "jméno",
|
||||
"scopes": "openid e-mailový profil",
|
||||
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
|
||||
"allowedUsers": "user@example.com, @company.com",
|
||||
"enterUsername": "Zadejte uživatelské jméno pro administrátora",
|
||||
"searchHosts": "Hledat hostitele podle jména, uživatelského jména, IP složky, tagů...",
|
||||
"searchHosts": "Hledat hostitele podle názvu, uživatelského jména, IP, složky, štítků…",
|
||||
"enterPassword": "Zadejte své heslo",
|
||||
"totpCode": "6-digit TOTP code",
|
||||
"searchHostsAny": "Hledat hostitele (try: tag:prod, user:root, ip:192.168)...",
|
||||
"searchHostsAny": "Hledat hostitele (zkuste: tag:prod, user:root, ip:192.168)…",
|
||||
"confirmPassword": "Pro potvrzení zadejte své heslo",
|
||||
"typeHere": "Napište zde",
|
||||
"fileName": "Zadejte název souboru (např. příklad.txt)",
|
||||
@@ -2179,7 +2375,7 @@
|
||||
"clickToSelectFile": "Kliknutím vyberte soubor",
|
||||
"noSshHosts": "Žádné SSH hostitele",
|
||||
"sshHosts": "SSH hostitelé",
|
||||
"importSshHosts": "Importovat SSH hostitele z JSON",
|
||||
"importSshHosts": "Importovat hostitele SSH z JSON",
|
||||
"clientId": "ID klienta",
|
||||
"clientSecret": "Tajný klíč klienta",
|
||||
"error": "Chyba",
|
||||
@@ -2374,7 +2570,7 @@
|
||||
"sourceDifferentFromTarget": "Zdroj a cíl musí být jiný",
|
||||
"exportJSON": "Exportovat JSON",
|
||||
"importJSON": "Importovat JSON",
|
||||
"searchHost": "Hledat v hostech...",
|
||||
"searchHost": "Hledat hostitele…",
|
||||
"noHostFound": "Nenalezeny žádné hostitele",
|
||||
"searchGroup": "Hledat skupiny...",
|
||||
"noGroupFound": "Nenalezeny žádné skupiny",
|
||||
@@ -2549,7 +2745,7 @@
|
||||
"viewDesc": "Kvůli šifrovacímu systému Termix dojde později k dalším úrovním oprávnění"
|
||||
},
|
||||
"commandPalette": {
|
||||
"searchPlaceholder": "Hledat hostitele nebo rychlé akce...",
|
||||
"searchPlaceholder": "Hledat hostitele nebo rychlé akce…",
|
||||
"recentActivity": "Nedávná aktivita",
|
||||
"navigation": "Navigation",
|
||||
"addHost": "Přidat hostitele",
|
||||
@@ -2569,7 +2765,7 @@
|
||||
"press": "Tisk",
|
||||
"toToggle": "přepnout",
|
||||
"close": "Zavřít",
|
||||
"hostManager": "Manažer serveru",
|
||||
"hostManager": "Správce hostitelů",
|
||||
"pressToToggle": "Dvakrát stiskněte levé tlačítko Shift pro otevření palety příkazu"
|
||||
},
|
||||
"docker": {
|
||||
@@ -2598,7 +2794,7 @@
|
||||
"failedToRemoveContainer": "Odstranění kontejneru {{name}} se nezdařilo",
|
||||
"image": "Obrázek",
|
||||
"idLabel": "ID",
|
||||
"ports": "Přístavy",
|
||||
"ports": "Porty",
|
||||
"noPorts": "Žádné porty",
|
||||
"created": "Vytvořeno",
|
||||
"start": "Začít",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user