mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
feat: host manager bug fixes, i18n improvements, general tab fixes
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
[server]
|
|
||||||
bind_host = 127.0.0.1
|
|
||||||
bind_port = 4822
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Guacamole Proxy Daemon (guacd)
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/sbin/guacd -f -b 127.0.0.1 -l 4822
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
worker_processes auto;
|
|
||||||
pid /run/nginx.pid;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
client_header_timeout 300s;
|
|
||||||
|
|
||||||
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;
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_session_timeout 10m;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
add_header X-Content-Type-Options nosniff always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
|
|
||||||
location = /sw.js {
|
|
||||||
root /opt/termix/html;
|
|
||||||
expires off;
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /manifest.json {
|
|
||||||
root /opt/termix/html;
|
|
||||||
expires off;
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
root /opt/termix/html;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /opt/termix/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
expires off;
|
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.map$ {
|
|
||||||
return 404;
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/users/sessions(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/users(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $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 $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 ~ ^/releases(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/alerts(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/rbac(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/credentials(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/snippets(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/c2s-tunnel-presets(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/terminal(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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 ~ ^/database(/.*)?$ {
|
|
||||||
client_max_body_size 5G;
|
|
||||||
client_body_timeout 300s;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
|
|
||||||
proxy_request_buffering off;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/db(/.*)?$ {
|
|
||||||
client_max_body_size 5G;
|
|
||||||
client_body_timeout 300s;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
|
|
||||||
proxy_request_buffering off;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/encryption(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /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 $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 ~ ^/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 $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 $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 ~ ^/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 $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 $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 /host/ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /ssh/websocket/ {
|
|
||||||
proxy_pass http://127.0.0.1:30002/;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-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_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/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 $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/tunnel/ {
|
|
||||||
proxy_pass http://127.0.0.1:30003;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_read_timeout 86400s;
|
|
||||||
proxy_send_timeout 86400s;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /host/file_manager/recent {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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 /host/file_manager/pinned {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /host/file_manager/shortcuts {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /host/file_manager/sudo-password {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /ssh/file_manager/ {
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
|
|
||||||
proxy_request_buffering off;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/network-topology(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /health {
|
|
||||||
proxy_pass http://127.0.0.1:30001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/status(/.*)?$ {
|
|
||||||
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 ~ ^/metrics(/.*)?$ {
|
|
||||||
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;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/(refresh|host-updated)$ {
|
|
||||||
proxy_pass http://127.0.0.1:30005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/global-settings(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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 $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 ~ ^/activity(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30006;
|
|
||||||
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 ~ ^/dashboard/preferences(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30006;
|
|
||||||
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 ^~ /docker/console/ {
|
|
||||||
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 $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 ~ ^/docker(/.*)?$ {
|
|
||||||
proxy_pass http://127.0.0.1:30007;
|
|
||||||
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;
|
|
||||||
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /opt/termix/html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
NODE_ENV=production
|
|
||||||
DATA_DIR=/opt/termix/data
|
|
||||||
GUACD_HOST=127.0.0.1
|
|
||||||
GUACD_PORT=4822
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Termix Backend
|
|
||||||
After=network.target guacd.service
|
|
||||||
Wants=guacd.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
WorkingDirectory=/opt/termix
|
|
||||||
EnvironmentFile=/opt/termix/.env
|
|
||||||
ExecStart=/usr/bin/node /opt/termix/dist/backend/backend/starter.js
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
Generated
+103
-85
@@ -138,7 +138,7 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.13",
|
||||||
"vite-plugin-svgr": "^5.2.0",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
@@ -3735,9 +3735,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.127.0",
|
"version": "0.130.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
|
||||||
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5678,9 +5678,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
|
||||||
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5695,9 +5695,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
|
||||||
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5712,9 +5712,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
|
||||||
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5729,9 +5729,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
|
||||||
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5746,9 +5746,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
|
||||||
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -5763,13 +5763,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
|
||||||
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5780,13 +5783,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
|
||||||
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5797,13 +5803,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
|
||||||
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5814,13 +5823,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
|
||||||
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5831,13 +5843,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
|
||||||
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5848,13 +5863,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
|
||||||
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5865,9 +5883,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
|
||||||
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5882,9 +5900,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
|
||||||
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -5901,9 +5919,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
|
||||||
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5918,9 +5936,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
|
||||||
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -15321,9 +15339,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -15341,7 +15359,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -15364,9 +15382,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss/node_modules/nanoid": {
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -16737,14 +16755,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.127.0",
|
"@oxc-project/types": "=0.130.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -16753,27 +16771,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-android-arm64": "1.0.1",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-darwin-arm64": "1.0.1",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
"@rolldown/binding-darwin-x64": "1.0.1",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
"@rolldown/binding-freebsd-x64": "1.0.1",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
"@rolldown/binding-linux-arm64-musl": "1.0.1",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
"@rolldown/binding-linux-x64-gnu": "1.0.1",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
"@rolldown/binding-linux-x64-musl": "1.0.1",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
"@rolldown/binding-openharmony-arm64": "1.0.1",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
"@rolldown/binding-wasm32-wasi": "1.0.1",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
"@rolldown/binding-win32-x64-msvc": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -18507,16 +18525,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.10",
|
"version": "8.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.14",
|
||||||
"rolldown": "1.0.0-rc.17",
|
"rolldown": "1.0.1",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -18533,7 +18551,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"@vitejs/devtools": "^0.1.0",
|
"@vitejs/devtools": "^0.1.18",
|
||||||
"esbuild": "^0.27.0 || ^0.28.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
+1
-1
@@ -166,7 +166,7 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.13",
|
||||||
"vite-plugin-svgr": "^5.2.0",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -671,6 +671,8 @@ router.post(
|
|||||||
authType ||
|
authType ||
|
||||||
authMethod ||
|
authMethod ||
|
||||||
(effectiveConnectionType !== "ssh" ? "password" : undefined);
|
(effectiveConnectionType !== "ssh" ? "password" : undefined);
|
||||||
|
const effectiveUsername =
|
||||||
|
username || rdpUser || vncUser || telnetUser || "";
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
connectionType: effectiveConnectionType,
|
connectionType: effectiveConnectionType,
|
||||||
@@ -679,7 +681,7 @@ router.post(
|
|||||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||||
ip,
|
ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username: effectiveUsername,
|
||||||
authType: effectiveAuthType,
|
authType: effectiveAuthType,
|
||||||
credentialId: credentialId || null,
|
credentialId: credentialId || null,
|
||||||
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||||
@@ -1203,6 +1205,8 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveAuthType = authType || authMethod;
|
const effectiveAuthType = authType || authMethod;
|
||||||
|
const effectiveUsername =
|
||||||
|
username || rdpUser || vncUser || telnetUser || "";
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
connectionType: connectionType || "ssh",
|
connectionType: connectionType || "ssh",
|
||||||
name,
|
name,
|
||||||
@@ -1210,7 +1214,7 @@ router.put(
|
|||||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||||
ip,
|
ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username: effectiveUsername,
|
||||||
authType: effectiveAuthType,
|
authType: effectiveAuthType,
|
||||||
credentialId: credentialId || null,
|
credentialId: credentialId || null,
|
||||||
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||||
@@ -3817,6 +3821,10 @@ router.post(
|
|||||||
overrideCredentialUsername: hostData.overrideCredentialUsername
|
overrideCredentialUsername: hostData.overrideCredentialUsername
|
||||||
? 1
|
? 1
|
||||||
: 0,
|
: 0,
|
||||||
|
enableSsh: hostData.enableSsh ?? false,
|
||||||
|
enableRdp: hostData.enableRdp ?? false,
|
||||||
|
enableVnc: hostData.enableVnc ?? false,
|
||||||
|
enableTelnet: hostData.enableTelnet ?? false,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import express from "express";
|
|
||||||
import { GuacamoleTokenService } from "./token-service.js";
|
import { GuacamoleTokenService } from "./token-service.js";
|
||||||
import { guacLogger } from "../utils/logger.js";
|
import { guacLogger } from "../utils/logger.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
@@ -197,7 +196,8 @@ router.post(
|
|||||||
|
|
||||||
if (guacConfig.dpi != null) {
|
if (guacConfig.dpi != null) {
|
||||||
const parsed = parseInt(String(guacConfig.dpi), 10);
|
const parsed = parseInt(String(guacConfig.dpi), 10);
|
||||||
guacConfig.dpi = Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
guacConfig.dpi =
|
||||||
|
Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let token: string;
|
let token: string;
|
||||||
@@ -222,61 +222,57 @@ router.post(
|
|||||||
|
|
||||||
if (jumpHosts.length > 0) {
|
if (jumpHosts.length > 0) {
|
||||||
try {
|
try {
|
||||||
const { resolveHostById } = await import(
|
const { resolveHostById } = await import("../ssh/host-resolver.js");
|
||||||
"../ssh/host-resolver.js"
|
const jumpHost = await resolveHostById(jumpHosts[0].hostId, userId);
|
||||||
);
|
|
||||||
const jumpHost = await resolveHostById(
|
|
||||||
jumpHosts[0].hostId,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
if (jumpHost) {
|
if (jumpHost) {
|
||||||
const tunnelPort = await new Promise<number>(
|
const tunnelPort = await new Promise<number>((resolve, reject) => {
|
||||||
(resolve, reject) => {
|
const sshClient = new Client();
|
||||||
const sshClient = new Client();
|
sshClient.on("ready", () => {
|
||||||
sshClient.on("ready", () => {
|
const server = net.createServer((sock) => {
|
||||||
const server = net.createServer((sock) => {
|
sshClient.forwardOut(
|
||||||
sshClient.forwardOut(
|
"127.0.0.1",
|
||||||
"127.0.0.1",
|
0,
|
||||||
0,
|
hostname,
|
||||||
hostname,
|
port,
|
||||||
port,
|
(err, stream) => {
|
||||||
(err, stream) => {
|
if (err) {
|
||||||
if (err) {
|
sock.destroy();
|
||||||
sock.destroy();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
sock.pipe(stream).pipe(sock);
|
||||||
sock.pipe(stream).pipe(sock);
|
},
|
||||||
},
|
);
|
||||||
);
|
});
|
||||||
});
|
server.listen(0, "127.0.0.1", () => {
|
||||||
server.listen(0, "127.0.0.1", () => {
|
const addr = server.address() as net.AddressInfo;
|
||||||
const addr = server.address() as net.AddressInfo;
|
// Auto-cleanup after 1 hour
|
||||||
// Auto-cleanup after 1 hour
|
setTimeout(
|
||||||
setTimeout(() => {
|
() => {
|
||||||
server.close();
|
server.close();
|
||||||
sshClient.end();
|
sshClient.end();
|
||||||
}, 60 * 60 * 1000);
|
},
|
||||||
resolve(addr.port);
|
60 * 60 * 1000,
|
||||||
});
|
);
|
||||||
|
resolve(addr.port);
|
||||||
});
|
});
|
||||||
sshClient.on("error", reject);
|
});
|
||||||
|
sshClient.on("error", reject);
|
||||||
|
|
||||||
const connectOpts: Record<string, unknown> = {
|
const connectOpts: Record<string, unknown> = {
|
||||||
host: jumpHost.ip,
|
host: jumpHost.ip,
|
||||||
port: jumpHost.port || 22,
|
port: jumpHost.port || 22,
|
||||||
username: jumpHost.username,
|
username: jumpHost.username,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
};
|
};
|
||||||
if (jumpHost.key) {
|
if (jumpHost.key) {
|
||||||
connectOpts.privateKey = jumpHost.key;
|
connectOpts.privateKey = jumpHost.key;
|
||||||
if (jumpHost.keyPassword)
|
if (jumpHost.keyPassword)
|
||||||
connectOpts.passphrase = jumpHost.keyPassword;
|
connectOpts.passphrase = jumpHost.keyPassword;
|
||||||
} else if (jumpHost.password) {
|
} else if (jumpHost.password) {
|
||||||
connectOpts.password = jumpHost.password;
|
connectOpts.password = jumpHost.password;
|
||||||
}
|
}
|
||||||
sshClient.connect(connectOpts);
|
sshClient.connect(connectOpts);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
hostname = "127.0.0.1";
|
hostname = "127.0.0.1";
|
||||||
port = tunnelPort;
|
port = tunnelPort;
|
||||||
guacLogger.info("SSH tunnel established for guacamole", {
|
guacLogger.info("SSH tunnel established for guacamole", {
|
||||||
|
|||||||
@@ -151,10 +151,7 @@ async function resolveJumpHost(
|
|||||||
): Promise<JumpHostConfig | null> {
|
): Promise<JumpHostConfig | null> {
|
||||||
try {
|
try {
|
||||||
const hostResults = await SimpleDBOps.select(
|
const hostResults = await SimpleDBOps.select(
|
||||||
getDb()
|
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||||
.select()
|
|
||||||
.from(hosts)
|
|
||||||
.where(eq(hosts.id, hostId)),
|
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
@@ -172,8 +169,10 @@ async function resolveJumpHost(
|
|||||||
const { SharedCredentialManager } =
|
const { SharedCredentialManager } =
|
||||||
await import("../utils/shared-credential-manager.js");
|
await import("../utils/shared-credential-manager.js");
|
||||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||||
const sharedCred =
|
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
||||||
await sharedCredManager.getSharedCredentialForUser(hostId, userId);
|
hostId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
if (sharedCred) {
|
if (sharedCred) {
|
||||||
return {
|
return {
|
||||||
...host,
|
...host,
|
||||||
@@ -889,7 +888,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Resolve credentials server-side when frontend doesn't provide them
|
// Resolve credentials server-side when frontend doesn't provide them
|
||||||
let resolvedCredentials = { password, sshKey, keyPassword, authType, sudoPassword: undefined as string | undefined };
|
let resolvedCredentials = {
|
||||||
|
password,
|
||||||
|
sshKey,
|
||||||
|
keyPassword,
|
||||||
|
authType,
|
||||||
|
sudoPassword: undefined as string | undefined,
|
||||||
|
};
|
||||||
if (hostId && userId && !password && !sshKey) {
|
if (hostId && userId && !password && !sshKey) {
|
||||||
try {
|
try {
|
||||||
const { resolveHostById } = await import("./host-resolver.js");
|
const { resolveHostById } = await import("./host-resolver.js");
|
||||||
@@ -3088,47 +3093,36 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|||||||
|
|
||||||
stream.on("close", (code) => {
|
stream.on("close", (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
const isPermissionDenied =
|
const isPermissionDenied = errorData
|
||||||
errorData.toLowerCase().includes("permission denied");
|
.toLowerCase()
|
||||||
|
.includes("permission denied");
|
||||||
|
|
||||||
if (isPermissionDenied && sshConn.sudoPassword) {
|
if (isPermissionDenied && sshConn.sudoPassword) {
|
||||||
execWithSudo(
|
execWithSudo(
|
||||||
sshConn,
|
sshConn,
|
||||||
`cat '${escapedPath}'`,
|
`cat '${escapedPath}'`,
|
||||||
sshConn.sudoPassword,
|
sshConn.sudoPassword,
|
||||||
(sudoErr, sudoStream) => {
|
)
|
||||||
if (sudoErr) {
|
.then(({ stdout, stderr, code: sudoCode }) => {
|
||||||
|
if (sudoCode !== 0) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
error: "Permission denied",
|
error: `Permission denied: ${stderr}`,
|
||||||
needsSudo: true,
|
needsSudo: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let sudoData = Buffer.alloc(0);
|
const sudoData = Buffer.from(stdout, "utf8");
|
||||||
let sudoErrorData = "";
|
const isBinary = detectBinary(sudoData);
|
||||||
sudoStream.on("data", (chunk: Buffer) => {
|
res.json({
|
||||||
sudoData = Buffer.concat([sudoData, chunk]);
|
content: isBinary ? sudoData.toString("base64") : stdout,
|
||||||
|
isBinary,
|
||||||
|
size: sudoData.length,
|
||||||
});
|
});
|
||||||
sudoStream.stderr.on("data", (chunk: Buffer) => {
|
})
|
||||||
sudoErrorData += chunk.toString();
|
.catch(() => {
|
||||||
});
|
res
|
||||||
sudoStream.on("close", (sudoCode) => {
|
.status(403)
|
||||||
if (sudoCode !== 0) {
|
.json({ error: "Permission denied", needsSudo: true });
|
||||||
return res.status(403).json({
|
});
|
||||||
error: `Permission denied: ${sudoErrorData}`,
|
|
||||||
needsSudo: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const isBinary = detectBinary(sudoData);
|
|
||||||
res.json({
|
|
||||||
content: isBinary
|
|
||||||
? sudoData.toString("base64")
|
|
||||||
: sudoData.toString("utf8"),
|
|
||||||
isBinary,
|
|
||||||
size: sudoData.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3564,30 +3558,38 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const isPermDenied = errorData.toLowerCase().includes("permission denied");
|
const isPermDenied = errorData
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("permission denied");
|
||||||
if (isPermDenied && sshConn.sudoPassword) {
|
if (isPermDenied && sshConn.sudoPassword) {
|
||||||
const sudoWriteCmd = `echo '${base64Content}' | base64 -d | sudo tee '${escapedPath}' > /dev/null && echo "SUCCESS"`;
|
execWithSudo(
|
||||||
execWithSudo(sshConn, `bash -c "echo '${base64Content}' | base64 -d > '${escapedPath}' && echo SUCCESS"`, sshConn.sudoPassword, (sudoErr, sudoStream) => {
|
sshConn,
|
||||||
if (sudoErr) {
|
`bash -c "echo '${base64Content}' | base64 -d > '${escapedPath}' && echo SUCCESS"`,
|
||||||
if (!res.headersSent) {
|
sshConn.sudoPassword,
|
||||||
res.status(403).json({ error: "Permission denied", needsSudo: true });
|
)
|
||||||
}
|
.then(({ stdout, code: sudoCode }) => {
|
||||||
return;
|
if (sudoCode === 0 && stdout.includes("SUCCESS")) {
|
||||||
}
|
|
||||||
let sudoOut = "";
|
|
||||||
sudoStream.on("data", (chunk: Buffer) => { sudoOut += chunk.toString(); });
|
|
||||||
sudoStream.on("close", () => {
|
|
||||||
if (sudoOut.includes("SUCCESS")) {
|
|
||||||
restoreOriginalMode(null, () => {
|
restoreOriginalMode(null, () => {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({ message: "File written successfully", path: filePath });
|
res.json({
|
||||||
|
message: "File written successfully",
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (!res.headersSent) {
|
} else if (!res.headersSent) {
|
||||||
res.status(403).json({ error: "Permission denied", needsSudo: true });
|
res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Permission denied", needsSudo: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Permission denied", needsSudo: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fileLogger.error(
|
fileLogger.error(
|
||||||
@@ -4981,7 +4983,9 @@ app.post("/ssh/file_manager/ssh/downloadFileStream", async (req, res) => {
|
|||||||
|
|
||||||
const sshConn = sshSessions[sessionId];
|
const sshConn = sshSessions[sessionId];
|
||||||
if (!sshConn?.isConnected) {
|
if (!sshConn?.isConnected) {
|
||||||
return res.status(400).json({ error: "SSH session not found or not connected" });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "SSH session not found or not connected" });
|
||||||
}
|
}
|
||||||
if (!verifySessionOwnership(sshConn, userId)) {
|
if (!verifySessionOwnership(sshConn, userId)) {
|
||||||
return res.status(403).json({ error: "Session access denied" });
|
return res.status(403).json({ error: "Session access denied" });
|
||||||
@@ -4991,9 +4995,11 @@ app.post("/ssh/file_manager/ssh/downloadFileStream", async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const sftp = await getSessionSftp(sshConn);
|
const sftp = await getSessionSftp(sshConn);
|
||||||
const stats = await new Promise<{ size: number; isFile: () => boolean }>((resolve, reject) => {
|
const stats = await new Promise<{ size: number; isFile: () => boolean }>(
|
||||||
sftp.stat(filePath, (err, s) => (err ? reject(err) : resolve(s)));
|
(resolve, reject) => {
|
||||||
});
|
sftp.stat(filePath, (err, s) => (err ? reject(err) : resolve(s)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!stats.isFile()) {
|
if (!stats.isFile()) {
|
||||||
return res.status(400).json({ error: "Cannot download directories" });
|
return res.status(400).json({ error: "Cannot download directories" });
|
||||||
@@ -5001,7 +5007,10 @@ app.post("/ssh/file_manager/ssh/downloadFileStream", async (req, res) => {
|
|||||||
|
|
||||||
const fileName = filePath.split("/").pop() || "download";
|
const fileName = filePath.split("/").pop() || "download";
|
||||||
res.setHeader("Content-Type", "application/octet-stream");
|
res.setHeader("Content-Type", "application/octet-stream");
|
||||||
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(fileName)}"`);
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="${encodeURIComponent(fileName)}"`,
|
||||||
|
);
|
||||||
res.setHeader("Content-Length", String(stats.size));
|
res.setHeader("Content-Length", String(stats.size));
|
||||||
|
|
||||||
const readStream = sftp.createReadStream(filePath);
|
const readStream = sftp.createReadStream(filePath);
|
||||||
@@ -5015,7 +5024,9 @@ app.post("/ssh/file_manager/ssh/downloadFileStream", async (req, res) => {
|
|||||||
readStream.pipe(res);
|
readStream.pipe(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({ error: `Download failed: ${(err as Error).message}` });
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: `Download failed: ${(err as Error).message}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+35
-28
@@ -1,14 +1,14 @@
|
|||||||
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
||||||
import {
|
import ssh2pkg from "ssh2";
|
||||||
Client,
|
const { Client, BaseAgent, utils: ssh2Utils } = ssh2pkg;
|
||||||
type ClientChannel,
|
import type {
|
||||||
type PseudoTtyOptions,
|
Client as ClientType,
|
||||||
BaseAgent,
|
ClientChannel,
|
||||||
utils as ssh2Utils,
|
PseudoTtyOptions,
|
||||||
type ParsedKey,
|
ParsedKey,
|
||||||
type SignCallback,
|
SignCallback,
|
||||||
type SigningRequestOptions,
|
SigningRequestOptions,
|
||||||
type IdentityCallback,
|
IdentityCallback,
|
||||||
} from "ssh2";
|
} from "ssh2";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
import dgram from "dgram";
|
import dgram from "dgram";
|
||||||
@@ -54,8 +54,7 @@ class MemoryAgent extends BaseAgent {
|
|||||||
cb?: SignCallback,
|
cb?: SignCallback,
|
||||||
): void {
|
): void {
|
||||||
const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb!;
|
const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb!;
|
||||||
const options =
|
const options = typeof optionsOrCb === "function" ? {} : optionsOrCb;
|
||||||
typeof optionsOrCb === "function" ? {} : optionsOrCb;
|
|
||||||
try {
|
try {
|
||||||
const algo =
|
const algo =
|
||||||
options.hash === "sha256"
|
options.hash === "sha256"
|
||||||
@@ -191,10 +190,7 @@ async function resolveJumpHost(
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const hostResults = await SimpleDBOps.select(
|
const hostResults = await SimpleDBOps.select(
|
||||||
getDb()
|
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||||
.select()
|
|
||||||
.from(hosts)
|
|
||||||
.where(eq(hosts.id, hostId)),
|
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
@@ -212,8 +208,10 @@ async function resolveJumpHost(
|
|||||||
const { SharedCredentialManager } =
|
const { SharedCredentialManager } =
|
||||||
await import("../utils/shared-credential-manager.js");
|
await import("../utils/shared-credential-manager.js");
|
||||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||||
const sharedCred =
|
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
||||||
await sharedCredManager.getSharedCredentialForUser(hostId, userId);
|
hostId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
if (sharedCred) {
|
if (sharedCred) {
|
||||||
return {
|
return {
|
||||||
...host,
|
...host,
|
||||||
@@ -275,13 +273,13 @@ async function createJumpHostChain(
|
|||||||
jumpHosts: Array<{ hostId: number }>,
|
jumpHosts: Array<{ hostId: number }>,
|
||||||
userId: string,
|
userId: string,
|
||||||
socks5Config?: SOCKS5Config | null,
|
socks5Config?: SOCKS5Config | null,
|
||||||
): Promise<Client | null> {
|
): Promise<ClientType | null> {
|
||||||
if (!jumpHosts || jumpHosts.length === 0) {
|
if (!jumpHosts || jumpHosts.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentClient: Client | null = null;
|
let currentClient: ClientType | null = null;
|
||||||
const clients: Client[] = [];
|
const clients: ClientType[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jumpHostConfigs = await Promise.all(
|
const jumpHostConfigs = await Promise.all(
|
||||||
@@ -562,9 +560,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let currentSessionId: string | null = null;
|
let currentSessionId: string | null = null;
|
||||||
let sshConn: Client | null = null;
|
let sshConn: ClientType | null = null;
|
||||||
let sshStream: ClientChannel | null = null;
|
let sshStream: ClientChannel | null = null;
|
||||||
let lastJumpClient: Client | null = null;
|
let lastJumpClient: ClientType | null = null;
|
||||||
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
|
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
|
||||||
let totpPromptSent = false;
|
let totpPromptSent = false;
|
||||||
let totpTimeout: NodeJS.Timeout | null = null;
|
let totpTimeout: NodeJS.Timeout | null = null;
|
||||||
@@ -938,7 +936,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sessionName: tmuxName,
|
sessionName: tmuxName,
|
||||||
hostId: session.hostId,
|
hostId: session.hostId,
|
||||||
});
|
});
|
||||||
ws.send(JSON.stringify({ type: "tmux_detached", sessionName: tmuxName }));
|
ws.send(
|
||||||
|
JSON.stringify({ type: "tmux_detached", sessionName: tmuxName }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2468,7 +2468,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
hostConfig.userId;
|
hostConfig.userId;
|
||||||
|
|
||||||
// Cloudflare Tunnel: connect via WebSocket proxy
|
// Cloudflare Tunnel: connect via WebSocket proxy
|
||||||
const cfConfig = hostConfig.terminalConfig as Record<string, unknown> | undefined;
|
const cfConfig = hostConfig.terminalConfig as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
if (cfConfig?.cfAccessClientId && cfConfig?.cfAccessClientSecret) {
|
if (cfConfig?.cfAccessClientId && cfConfig?.cfAccessClientSecret) {
|
||||||
try {
|
try {
|
||||||
const WebSocket = (await import("ws")).default;
|
const WebSocket = (await import("ws")).default;
|
||||||
@@ -2484,7 +2486,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
cfWs.on("open", () => resolve());
|
cfWs.on("open", () => resolve());
|
||||||
cfWs.on("error", (err) => reject(err));
|
cfWs.on("error", (err) => reject(err));
|
||||||
setTimeout(() => reject(new Error("Cloudflare tunnel timeout")), 30000);
|
setTimeout(
|
||||||
|
() => reject(new Error("Cloudflare tunnel timeout")),
|
||||||
|
30000,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Duplex } = await import("stream");
|
const { Duplex } = await import("stream");
|
||||||
@@ -2497,7 +2502,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
cfWs.on("message", (data) => duplexStream.push(data));
|
cfWs.on("message", (data) => duplexStream.push(data));
|
||||||
cfWs.on("close", () => duplexStream.push(null));
|
cfWs.on("close", () => duplexStream.push(null));
|
||||||
|
|
||||||
connectConfig.sock = duplexStream as unknown as typeof connectConfig.sock;
|
connectConfig.sock =
|
||||||
|
duplexStream as unknown as typeof connectConfig.sock;
|
||||||
sendLog("handshake", "info", "Connected via Cloudflare Tunnel");
|
sendLog("handshake", "info", "Connected via Cloudflare Tunnel");
|
||||||
} catch (cfError) {
|
} catch (cfError) {
|
||||||
sshLogger.error("Cloudflare tunnel connection failed", cfError, {
|
sshLogger.error("Cloudflare tunnel connection failed", cfError, {
|
||||||
@@ -2507,7 +2513,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Cloudflare tunnel connection failed: " +
|
message:
|
||||||
|
"Cloudflare tunnel connection failed: " +
|
||||||
(cfError instanceof Error ? cfError.message : "Unknown error"),
|
(cfError instanceof Error ? cfError.message : "Unknown error"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type Host = {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
authType: "password" | "key" | "credential" | "none" | "opkssh";
|
authType: "password" | "key" | "credential" | "none" | "opkssh";
|
||||||
credentialId?: string;
|
credentialId?: string;
|
||||||
|
overrideCredentialUsername?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
@@ -45,6 +46,7 @@ export type Host = {
|
|||||||
keepaliveInterval?: number;
|
keepaliveInterval?: number;
|
||||||
keepaliveCountMax?: number;
|
keepaliveCountMax?: number;
|
||||||
environmentVariables: { key: string; value: string }[];
|
environmentVariables: { key: string; value: string }[];
|
||||||
|
startupSnippetId?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
useSocks5?: boolean;
|
useSocks5?: boolean;
|
||||||
@@ -55,7 +57,7 @@ export type Host = {
|
|||||||
socks5ProxyChain?: {
|
socks5ProxyChain?: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
type: 4 | 5 | "http";
|
type: 4 | 5 | "http" | string;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
+23
-20
@@ -27,7 +27,7 @@ import type {
|
|||||||
SplitMode,
|
SplitMode,
|
||||||
HostFolder,
|
HostFolder,
|
||||||
} from "@/types/ui-types";
|
} from "@/types/ui-types";
|
||||||
import { getSSHHosts } from "@/main-axios";
|
import { getSSHHosts, getUserInfo } from "@/main-axios";
|
||||||
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
||||||
import type { SSHHostWithStatus } from "@/main-axios";
|
import type { SSHHostWithStatus } from "@/main-axios";
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
|||||||
enableTunnel: h.enableTunnel ?? false,
|
enableTunnel: h.enableTunnel ?? false,
|
||||||
enableFileManager: h.enableFileManager ?? false,
|
enableFileManager: h.enableFileManager ?? false,
|
||||||
enableDocker: h.enableDocker ?? false,
|
enableDocker: h.enableDocker ?? false,
|
||||||
enableRdp: h.connectionType === "rdp",
|
enableRdp: h.enableRdp ?? h.connectionType === "rdp",
|
||||||
enableVnc: h.connectionType === "vnc",
|
enableVnc: h.enableVnc ?? h.connectionType === "vnc",
|
||||||
enableTelnet: h.connectionType === "telnet",
|
enableTelnet: h.enableTelnet ?? h.connectionType === "telnet",
|
||||||
sshPort: h.port,
|
sshPort: h.port,
|
||||||
rdpPort: 3389,
|
rdpPort: 3389,
|
||||||
vncPort: 5900,
|
vncPort: 5900,
|
||||||
@@ -112,19 +112,6 @@ function buildHostTree(hosts: SSHHostWithStatus[]): HostFolder {
|
|||||||
export { tabIcon, renderTabContent } from "@/shell/tabUtils";
|
export { tabIcon, renderTabContent } from "@/shell/tabUtils";
|
||||||
import { renderTabContent } from "@/shell/tabUtils";
|
import { renderTabContent } from "@/shell/tabUtils";
|
||||||
|
|
||||||
const DASHBOARD_TAB: Tab = {
|
|
||||||
id: "dashboard",
|
|
||||||
type: "dashboard",
|
|
||||||
label: "Dashboard",
|
|
||||||
};
|
|
||||||
|
|
||||||
const SINGLETON_TAB_LABELS: Partial<Record<TabType, string>> = {
|
|
||||||
"host-manager": "Host Manager",
|
|
||||||
docker: "Docker",
|
|
||||||
tunnel: "Tunnels",
|
|
||||||
network_graph: "Network Graph",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── AppShell ────────────────────────────────────────────────────────────────
|
// ─── AppShell ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function AppShell({
|
export function AppShell({
|
||||||
@@ -135,7 +122,9 @@ export function AppShell({
|
|||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [tabs, setTabs] = useState<Tab[]>([DASHBOARD_TAB]);
|
const [tabs, setTabs] = useState<Tab[]>([
|
||||||
|
{ id: "dashboard", type: "dashboard", label: t("nav.dashboard") },
|
||||||
|
]);
|
||||||
const [activeTabId, setActiveTabId] = useState("dashboard");
|
const [activeTabId, setActiveTabId] = useState("dashboard");
|
||||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
||||||
const [splitMode, setSplitMode] = useState<SplitMode>("none");
|
const [splitMode, setSplitMode] = useState<SplitMode>("none");
|
||||||
@@ -144,6 +133,7 @@ export function AppShell({
|
|||||||
);
|
);
|
||||||
const [realHostTree, setRealHostTree] = useState<HostFolder | null>(null);
|
const [realHostTree, setRealHostTree] = useState<HostFolder | null>(null);
|
||||||
const [allHosts, setAllHosts] = useState<Host[]>([]);
|
const [allHosts, setAllHosts] = useState<Host[]>([]);
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [railView, setRailView] = useState<RailView>("hosts");
|
const [railView, setRailView] = useState<RailView>("hosts");
|
||||||
@@ -159,6 +149,12 @@ export function AppShell({
|
|||||||
if (isMobile) setSidebarOpen(false);
|
if (isMobile) setSidebarOpen(false);
|
||||||
}, [isMobile]);
|
}, [isMobile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserInfo()
|
||||||
|
.then((info) => setIsAdmin(info.is_admin))
|
||||||
|
.catch(() => setIsAdmin(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const pendingHostManagerEditId = useRef<string | null>(null);
|
const pendingHostManagerEditId = useRef<string | null>(null);
|
||||||
const pendingHostManagerAction = useRef<"add-host" | "add-credential" | null>(
|
const pendingHostManagerAction = useRef<"add-host" | "add-credential" | null>(
|
||||||
null,
|
null,
|
||||||
@@ -378,7 +374,13 @@ export function AppShell({
|
|||||||
const id = type;
|
const id = type;
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
if (prev.find((t) => t.id === id)) return prev;
|
if (prev.find((t) => t.id === id)) return prev;
|
||||||
return [...prev, { id, type, label: SINGLETON_TAB_LABELS[type] ?? type }];
|
const singletonLabels: Partial<Record<TabType, string>> = {
|
||||||
|
"host-manager": t("nav.hostManager"),
|
||||||
|
docker: t("nav.docker"),
|
||||||
|
tunnel: t("nav.tunnels"),
|
||||||
|
network_graph: t("nav.networkGraph"),
|
||||||
|
};
|
||||||
|
return [...prev, { id, type, label: singletonLabels[type] ?? type }];
|
||||||
});
|
});
|
||||||
setActiveTabId(id);
|
setActiveTabId(id);
|
||||||
}
|
}
|
||||||
@@ -530,7 +532,7 @@ export function AppShell({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{railView === "admin-settings" && (
|
{railView === "admin-settings" && isAdmin && (
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
<AdminSettingsPanel />
|
<AdminSettingsPanel />
|
||||||
</div>
|
</div>
|
||||||
@@ -582,6 +584,7 @@ export function AppShell({
|
|||||||
sidebarOpen={sidebarOpen}
|
sidebarOpen={sidebarOpen}
|
||||||
splitMode={splitMode}
|
splitMode={splitMode}
|
||||||
username={username}
|
username={username}
|
||||||
|
isAdmin={isAdmin}
|
||||||
profileDropdownOpen={profileDropdownOpen}
|
profileDropdownOpen={profileDropdownOpen}
|
||||||
onProfileDropdownChange={setProfileDropdownOpen}
|
onProfileDropdownChange={setProfileDropdownOpen}
|
||||||
onRailClick={handleRailClick}
|
onRailClick={handleRailClick}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
type === "number" &&
|
||||||
|
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ import {
|
|||||||
listSSHFiles,
|
listSSHFiles,
|
||||||
resolveSSHPath,
|
resolveSSHPath,
|
||||||
uploadSSHFile,
|
uploadSSHFile,
|
||||||
downloadSSHFile,
|
|
||||||
createSSHFile,
|
createSSHFile,
|
||||||
createSSHFolder,
|
createSSHFolder,
|
||||||
deleteSSHItem,
|
deleteSSHItem,
|
||||||
@@ -410,6 +409,17 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
async function initializeSSHConnection() {
|
async function initializeSSHConnection() {
|
||||||
if (!currentHost || isConnectingRef.current) return;
|
if (!currentHost || isConnectingRef.current) return;
|
||||||
|
|
||||||
|
if (currentHost.enableSsh === false) {
|
||||||
|
setHasConnectionError(true);
|
||||||
|
addLog({
|
||||||
|
type: "error",
|
||||||
|
message: t("fileManager.sshRequiredForFileManager"),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isConnectingRef.current = true;
|
isConnectingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -813,9 +823,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
);
|
);
|
||||||
files.push({ file, relativePath: path });
|
files.push({ file, relativePath: path });
|
||||||
} else if (entry.isDirectory) {
|
} else if (entry.isDirectory) {
|
||||||
const reader = (
|
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||||
entry as FileSystemDirectoryEntry
|
|
||||||
).createReader();
|
|
||||||
const dirEntries = await new Promise<FileSystemEntry[]>(
|
const dirEntries = await new Promise<FileSystemEntry[]>(
|
||||||
(resolve, reject) => reader.readEntries(resolve, reject),
|
(resolve, reject) => reader.readEntries(resolve, reject),
|
||||||
);
|
);
|
||||||
|
|||||||
+52
-1672
File diff suppressed because it is too large
Load Diff
+35
-22
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Hammer,
|
Hammer,
|
||||||
@@ -35,22 +36,29 @@ type RailItem =
|
|||||||
}
|
}
|
||||||
| { kind: "separator" };
|
| { kind: "separator" };
|
||||||
|
|
||||||
function buildRailButtons(splitMode: SplitMode): RailItem[] {
|
function buildRailButtons(
|
||||||
|
splitMode: SplitMode,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): RailItem[] {
|
||||||
return [
|
return [
|
||||||
{ view: "hosts", icon: <Server size={16} />, title: "Hosts" },
|
{ view: "hosts", icon: <Server size={16} />, title: t("nav.hosts") },
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{ view: "quick-connect", icon: <Zap size={16} />, title: "Quick Connect" },
|
{
|
||||||
|
view: "quick-connect",
|
||||||
|
icon: <Zap size={16} />,
|
||||||
|
title: t("nav.quickConnect"),
|
||||||
|
},
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{ view: "ssh-tools", icon: <Hammer size={16} />, title: "SSH Tools" },
|
{ view: "ssh-tools", icon: <Hammer size={16} />, title: t("nav.sshTools") },
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{ view: "snippets", icon: <Play size={16} />, title: "Snippets" },
|
{ view: "snippets", icon: <Play size={16} />, title: t("nav.snippets") },
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{ view: "history", icon: <Clock size={16} />, title: "History" },
|
{ view: "history", icon: <Clock size={16} />, title: t("nav.history") },
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{
|
{
|
||||||
view: "split-screen",
|
view: "split-screen",
|
||||||
icon: <LayoutPanelLeft size={16} />,
|
icon: <LayoutPanelLeft size={16} />,
|
||||||
title: "Split Screen",
|
title: t("nav.splitScreen"),
|
||||||
dot: splitMode !== "none",
|
dot: splitMode !== "none",
|
||||||
},
|
},
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
@@ -66,6 +74,7 @@ export function AppRail({
|
|||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
splitMode,
|
splitMode,
|
||||||
username,
|
username,
|
||||||
|
isAdmin,
|
||||||
profileDropdownOpen,
|
profileDropdownOpen,
|
||||||
onProfileDropdownChange,
|
onProfileDropdownChange,
|
||||||
onRailClick,
|
onRailClick,
|
||||||
@@ -75,14 +84,16 @@ export function AppRail({
|
|||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
splitMode: SplitMode;
|
splitMode: SplitMode;
|
||||||
username: string;
|
username: string;
|
||||||
|
isAdmin: boolean;
|
||||||
profileDropdownOpen: boolean;
|
profileDropdownOpen: boolean;
|
||||||
onProfileDropdownChange: (open: boolean) => void;
|
onProfileDropdownChange: (open: boolean) => void;
|
||||||
onRailClick: (view: RailView) => void;
|
onRailClick: (view: RailView) => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const railExpanded = hovered || profileDropdownOpen;
|
const railExpanded = hovered || profileDropdownOpen;
|
||||||
const railButtons = buildRailButtons(splitMode);
|
const railButtons = buildRailButtons(splitMode, t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -132,20 +143,22 @@ export function AppRail({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 flex flex-col gap-1 border-t border-border pt-1 pb-1">
|
<div className="shrink-0 flex flex-col gap-1 border-t border-border pt-1 pb-1">
|
||||||
{(
|
{[
|
||||||
[
|
{
|
||||||
{
|
view: "user-profile" as RailView,
|
||||||
view: "user-profile" as RailView,
|
icon: <User size={16} />,
|
||||||
icon: <User size={16} />,
|
title: t("nav.userProfile"),
|
||||||
title: "Profile",
|
},
|
||||||
},
|
...(isAdmin
|
||||||
{
|
? [
|
||||||
view: "admin-settings" as RailView,
|
{
|
||||||
icon: <Settings size={16} />,
|
view: "admin-settings" as RailView,
|
||||||
title: "Admin",
|
icon: <Settings size={16} />,
|
||||||
},
|
title: t("nav.admin"),
|
||||||
] as const
|
},
|
||||||
).map((item) => (
|
]
|
||||||
|
: []),
|
||||||
|
].map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.view}
|
key={item.view}
|
||||||
onClick={() => onRailClick(item.view)}
|
onClick={() => onRailClick(item.view)}
|
||||||
|
|||||||
+604
-190
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/lib/terminal-themes";
|
} from "@/lib/terminal-themes";
|
||||||
import { Button } from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import { Input } from "@/components/input";
|
import { Input } from "@/components/input";
|
||||||
|
import { Slider } from "@/components/slider";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -146,10 +147,12 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
|||||||
enableVnc: h.enableVnc != null ? h.enableVnc : h.connectionType === "vnc",
|
enableVnc: h.enableVnc != null ? h.enableVnc : h.connectionType === "vnc",
|
||||||
enableTelnet:
|
enableTelnet:
|
||||||
h.enableTelnet != null ? h.enableTelnet : h.connectionType === "telnet",
|
h.enableTelnet != null ? h.enableTelnet : h.connectionType === "telnet",
|
||||||
sshPort: h.sshPort ?? h.port,
|
sshPort:
|
||||||
rdpPort: h.rdpPort ?? 3389,
|
h.sshPort ??
|
||||||
vncPort: h.vncPort ?? 5900,
|
(h.connectionType === "ssh" || !h.connectionType ? h.port : 22),
|
||||||
telnetPort: h.telnetPort ?? 23,
|
rdpPort: h.rdpPort ?? (h.connectionType === "rdp" ? h.port : 3389),
|
||||||
|
vncPort: h.vncPort ?? (h.connectionType === "vnc" ? h.port : 5900),
|
||||||
|
telnetPort: h.telnetPort ?? (h.connectionType === "telnet" ? h.port : 23),
|
||||||
rdpUser: h.rdpUser,
|
rdpUser: h.rdpUser,
|
||||||
rdpPassword: h.rdpPassword,
|
rdpPassword: h.rdpPassword,
|
||||||
domain: h.rdpDomain,
|
domain: h.rdpDomain,
|
||||||
@@ -178,6 +181,8 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
|||||||
socks5Port: h.socks5Port,
|
socks5Port: h.socks5Port,
|
||||||
socks5Username: h.socks5Username,
|
socks5Username: h.socks5Username,
|
||||||
socks5Password: h.socks5Password,
|
socks5Password: h.socks5Password,
|
||||||
|
socks5ProxyChain: parseJson(h.socks5ProxyChain) ?? [],
|
||||||
|
overrideCredentialUsername: h.overrideCredentialUsername ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +781,10 @@ function HostEditor({
|
|||||||
password: host?.password ?? "",
|
password: host?.password ?? "",
|
||||||
key: host?.key ?? "",
|
key: host?.key ?? "",
|
||||||
keyPassword: host?.keyPassword ?? "",
|
keyPassword: host?.keyPassword ?? "",
|
||||||
|
keyType: host?.keyType ?? "auto",
|
||||||
|
keySubTab: "paste" as "paste" | "upload",
|
||||||
credentialId: host?.credentialId ?? "",
|
credentialId: host?.credentialId ?? "",
|
||||||
|
overrideCredentialUsername: host?.overrideCredentialUsername ?? false,
|
||||||
folder: host?.folder ?? "",
|
folder: host?.folder ?? "",
|
||||||
tags: host?.tags ?? ([] as string[]),
|
tags: host?.tags ?? ([] as string[]),
|
||||||
tagInput: "",
|
tagInput: "",
|
||||||
@@ -788,6 +796,16 @@ function HostEditor({
|
|||||||
socks5Port: host?.socks5Port ?? 1080,
|
socks5Port: host?.socks5Port ?? 1080,
|
||||||
socks5Username: host?.socks5Username ?? "",
|
socks5Username: host?.socks5Username ?? "",
|
||||||
socks5Password: host?.socks5Password ?? "",
|
socks5Password: host?.socks5Password ?? "",
|
||||||
|
socks5ProxyMode: ((host?.socks5ProxyChain as any[])?.length > 0
|
||||||
|
? "chain"
|
||||||
|
: "single") as "single" | "chain",
|
||||||
|
socks5ProxyChain: ((host?.socks5ProxyChain as any[]) ?? []) as {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
type: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}[],
|
||||||
enableTerminal: host?.enableTerminal ?? true,
|
enableTerminal: host?.enableTerminal ?? true,
|
||||||
enableFileManager: host?.enableFileManager ?? false,
|
enableFileManager: host?.enableFileManager ?? false,
|
||||||
enableDocker: host?.enableDocker ?? false,
|
enableDocker: host?.enableDocker ?? false,
|
||||||
@@ -978,7 +996,9 @@ function HostEditor({
|
|||||||
password: form.password || null,
|
password: form.password || null,
|
||||||
key: form.key || null,
|
key: form.key || null,
|
||||||
keyPassword: form.keyPassword || null,
|
keyPassword: form.keyPassword || null,
|
||||||
|
keyType: form.keyType !== "auto" ? form.keyType : null,
|
||||||
credentialId: form.credentialId ? Number(form.credentialId) : null,
|
credentialId: form.credentialId ? Number(form.credentialId) : null,
|
||||||
|
overrideCredentialUsername: form.overrideCredentialUsername,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
macAddress: form.macAddress || null,
|
macAddress: form.macAddress || null,
|
||||||
enableTerminal: form.enableTerminal,
|
enableTerminal: form.enableTerminal,
|
||||||
@@ -987,10 +1007,20 @@ function HostEditor({
|
|||||||
enableDocker: form.enableDocker,
|
enableDocker: form.enableDocker,
|
||||||
defaultPath: form.defaultPath || "/",
|
defaultPath: form.defaultPath || "/",
|
||||||
useSocks5: form.useSocks5,
|
useSocks5: form.useSocks5,
|
||||||
socks5Host: form.socks5Host || null,
|
socks5Host:
|
||||||
socks5Port: form.socks5Port || null,
|
form.socks5ProxyMode === "single" ? form.socks5Host || null : null,
|
||||||
socks5Username: form.socks5Username || null,
|
socks5Port:
|
||||||
socks5Password: form.socks5Password || null,
|
form.socks5ProxyMode === "single" ? form.socks5Port || null : null,
|
||||||
|
socks5Username:
|
||||||
|
form.socks5ProxyMode === "single"
|
||||||
|
? form.socks5Username || null
|
||||||
|
: null,
|
||||||
|
socks5Password:
|
||||||
|
form.socks5ProxyMode === "single"
|
||||||
|
? form.socks5Password || null
|
||||||
|
: null,
|
||||||
|
socks5ProxyChain:
|
||||||
|
form.socks5ProxyMode === "chain" ? form.socks5ProxyChain : null,
|
||||||
enableSsh: protocols.enableSsh,
|
enableSsh: protocols.enableSsh,
|
||||||
enableRdp: protocols.enableRdp,
|
enableRdp: protocols.enableRdp,
|
||||||
enableVnc: protocols.enableVnc,
|
enableVnc: protocols.enableVnc,
|
||||||
@@ -1418,59 +1448,274 @@ function HostEditor({
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{form.useSocks5 && (
|
{form.useSocks5 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-3 bg-muted/20 border border-border">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
{/* Single / Chain mode toggle */}
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex gap-2">
|
||||||
{t("hosts.proxyHost")}
|
{(["single", "chain"] as const).map((m) => (
|
||||||
</label>
|
<button
|
||||||
<Input
|
key={m}
|
||||||
className="h-7 text-xs"
|
type="button"
|
||||||
placeholder="proxy.example.com"
|
onClick={() => setField("socks5ProxyMode", m)}
|
||||||
value={form.socks5Host}
|
className={`px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest border transition-colors ${form.socks5ProxyMode === m ? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand" : "border-border text-muted-foreground hover:text-foreground"}`}
|
||||||
onChange={(e) => setField("socks5Host", e.target.value)}
|
>
|
||||||
/>
|
{m === "single"
|
||||||
</div>
|
? t("hosts.proxySingleMode")
|
||||||
<div className="flex flex-col gap-1.5">
|
: t("hosts.proxyChainMode")}
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
</button>
|
||||||
{t("hosts.proxyPort")}
|
))}
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
className="h-7 text-xs"
|
|
||||||
type="number"
|
|
||||||
placeholder="1080"
|
|
||||||
value={form.socks5Port}
|
|
||||||
onChange={(e) =>
|
|
||||||
setField("socks5Port", Number(e.target.value) as any)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
||||||
{t("hosts.proxyUsername")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
className="h-7 text-xs"
|
|
||||||
placeholder="Optional"
|
|
||||||
value={form.socks5Username}
|
|
||||||
onChange={(e) =>
|
|
||||||
setField("socks5Username", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
||||||
{t("hosts.proxyPassword")}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
className="h-7 text-xs"
|
|
||||||
type="password"
|
|
||||||
placeholder="Optional"
|
|
||||||
value={form.socks5Password}
|
|
||||||
onChange={(e) =>
|
|
||||||
setField("socks5Password", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{form.socks5ProxyMode === "single" && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-3 bg-muted/20 border border-border">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyHost")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
placeholder="proxy.example.com"
|
||||||
|
value={form.socks5Host}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("socks5Host", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyPort")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
type="number"
|
||||||
|
placeholder="1080"
|
||||||
|
value={form.socks5Port}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField(
|
||||||
|
"socks5Port",
|
||||||
|
Number(e.target.value) as any,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyUsername")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
placeholder={t("hosts.optional")}
|
||||||
|
value={form.socks5Username}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("socks5Username", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyPassword")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
type="password"
|
||||||
|
placeholder={t("hosts.optional")}
|
||||||
|
value={form.socks5Password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("socks5Password", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.socks5ProxyMode === "chain" && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{form.socks5ProxyChain.map((node, ni) => (
|
||||||
|
<div
|
||||||
|
key={ni}
|
||||||
|
className="flex flex-col gap-2 p-3 bg-muted/20 border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] font-bold text-muted-foreground">
|
||||||
|
{t("hosts.proxyNode")} {ni + 1}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-destructive"
|
||||||
|
onClick={() =>
|
||||||
|
setField(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
form.socks5ProxyChain.filter(
|
||||||
|
(_, idx) => idx !== ni,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyHost")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
placeholder="proxy.example.com"
|
||||||
|
value={node.host}
|
||||||
|
onChange={(e) => {
|
||||||
|
const u = [...form.socks5ProxyChain];
|
||||||
|
u[ni] = { ...u[ni], host: e.target.value };
|
||||||
|
setField("socks5ProxyChain", u);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyPort")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
type="number"
|
||||||
|
placeholder="1080"
|
||||||
|
value={node.port}
|
||||||
|
onChange={(e) => {
|
||||||
|
const u = [...form.socks5ProxyChain];
|
||||||
|
u[ni] = {
|
||||||
|
...u[ni],
|
||||||
|
port: Number(e.target.value),
|
||||||
|
};
|
||||||
|
setField("socks5ProxyChain", u);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyType")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="h-7 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
value={node.type}
|
||||||
|
onChange={(e) => {
|
||||||
|
const u = [...form.socks5ProxyChain];
|
||||||
|
u[ni] = { ...u[ni], type: e.target.value };
|
||||||
|
setField("socks5ProxyChain", u);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="socks5">SOCKS5</option>
|
||||||
|
<option value="socks4">SOCKS4</option>
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyUsername")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
placeholder={t("hosts.optional")}
|
||||||
|
value={node.username}
|
||||||
|
onChange={(e) => {
|
||||||
|
const u = [...form.socks5ProxyChain];
|
||||||
|
u[ni] = {
|
||||||
|
...u[ni],
|
||||||
|
username: e.target.value,
|
||||||
|
};
|
||||||
|
setField("socks5ProxyChain", u);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 col-span-2">
|
||||||
|
<label className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.proxyPassword")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs"
|
||||||
|
type="password"
|
||||||
|
placeholder={t("hosts.optional")}
|
||||||
|
value={node.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
const u = [...form.socks5ProxyChain];
|
||||||
|
u[ni] = {
|
||||||
|
...u[ni],
|
||||||
|
password: e.target.value,
|
||||||
|
};
|
||||||
|
setField("socks5ProxyChain", u);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand self-start"
|
||||||
|
onClick={() =>
|
||||||
|
setField("socks5ProxyChain", [
|
||||||
|
...form.socks5ProxyChain,
|
||||||
|
{
|
||||||
|
host: "",
|
||||||
|
port: 1080,
|
||||||
|
type: "socks5",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="size-3 mr-1" />{" "}
|
||||||
|
{t("hosts.addProxyNode")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Connection path visualization */}
|
||||||
|
{(form.socks5ProxyMode === "single" && form.socks5Host) ||
|
||||||
|
(form.socks5ProxyMode === "chain" &&
|
||||||
|
form.socks5ProxyChain.length > 0) ? (
|
||||||
|
<div className="flex items-center gap-1 flex-wrap p-2 bg-muted/30 border border-border text-[10px]">
|
||||||
|
<span className="px-2 py-0.5 bg-background border border-border text-foreground font-mono">
|
||||||
|
{t("hosts.you")}
|
||||||
|
</span>
|
||||||
|
{form.socks5ProxyMode === "single" &&
|
||||||
|
form.socks5Host ? (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
<span className="px-2 py-0.5 bg-muted border border-border text-muted-foreground font-mono">
|
||||||
|
{form.socks5Host}:{form.socks5Port}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
form.socks5ProxyChain
|
||||||
|
.filter((n) => n.host)
|
||||||
|
.map((n, ni) => (
|
||||||
|
<React.Fragment key={ni}>
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
<span className="px-2 py-0.5 bg-muted border border-border text-muted-foreground font-mono">
|
||||||
|
{n.host}:{n.port}
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{form.jumpHosts
|
||||||
|
.filter((j) => j.hostId)
|
||||||
|
.map((j, ji) => {
|
||||||
|
const jh = hosts.find((h) => h.id === j.hostId);
|
||||||
|
return jh ? (
|
||||||
|
<React.Fragment key={`j${ji}`}>
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
<span className="px-2 py-0.5 bg-muted border border-border text-muted-foreground font-mono">
|
||||||
|
{jh.name || jh.ip}
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
<span className="px-2 py-0.5 bg-accent-brand/10 border border-accent-brand/30 text-accent-brand font-mono">
|
||||||
|
{form.ip || "target"}:{form.sshPort}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -1619,16 +1864,68 @@ function HostEditor({
|
|||||||
{authMethod === "key" && (
|
{authMethod === "key" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1.5 col-span-2">
|
<div className="flex flex-col gap-1.5 col-span-2">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.sshPrivateKey")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.sshPrivateKey")}
|
||||||
<textarea
|
</label>
|
||||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
<div className="flex gap-1">
|
||||||
rows={5}
|
{(["paste", "upload"] as const).map((tab) => (
|
||||||
value={form.key}
|
<button
|
||||||
onChange={(e) => setField("key", e.target.value)}
|
key={tab}
|
||||||
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
type="button"
|
||||||
/>
|
onClick={() => setField("keySubTab", tab)}
|
||||||
|
className={`px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest border transition-colors ${form.keySubTab === tab ? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand" : "border-border text-muted-foreground hover:text-foreground"}`}
|
||||||
|
>
|
||||||
|
{tab === "paste"
|
||||||
|
? t("hosts.keyPasteTab")
|
||||||
|
: t("hosts.keyUploadTab")}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{form.keySubTab === "paste" ? (
|
||||||
|
<textarea
|
||||||
|
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||||
|
rows={5}
|
||||||
|
value={form.key}
|
||||||
|
onChange={(e) => setField("key", e.target.value)}
|
||||||
|
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
className={`flex items-center justify-center gap-2 h-16 border-2 border-dashed cursor-pointer transition-colors ${form.key ? "border-accent-brand/40 bg-accent-brand/5 text-accent-brand" : "border-border text-muted-foreground hover:border-accent-brand/30 hover:text-foreground"}`}
|
||||||
|
>
|
||||||
|
<Upload className="size-4" />
|
||||||
|
<span className="text-xs">
|
||||||
|
{form.key
|
||||||
|
? t("hosts.keyFileLoaded")
|
||||||
|
: t("hosts.keyUploadClick")}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pem,.key,.txt,.ppk"
|
||||||
|
className="hidden"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const text = await file.text();
|
||||||
|
setField("key", text);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{form.key && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setField("key", "")}
|
||||||
|
className="text-[10px] text-destructive self-start"
|
||||||
|
>
|
||||||
|
{t("hosts.clearKey")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
@@ -1643,28 +1940,90 @@ function HostEditor({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.keyTypeLabel")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={form.keyType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("keyType", e.target.value as any)
|
||||||
|
}
|
||||||
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="auto">{t("hosts.keyTypeAuto")}</option>
|
||||||
|
<option value="ssh-rsa">RSA</option>
|
||||||
|
<option value="ssh-ed25519">Ed25519</option>
|
||||||
|
<option value="ecdsa-sha2-nistp256">
|
||||||
|
ECDSA P-256
|
||||||
|
</option>
|
||||||
|
<option value="ecdsa-sha2-nistp384">
|
||||||
|
ECDSA P-384
|
||||||
|
</option>
|
||||||
|
<option value="ecdsa-sha2-nistp521">
|
||||||
|
ECDSA P-521
|
||||||
|
</option>
|
||||||
|
<option value="ssh-dss">DSA</option>
|
||||||
|
<option value="ssh-rsa-sha2-256">RSA SHA2-256</option>
|
||||||
|
<option value="ssh-rsa-sha2-512">RSA SHA2-512</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{authMethod === "credential" && (
|
{authMethod === "credential" && (
|
||||||
<div className="flex flex-col gap-1.5">
|
<>
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex flex-col gap-1.5">
|
||||||
{t("hosts.storedCredential")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.storedCredential")}
|
||||||
<select
|
</label>
|
||||||
value={form.credentialId}
|
<select
|
||||||
onChange={(e) =>
|
value={form.credentialId}
|
||||||
setField("credentialId", e.target.value)
|
onChange={(e) =>
|
||||||
}
|
setField("credentialId", e.target.value)
|
||||||
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
}
|
||||||
>
|
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||||
<option value="">{t("hosts.selectACredential")}</option>
|
>
|
||||||
{credentials.map((c) => (
|
<option value="">
|
||||||
<option key={c.id} value={c.id}>
|
{t("hosts.selectACredential")}
|
||||||
{c.name} ({c.username})
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
{credentials.map((c) => (
|
||||||
</select>
|
<option key={c.id} value={c.id}>
|
||||||
</div>
|
{c.name} ({c.username})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between col-span-2 pt-1">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{t("hosts.overrideCredentialUsername")}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{t("hosts.overrideCredentialUsernameDesc")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<FakeSwitch
|
||||||
|
checked={form.overrideCredentialUsername}
|
||||||
|
onChange={(v) =>
|
||||||
|
setField("overrideCredentialUsername", v)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{form.overrideCredentialUsername && (
|
||||||
|
<div className="flex flex-col gap-1.5 col-span-2">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("hosts.username")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder="root"
|
||||||
|
value={form.username}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("username", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
@@ -1780,16 +2139,20 @@ function HostEditor({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.fontSizeLabel")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.fontSizeLabel")}
|
||||||
<Input
|
</label>
|
||||||
type="number"
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
value={form.fontSize}
|
{form.fontSize}px
|
||||||
onChange={(e) =>
|
</span>
|
||||||
setField("fontSize", Number(e.target.value) as any)
|
</div>
|
||||||
}
|
<Slider
|
||||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
min={8}
|
||||||
|
max={24}
|
||||||
|
step={1}
|
||||||
|
value={[form.fontSize]}
|
||||||
|
onValueChange={([v]) => setField("fontSize", v as any)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -1811,30 +2174,39 @@ function HostEditor({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.letterSpacingPx")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.letterSpacingPx")}
|
||||||
<Input
|
</label>
|
||||||
type="number"
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
value={form.letterSpacing}
|
{form.letterSpacing}px
|
||||||
onChange={(e) =>
|
</span>
|
||||||
setField("letterSpacing", Number(e.target.value) as any)
|
</div>
|
||||||
|
<Slider
|
||||||
|
min={-2}
|
||||||
|
max={10}
|
||||||
|
step={0.5}
|
||||||
|
value={[form.letterSpacing]}
|
||||||
|
onValueChange={([v]) =>
|
||||||
|
setField("letterSpacing", v as any)
|
||||||
}
|
}
|
||||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.lineHeightLabel")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.lineHeightLabel")}
|
||||||
<Input
|
</label>
|
||||||
type="number"
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
step="0.1"
|
{form.lineHeight.toFixed(1)}
|
||||||
value={form.lineHeight}
|
</span>
|
||||||
onChange={(e) =>
|
</div>
|
||||||
setField("lineHeight", Number(e.target.value) as any)
|
<Slider
|
||||||
}
|
min={1.0}
|
||||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
max={2.0}
|
||||||
|
step={0.1}
|
||||||
|
value={[form.lineHeight]}
|
||||||
|
onValueChange={([v]) => setField("lineHeight", v as any)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -1898,20 +2270,22 @@ function HostEditor({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4 py-3">
|
<div className="flex flex-col gap-4 py-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.scrollbackBufferLabel")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.scrollbackBufferLabel")}
|
||||||
<Input
|
</label>
|
||||||
type="number"
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
value={form.scrollback}
|
{form.scrollback.toLocaleString()}{" "}
|
||||||
onChange={(e) =>
|
{t("hosts.scrollbackMaxLines")}
|
||||||
setField("scrollback", Number(e.target.value) as any)
|
</span>
|
||||||
}
|
</div>
|
||||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
<Slider
|
||||||
|
min={1000}
|
||||||
|
max={100000}
|
||||||
|
step={1000}
|
||||||
|
value={[form.scrollback]}
|
||||||
|
onValueChange={([v]) => setField("scrollback", v as any)}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
{t("hosts.scrollbackMaxLines")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label={t("hosts.sshAgentForwardingLabel")}
|
label={t("hosts.sshAgentForwardingLabel")}
|
||||||
@@ -2050,19 +2424,22 @@ function HostEditor({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{t("hosts.fastScrollSensitivityLabel")}
|
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||||
</label>
|
{t("hosts.fastScrollSensitivityLabel")}
|
||||||
<Input
|
</label>
|
||||||
type="number"
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||||
value={form.fastScrollSensitivity}
|
{form.fastScrollSensitivity}
|
||||||
onChange={(e) =>
|
</span>
|
||||||
setField(
|
</div>
|
||||||
"fastScrollSensitivity",
|
<Slider
|
||||||
Number(e.target.value) as any,
|
min={1}
|
||||||
)
|
max={10}
|
||||||
|
step={1}
|
||||||
|
value={[form.fastScrollSensitivity]}
|
||||||
|
onValueChange={([v]) =>
|
||||||
|
setField("fastScrollSensitivity", v as any)
|
||||||
}
|
}
|
||||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2116,6 +2493,7 @@ function HostEditor({
|
|||||||
Number(e.target.value) as any,
|
Number(e.target.value) as any,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -2131,6 +2509,7 @@ function HostEditor({
|
|||||||
Number(e.target.value) as any,
|
Number(e.target.value) as any,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2323,6 +2702,13 @@ function HostEditor({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
|
||||||
|
{tun.mode === "local"
|
||||||
|
? t("hosts.tunnelModeLocalDesc")
|
||||||
|
: tun.mode === "remote"
|
||||||
|
? t("hosts.tunnelModeRemoteDesc")
|
||||||
|
: t("hosts.tunnelModeDynamicDesc")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{tun.mode !== "dynamic" && (
|
{tun.mode !== "dynamic" && (
|
||||||
@@ -2330,10 +2716,9 @@ function HostEditor({
|
|||||||
<label className="text-[10px] font-bold text-muted-foreground">
|
<label className="text-[10px] font-bold text-muted-foreground">
|
||||||
{t("hosts.endpointHost")}
|
{t("hosts.endpointHost")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<select
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||||
placeholder="e.g. 127.0.0.1"
|
value={tun.endpointHost ?? ""}
|
||||||
value={tun.endpointHost}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updated = [...form.serverTunnels];
|
const updated = [...form.serverTunnels];
|
||||||
updated[i] = {
|
updated[i] = {
|
||||||
@@ -2342,7 +2727,21 @@ function HostEditor({
|
|||||||
};
|
};
|
||||||
setField("serverTunnels", updated);
|
setField("serverTunnels", updated);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<option value="">
|
||||||
|
{t("hosts.selectAServer")}
|
||||||
|
</option>
|
||||||
|
<option value="127.0.0.1">
|
||||||
|
127.0.0.1 (localhost)
|
||||||
|
</option>
|
||||||
|
{hosts
|
||||||
|
.filter((h) => h.enableSsh)
|
||||||
|
.map((h) => (
|
||||||
|
<option key={h.id} value={h.ip}>
|
||||||
|
{h.name || h.ip} ({h.ip})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{tun.mode !== "dynamic" && (
|
{tun.mode !== "dynamic" && (
|
||||||
@@ -2351,7 +2750,7 @@ function HostEditor({
|
|||||||
{t("hosts.endpointPort")}
|
{t("hosts.endpointPort")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
type="number"
|
type="number"
|
||||||
value={tun.endpointPort}
|
value={tun.endpointPort}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -2388,7 +2787,7 @@ function HostEditor({
|
|||||||
{t("hosts.sourcePort")}
|
{t("hosts.sourcePort")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
type="number"
|
type="number"
|
||||||
value={tun.sourcePort}
|
value={tun.sourcePort}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -2406,7 +2805,7 @@ function HostEditor({
|
|||||||
{t("hosts.maxRetries")}
|
{t("hosts.maxRetries")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
type="number"
|
type="number"
|
||||||
value={tun.maxRetries}
|
value={tun.maxRetries}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -2424,7 +2823,7 @@ function HostEditor({
|
|||||||
{t("hosts.retryIntervalS")}
|
{t("hosts.retryIntervalS")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
type="number"
|
type="number"
|
||||||
value={tun.retryInterval}
|
value={tun.retryInterval}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -2545,24 +2944,25 @@ function HostEditor({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{!form.statsConfig.useGlobalStatusInterval && (
|
{form.statsConfig.statusCheckEnabled &&
|
||||||
<SettingRow
|
!form.statsConfig.useGlobalStatusInterval && (
|
||||||
label={t("hosts.checkIntervalS")}
|
<SettingRow
|
||||||
description={t("hosts.checkIntervalDesc")}
|
label={t("hosts.checkIntervalS")}
|
||||||
>
|
description={t("hosts.checkIntervalDesc")}
|
||||||
<Input
|
>
|
||||||
type="number"
|
<Input
|
||||||
value={form.statsConfig.statusCheckInterval}
|
type="number"
|
||||||
onChange={(e) =>
|
value={form.statsConfig.statusCheckInterval}
|
||||||
setField("statsConfig", {
|
onChange={(e) =>
|
||||||
...form.statsConfig,
|
setField("statsConfig", {
|
||||||
statusCheckInterval: Number(e.target.value),
|
...form.statsConfig,
|
||||||
})
|
statusCheckInterval: Number(e.target.value),
|
||||||
}
|
})
|
||||||
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
}
|
||||||
/>
|
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
</SettingRow>
|
/>
|
||||||
)}
|
</SettingRow>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
<SectionCard
|
<SectionCard
|
||||||
@@ -2598,24 +2998,25 @@ function HostEditor({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{!form.statsConfig.useGlobalMetricsInterval && (
|
{form.statsConfig.metricsEnabled &&
|
||||||
<SettingRow
|
!form.statsConfig.useGlobalMetricsInterval && (
|
||||||
label={t("hosts.metricsIntervalS")}
|
<SettingRow
|
||||||
description={t("hosts.metricsIntervalDesc2")}
|
label={t("hosts.metricsIntervalS")}
|
||||||
>
|
description={t("hosts.metricsIntervalDesc2")}
|
||||||
<Input
|
>
|
||||||
type="number"
|
<Input
|
||||||
value={form.statsConfig.metricsInterval}
|
type="number"
|
||||||
onChange={(e) =>
|
value={form.statsConfig.metricsInterval}
|
||||||
setField("statsConfig", {
|
onChange={(e) =>
|
||||||
...form.statsConfig,
|
setField("statsConfig", {
|
||||||
metricsInterval: Number(e.target.value),
|
...form.statsConfig,
|
||||||
})
|
metricsInterval: Number(e.target.value),
|
||||||
}
|
})
|
||||||
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
}
|
||||||
/>
|
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
</SettingRow>
|
/>
|
||||||
)}
|
</SettingRow>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
<SectionCard
|
<SectionCard
|
||||||
@@ -2695,7 +3096,7 @@ function HostEditor({
|
|||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title={t("hosts.quickActionsToolbar").split(".")[0]}
|
title={t("hosts.quickActionsLabel")}
|
||||||
icon={<Zap className="size-3.5" />}
|
icon={<Zap className="size-3.5" />}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
@@ -2906,6 +3307,7 @@ function HostEditor({
|
|||||||
placeholder="Auto"
|
placeholder="Auto"
|
||||||
value={form.guacamoleConfig["width"] ?? ""}
|
value={form.guacamoleConfig["width"] ?? ""}
|
||||||
onChange={(e) => setGuacField("width", e.target.value)}
|
onChange={(e) => setGuacField("width", e.target.value)}
|
||||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -2917,6 +3319,7 @@ function HostEditor({
|
|||||||
placeholder="Auto"
|
placeholder="Auto"
|
||||||
value={form.guacamoleConfig["height"] ?? ""}
|
value={form.guacamoleConfig["height"] ?? ""}
|
||||||
onChange={(e) => setGuacField("height", e.target.value)}
|
onChange={(e) => setGuacField("height", e.target.value)}
|
||||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2929,6 +3332,7 @@ function HostEditor({
|
|||||||
placeholder="96"
|
placeholder="96"
|
||||||
value={form.guacamoleConfig["dpi"] ?? ""}
|
value={form.guacamoleConfig["dpi"] ?? ""}
|
||||||
onChange={(e) => setGuacField("dpi", e.target.value)}
|
onChange={(e) => setGuacField("dpi", e.target.value)}
|
||||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -4650,7 +5054,10 @@ function CredentialEditorView({
|
|||||||
setGeneratingKey(true);
|
setGeneratingKey(true);
|
||||||
try {
|
try {
|
||||||
const result = await generateKeyPair(
|
const result = await generateKeyPair(
|
||||||
keyType,
|
keyType as
|
||||||
|
| "ssh-ed25519"
|
||||||
|
| "ssh-rsa"
|
||||||
|
| "ecdsa-sha2-nistp256",
|
||||||
bits,
|
bits,
|
||||||
credForm.passphrase || undefined,
|
credForm.passphrase || undefined,
|
||||||
);
|
);
|
||||||
@@ -5681,6 +6088,13 @@ export function HostManager({
|
|||||||
const normalized = hostsArray.map((h: any) => ({
|
const normalized = hostsArray.map((h: any) => ({
|
||||||
...h,
|
...h,
|
||||||
port: h.port ?? h.sshPort ?? 22,
|
port: h.port ?? h.sshPort ?? 22,
|
||||||
|
enableSsh:
|
||||||
|
h.enableSsh ??
|
||||||
|
(h.connectionType === "ssh" || !h.connectionType),
|
||||||
|
enableRdp: h.enableRdp ?? h.connectionType === "rdp",
|
||||||
|
enableVnc: h.enableVnc ?? h.connectionType === "vnc",
|
||||||
|
enableTelnet:
|
||||||
|
h.enableTelnet ?? h.connectionType === "telnet",
|
||||||
}));
|
}));
|
||||||
const result = await bulkImportSSHHosts(
|
const result = await bulkImportSSHHosts(
|
||||||
normalized,
|
normalized,
|
||||||
|
|||||||
Reference in New Issue
Block a user