Feature/german language support #374
18
README.md
18
README.md
@@ -45,22 +45,22 @@ If you would like, you can support the project here!\
|
||||
|
||||
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based
|
||||
solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal
|
||||
access, SSH tunneling capabilities, remote file management, with many more tools to come.
|
||||
access, SSH tunneling capabilities, and remote file management, with many more tools to come.
|
||||
|
||||
# Features
|
||||
|
||||
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system
|
||||
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring
|
||||
- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly.
|
||||
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders and easily save reusable login info while being able to automate the deploying of SSH keys
|
||||
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders, and easily save reusable login info while being able to automate the deployment of SSH keys
|
||||
- **Server Stats** - View CPU, memory, and HDD usage on any SSH server
|
||||
- **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support
|
||||
- **Database Encryption** - SQLite database files encrypted at rest with automatic encryption/decryption
|
||||
- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data with incremental sync
|
||||
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
|
||||
- **Modern UI** - Clean desktop/mobile friendly interface built with React, Tailwind CSS, and Shadcn
|
||||
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn
|
||||
- **Languages** - Built-in support for English and Chinese
|
||||
- **Platform Support** - Available as a web app, desktop application (Windows & Linux), and dedicated mobile app for iOS and Android (coming in a few days)
|
||||
- **Platform Support** - Available as a web app, desktop application (Windows & Linux), and dedicated mobile app for iOS and Android. macOS and iPadOS support is planned.
|
||||
|
||||
# Planned Features
|
||||
|
||||
@@ -73,12 +73,12 @@ Supported Devices:
|
||||
- Website (any modern browser like Google, Safari, and Firefox)
|
||||
- Windows (app)
|
||||
- Linux (app)
|
||||
- iOS (coming in a few days)
|
||||
- Android (coming in a few days)
|
||||
- iOS (app)
|
||||
- Android (app)
|
||||
- iPadOS and macOS are in progress
|
||||
|
||||
Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix on all platforms. Otherwise, view
|
||||
a sample docker-compose file here:
|
||||
a sample Docker Compose file here:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -121,6 +121,10 @@ repo.
|
||||
<img src="./repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/user-attachments/assets/88936e0d-2399-4122-8eee-c255c25da48c" width="800" controls>
|
||||
Your browser does not support the video tag.
|
||||
|
||||
@@ -10,6 +10,9 @@ http {
|
||||
keepalive_timeout 65;
|
||||
client_header_timeout 300s;
|
||||
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
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;
|
||||
@@ -23,7 +26,6 @@ http {
|
||||
return 301 https://$host:${SSL_PORT}$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS Server
|
||||
server {
|
||||
listen ${SSL_PORT} ssl;
|
||||
server_name _;
|
||||
@@ -41,7 +43,6 @@ http {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# Handle missing source map files gracefully
|
||||
location ~* \.map$ {
|
||||
return 404;
|
||||
access_log off;
|
||||
|
||||
@@ -10,6 +10,9 @@ http {
|
||||
keepalive_timeout 65;
|
||||
client_header_timeout 300s;
|
||||
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
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;
|
||||
@@ -29,7 +32,6 @@ http {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# Handle missing source map files gracefully
|
||||
location ~* \.map$ {
|
||||
return 404;
|
||||
access_log off;
|
||||
|
||||
61
package-lock.json
generated
61
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.2",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
"@codemirror/commands": "^6.3.3",
|
||||
@@ -128,7 +128,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz",
|
||||
"integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -177,7 +176,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
@@ -204,7 +202,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.10.tgz",
|
||||
"integrity": "sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
@@ -232,7 +229,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
@@ -420,7 +416,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
@@ -496,7 +491,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
@@ -518,7 +512,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.4.tgz",
|
||||
"integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -1211,6 +1204,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1232,6 +1226,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -1251,6 +1246,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1267,6 +1263,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1280,7 +1277,8 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@electron/windows-sign/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
@@ -1289,6 +1287,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -2313,8 +2312,7 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
|
||||
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
"version": "1.1.3",
|
||||
@@ -2354,7 +2352,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
@@ -2386,7 +2383,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
@@ -2409,7 +2405,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
@@ -4837,7 +4832,6 @@
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -4922,7 +4916,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -5087,7 +5080,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -5098,7 +5090,6 @@
|
||||
"integrity": "sha512-3BKc/yGdNTYQVVw4idqHtSOcFsgGuBbMveKCOgF8wQ5QtrYOc3jDIlzg3jef04zcXFIHLelyGlj0T+BJ8+KN+w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
@@ -5174,7 +5165,8 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
|
||||
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
@@ -5739,8 +5731,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/7zip-bin": {
|
||||
"version": "5.2.0",
|
||||
@@ -5775,7 +5766,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5836,7 +5826,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -6261,7 +6250,6 @@
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -7331,7 +7319,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@@ -8108,6 +8097,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -8128,6 +8118,7 @@
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -8146,6 +8137,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -8160,7 +8152,8 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "22.18.8",
|
||||
@@ -8387,7 +8380,6 @@
|
||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9959,7 +9951,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -13014,6 +13005,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -13031,6 +13023,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -13440,7 +13433,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13450,7 +13442,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -13477,7 +13468,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
||||
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -15120,6 +15110,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -15183,6 +15174,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -15197,6 +15189,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -15274,7 +15267,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15480,7 +15472,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15818,7 +15809,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -15910,7 +15900,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"private": true,
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
|
||||
BIN
repo-images/Image 7.png
Normal file
BIN
repo-images/Image 7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 723 KiB |
@@ -46,7 +46,7 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
|
||||
password: text("password"),
|
||||
key: text("key", { length: 8192 }),
|
||||
keyPassword: text("key_password"),
|
||||
key_password: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
|
||||
autostartPassword: text("autostart_password"),
|
||||
@@ -142,9 +142,9 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
|
||||
username: text("username").notNull(),
|
||||
password: text("password"),
|
||||
key: text("key", { length: 16384 }),
|
||||
privateKey: text("private_key", { length: 16384 }),
|
||||
publicKey: text("public_key", { length: 4096 }),
|
||||
keyPassword: text("key_password"),
|
||||
private_key: text("private_key", { length: 16384 }),
|
||||
public_key: text("public_key", { length: 4096 }),
|
||||
key_password: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
detectedKeyType: text("detected_key_type"),
|
||||
usageCount: integer("usage_count").notNull().default(0),
|
||||
|
||||
@@ -174,9 +174,9 @@ router.post(
|
||||
username: username.trim(),
|
||||
password: plainPassword,
|
||||
key: plainKey,
|
||||
privateKey: keyInfo?.privateKey || plainKey,
|
||||
publicKey: keyInfo?.publicKey || null,
|
||||
keyPassword: plainKeyPassword,
|
||||
private_key: keyInfo?.privateKey || plainKey,
|
||||
public_key: keyInfo?.publicKey || null,
|
||||
key_password: plainKeyPassword,
|
||||
keyType: keyType || null,
|
||||
detectedKeyType: keyInfo?.keyType || null,
|
||||
usageCount: 0,
|
||||
@@ -424,13 +424,13 @@ router.put(
|
||||
error: `Invalid SSH key: ${keyInfo.error}`,
|
||||
});
|
||||
}
|
||||
updateFields.privateKey = keyInfo.privateKey;
|
||||
updateFields.publicKey = keyInfo.publicKey;
|
||||
updateFields.private_key = keyInfo.privateKey;
|
||||
updateFields.public_key = keyInfo.publicKey;
|
||||
updateFields.detectedKeyType = keyInfo.keyType;
|
||||
}
|
||||
}
|
||||
if (updateData.keyPassword !== undefined) {
|
||||
updateFields.keyPassword = updateData.keyPassword || null;
|
||||
updateFields.key_password = updateData.keyPassword || null;
|
||||
}
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
@@ -537,7 +537,7 @@ router.delete(
|
||||
credentialId: null,
|
||||
password: null,
|
||||
key: null,
|
||||
keyPassword: null,
|
||||
key_password: null,
|
||||
authType: "password",
|
||||
})
|
||||
.where(
|
||||
@@ -633,7 +633,7 @@ router.post(
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: null,
|
||||
key: null,
|
||||
keyPassword: null,
|
||||
key_password: null,
|
||||
keyType: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
|
||||
username: host.username,
|
||||
password: host.autostartPassword,
|
||||
key: host.autostartKey,
|
||||
keyPassword: host.autostartKeyPassword,
|
||||
key_password: host.autostartKeyPassword,
|
||||
autostartPassword: host.autostartPassword,
|
||||
autostartKey: host.autostartKey,
|
||||
autostartKeyPassword: host.autostartKeyPassword,
|
||||
@@ -151,7 +151,7 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
|
||||
username: host.username,
|
||||
password: host.autostartPassword || host.password,
|
||||
key: host.autostartKey || host.key,
|
||||
keyPassword: host.autostartKeyPassword || host.keyPassword,
|
||||
key_password: host.autostartKeyPassword || host.key_password,
|
||||
autostartPassword: host.autostartPassword,
|
||||
autostartKey: host.autostartKey,
|
||||
autostartKeyPassword: host.autostartKeyPassword,
|
||||
@@ -226,7 +226,7 @@ router.post(
|
||||
authType,
|
||||
credentialId,
|
||||
key,
|
||||
keyPassword,
|
||||
key_password,
|
||||
keyType,
|
||||
pin,
|
||||
enableTerminal,
|
||||
@@ -274,17 +274,17 @@ router.post(
|
||||
if (effectiveAuthType === "password") {
|
||||
sshDataObj.password = password || null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
sshDataObj.key = key || null;
|
||||
sshDataObj.keyPassword = keyPassword || null;
|
||||
sshDataObj.key_password = key_password || null;
|
||||
sshDataObj.keyType = keyType;
|
||||
sshDataObj.password = null;
|
||||
} else {
|
||||
sshDataObj.password = null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ router.put(
|
||||
authType,
|
||||
credentialId,
|
||||
key,
|
||||
keyPassword,
|
||||
key_password,
|
||||
keyType,
|
||||
pin,
|
||||
enableTerminal,
|
||||
@@ -458,14 +458,14 @@ router.put(
|
||||
sshDataObj.password = password;
|
||||
}
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
if (key) {
|
||||
sshDataObj.key = key;
|
||||
}
|
||||
if (keyPassword !== undefined) {
|
||||
sshDataObj.keyPassword = keyPassword || null;
|
||||
if (key_password !== undefined) {
|
||||
sshDataObj.key_password = key_password || null;
|
||||
}
|
||||
if (keyType) {
|
||||
sshDataObj.keyType = keyType;
|
||||
@@ -474,7 +474,7 @@ router.put(
|
||||
} else {
|
||||
sshDataObj.password = null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ router.get(
|
||||
authType: resolvedHost.authType,
|
||||
password: resolvedHost.password || null,
|
||||
key: resolvedHost.key || null,
|
||||
keyPassword: resolvedHost.keyPassword || null,
|
||||
key_password: resolvedHost.key_password || null,
|
||||
keyType: resolvedHost.keyType || null,
|
||||
folder: resolvedHost.folder,
|
||||
tags:
|
||||
@@ -1234,7 +1234,7 @@ async function resolveHostCredentials(host: any): Promise<any> {
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password,
|
||||
key: credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
key_password: credential.key_password || credential.key_password,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
};
|
||||
}
|
||||
@@ -1404,8 +1404,8 @@ router.post(
|
||||
credentialId:
|
||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
||||
key: hostData.authType === "key" ? hostData.key : null,
|
||||
keyPassword:
|
||||
hostData.authType === "key" ? hostData.keyPassword : null,
|
||||
key_password:
|
||||
hostData.authType === "key" ? hostData.key_password : null,
|
||||
keyType:
|
||||
hostData.authType === "key" ? hostData.keyType || "auto" : null,
|
||||
pin: hostData.pin || false,
|
||||
@@ -1540,7 +1540,7 @@ router.post(
|
||||
...tunnel,
|
||||
endpointPassword: decryptedEndpoint.password || null,
|
||||
endpointKey: decryptedEndpoint.key || null,
|
||||
endpointKeyPassword: decryptedEndpoint.keyPassword || null,
|
||||
endpointKeyPassword: decryptedEndpoint.key_password || null,
|
||||
endpointAuthType: endpointHost.authType,
|
||||
};
|
||||
}
|
||||
@@ -1563,7 +1563,7 @@ router.post(
|
||||
.set({
|
||||
autostartPassword: decryptedConfig.password || null,
|
||||
autostartKey: decryptedConfig.key || null,
|
||||
autostartKeyPassword: decryptedConfig.keyPassword || null,
|
||||
autostartKeyPassword: decryptedConfig.key_password || null,
|
||||
tunnelConnections: updatedTunnelConnections,
|
||||
})
|
||||
.where(eq(sshData.id, sshConfigId));
|
||||
|
||||
@@ -6,6 +6,20 @@ export class LazyFieldEncryption {
|
||||
key_password: "keyPassword",
|
||||
private_key: "privateKey",
|
||||
public_key: "publicKey",
|
||||
password_hash: "passwordHash",
|
||||
client_secret: "clientSecret",
|
||||
totp_secret: "totpSecret",
|
||||
totp_backup_codes: "totpBackupCodes",
|
||||
oidc_identifier: "oidcIdentifier",
|
||||
|
||||
keyPassword: "key_password",
|
||||
privateKey: "private_key",
|
||||
publicKey: "public_key",
|
||||
passwordHash: "password_hash",
|
||||
clientSecret: "client_secret",
|
||||
totpSecret: "totp_secret",
|
||||
totpBackupCodes: "totp_backup_codes",
|
||||
oidcIdentifier: "oidc_identifier",
|
||||
};
|
||||
|
||||
static isPlaintextField(value: string): boolean {
|
||||
|
||||
@@ -70,7 +70,36 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
async setupOIDCUserEncryption(userId: string): Promise<void> {
|
||||
const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||
|
||||
let DEK: Buffer;
|
||||
|
||||
if (existingEncryptedDEK) {
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
DEK = this.decryptDEK(existingEncryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
} else {
|
||||
DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
|
||||
try {
|
||||
const encryptedDEK = this.encryptDEK(DEK, systemKey);
|
||||
await this.storeEncryptedDEK(userId, encryptedDEK);
|
||||
|
||||
const storedEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||
if (
|
||||
storedEncryptedDEK &&
|
||||
storedEncryptedDEK.data !== encryptedDEK.data
|
||||
) {
|
||||
DEK.fill(0);
|
||||
DEK = this.decryptDEK(storedEncryptedDEK, systemKey);
|
||||
} else if (!storedEncryptedDEK) {
|
||||
throw new Error("Failed to store and retrieve user encryption key.");
|
||||
}
|
||||
} finally {
|
||||
systemKey.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
this.userSessions.set(userId, {
|
||||
@@ -134,20 +163,14 @@ class UserCrypto {
|
||||
|
||||
async authenticateOIDCUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const kekSalt = await this.getKEKSalt(userId);
|
||||
if (!kekSalt) {
|
||||
const encryptedDEK = await this.getEncryptedDEK(userId);
|
||||
|
||||
if (!encryptedDEK) {
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
const encryptedDEK = await this.getEncryptedDEK(userId);
|
||||
if (!encryptedDEK) {
|
||||
systemKey.fill(0);
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const DEK = this.decryptDEK(encryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
|
||||
|
||||
1385
src/locales/de/translation.json
Normal file
1385
src/locales/de/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -286,6 +286,7 @@
|
||||
"sshTools": "SSH Tools",
|
||||
"english": "English",
|
||||
"chinese": "Chinese",
|
||||
"german": "German",
|
||||
"cancel": "Cancel",
|
||||
"username": "Username",
|
||||
"name": "Name",
|
||||
@@ -844,8 +845,13 @@
|
||||
"selectServerToEdit": "Select a server from the sidebar to start editing files",
|
||||
"fileOperations": "File Operations",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete <strong>{{name}}</strong>?",
|
||||
"confirmDeleteSingleItem": "Are you sure you want to permanently delete \"{{name}}\"?",
|
||||
"confirmDeleteMultipleItems": "Are you sure you want to permanently delete {{count}} items?",
|
||||
"confirmDeleteMultipleItemsWithFolders": "Are you sure you want to permanently delete {{count}} items? This includes folders and their contents.",
|
||||
"confirmDeleteFolder": "Are you sure you want to permanently delete the folder \"{{name}}\" and all its contents?",
|
||||
"deleteDirectoryWarning": "This will delete the folder and all its contents.",
|
||||
"actionCannotBeUndone": "This action cannot be undone.",
|
||||
"permanentDeleteWarning": "This action cannot be undone. The item(s) will be permanently deleted from the server.",
|
||||
"recent": "Recent",
|
||||
"pinned": "Pinned",
|
||||
"folderShortcuts": "Folder Shortcuts",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"sshTools": "SSH 工具",
|
||||
"english": "英语",
|
||||
"chinese": "中文",
|
||||
"german": "德语",
|
||||
"cancel": "取消",
|
||||
"username": "用户名",
|
||||
"name": "名称",
|
||||
@@ -852,8 +853,13 @@
|
||||
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
|
||||
"fileOperations": "文件操作",
|
||||
"confirmDeleteMessage": "确定要删除 <strong>{{name}}</strong> 吗?",
|
||||
"confirmDeleteSingleItem": "确定要永久删除 \"{{name}}\" 吗?",
|
||||
"confirmDeleteMultipleItems": "确定要永久删除 {{count}} 个项目吗?",
|
||||
"confirmDeleteMultipleItemsWithFolders": "确定要永久删除 {{count}} 个项目吗?这包括文件夹及其内容。",
|
||||
"confirmDeleteFolder": "确定要永久删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
||||
"deleteDirectoryWarning": "这将删除文件夹及其所有内容。",
|
||||
"actionCannotBeUndone": "此操作无法撤销。",
|
||||
"permanentDeleteWarning": "此操作无法撤销。项目将从服务器永久删除。",
|
||||
"dragSystemFilesToUpload": "拖拽系统文件到此处上传",
|
||||
"dragFilesToWindowToDownload": "拖拽文件到窗口外下载",
|
||||
"openTerminalHere": "在此处打开终端",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FileWindow } from "./components/FileWindow";
|
||||
import { DiffWindow } from "./components/DiffWindow";
|
||||
import { useDragToDesktop } from "../../../hooks/useDragToDesktop";
|
||||
import { useDragToSystemDesktop } from "../../../hooks/useDragToSystemDesktop";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
@@ -82,6 +83,7 @@ function formatFileSize(bytes?: number): string {
|
||||
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const { openWindow } = useWindowManager();
|
||||
const { t } = useTranslation();
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
|
||||
const [currentHost, setCurrentHost] = useState<SSHHost | null>(
|
||||
initialHost || null,
|
||||
@@ -587,54 +589,85 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
async function handleDeleteFiles(files: FileItem[]) {
|
||||
if (!sshSessionId || files.length === 0) return;
|
||||
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
for (const file of files) {
|
||||
await deleteSSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
file.type === "directory",
|
||||
currentHost?.id,
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const deletedFiles = files.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
}));
|
||||
|
||||
const undoAction: UndoAction = {
|
||||
type: "delete",
|
||||
description: t("fileManager.deletedItems", { count: files.length }),
|
||||
data: {
|
||||
operation: "cut",
|
||||
deletedFiles,
|
||||
targetDirectory: currentPath,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setUndoHistory((prev) => [...prev.slice(-9), undoAction]);
|
||||
|
||||
toast.success(
|
||||
t("fileManager.itemsDeletedSuccessfully", { count: files.length }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
);
|
||||
let confirmMessage: string;
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
if (file.type === "directory") {
|
||||
confirmMessage = t("fileManager.confirmDeleteFolder", {
|
||||
name: file.name,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDeleteItems"));
|
||||
confirmMessage = t("fileManager.confirmDeleteSingleItem", {
|
||||
name: file.name,
|
||||
});
|
||||
}
|
||||
console.error("Delete failed:", error);
|
||||
} else {
|
||||
const hasDirectory = files.some((file) => file.type === "directory");
|
||||
const translationKey = hasDirectory
|
||||
? "fileManager.confirmDeleteMultipleItemsWithFolders"
|
||||
: "fileManager.confirmDeleteMultipleItems";
|
||||
|
||||
confirmMessage = t(translationKey, {
|
||||
count: files.length,
|
||||
});
|
||||
}
|
||||
|
||||
const fullMessage = `${confirmMessage}\n\n${t("fileManager.permanentDeleteWarning")}`;
|
||||
|
||||
confirmWithToast(
|
||||
fullMessage,
|
||||
async () => {
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
for (const file of files) {
|
||||
await deleteSSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
file.type === "directory",
|
||||
currentHost?.id,
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const deletedFiles = files.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
}));
|
||||
|
||||
const undoAction: UndoAction = {
|
||||
type: "delete",
|
||||
description: t("fileManager.deletedItems", { count: files.length }),
|
||||
data: {
|
||||
operation: "cut",
|
||||
deletedFiles,
|
||||
targetDirectory: currentPath,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setUndoHistory((prev) => [...prev.slice(-9), undoAction]);
|
||||
|
||||
toast.success(
|
||||
t("fileManager.itemsDeletedSuccessfully", { count: files.length }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
);
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDeleteItems"));
|
||||
}
|
||||
console.error("Delete failed:", error);
|
||||
}
|
||||
},
|
||||
"destructive",
|
||||
);
|
||||
}
|
||||
|
||||
function handleCreateNewFolder() {
|
||||
|
||||
@@ -210,7 +210,18 @@ export function HostManagerEditor({
|
||||
defaultPath: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.authType === "key") {
|
||||
if (data.authType === "password") {
|
||||
if (
|
||||
!data.password ||
|
||||
(typeof data.password === "string" && data.password.trim() === "")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("hosts.passwordRequired"),
|
||||
path: ["password"],
|
||||
});
|
||||
}
|
||||
} else if (data.authType === "key") {
|
||||
if (
|
||||
!data.key ||
|
||||
(typeof data.key === "string" && data.key.trim() === "")
|
||||
|
||||
@@ -24,7 +24,10 @@ function AppContent() {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [showVersionCheck, setShowVersionCheck] = useState(true);
|
||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem("topNavbarOpen");
|
||||
return saved !== null ? JSON.parse(saved) : true;
|
||||
});
|
||||
const { currentTab, tabs } = useTabs();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,6 +67,10 @@ function AppContent() {
|
||||
return () => window.removeEventListener("storage", handleStorageChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("topNavbarOpen", JSON.stringify(isTopbarOpen));
|
||||
}, [isTopbarOpen]);
|
||||
|
||||
const handleSelectView = (nextView: string) => {
|
||||
setMountedViews((prev) => {
|
||||
if (prev.has(nextView)) return prev;
|
||||
|
||||
@@ -101,7 +101,10 @@ export function LeftSidebar({
|
||||
const [deleteLoading, setDeleteLoading] = React.useState(false);
|
||||
const [deleteError, setDeleteError] = React.useState<string | null>(null);
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem("leftSidebarOpen");
|
||||
return saved !== null ? JSON.parse(saved) : true;
|
||||
});
|
||||
|
||||
const {
|
||||
tabs: tabList,
|
||||
@@ -181,7 +184,6 @@ export function LeftSidebar({
|
||||
newHost.key !== existingHost.key ||
|
||||
newHost.keyPassword !== existingHost.keyPassword ||
|
||||
newHost.keyType !== existingHost.keyType ||
|
||||
newHost.credentialId !== existingHost.credentialId ||
|
||||
newHost.defaultPath !== existingHost.defaultPath ||
|
||||
JSON.stringify(newHost.tags) !==
|
||||
JSON.stringify(existingHost.tags) ||
|
||||
@@ -247,6 +249,10 @@ export function LeftSidebar({
|
||||
return () => clearTimeout(handler);
|
||||
}, [search]);
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem("leftSidebarOpen", JSON.stringify(isSidebarOpen));
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
const filteredHosts = React.useMemo(() => {
|
||||
if (!debouncedSearch.trim()) return hosts;
|
||||
const q = debouncedSearch.trim().toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user