Dev 1.8.0 #399

Merged
LukeGus merged 42 commits from dev-1.8.0 into main 2025-10-15 03:50:34 +00:00
141 changed files with 10880 additions and 6757 deletions

21
.commitlintrc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"revert"
]
],
"subject-case": [0]
}
}

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Matches multiple files with brace expansion notation
[*.{js,jsx,ts,tsx,json,css,scss,md,yml,yaml}]
indent_style = space
indent_size = 2
# Markdown files
[*.md]
trim_trailing_whitespace = false

36
.gitattributes vendored Normal file
View File

@@ -0,0 +1,36 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf
# Source code
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.md text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Scripts
*.sh text eol=lf
*.bash text eol=lf
# Windows scripts should use CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

View File

@@ -1,82 +0,0 @@
name: Bug report
description: Create a report to help Termix improve
title: "[BUG]"
labels: [bug]
assignees: []
body:
- type: input
id: title
attributes:
label: Title
description: Brief, descriptive title for the bug
placeholder: "Brief description of the bug"
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: How are you using Termix?
options:
- Website - Firefox
- Website - Safari
- Website - Chrome
- Website - Other Browser
- App - Windows
- App - Linux
- App - iOS
- App - Android
validations:
required: true
- type: dropdown
id: server-installation-method
attributes:
label: Server Installation Method
description: How is the Termix server installed?
options:
- Docker
- Manual Build
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Find your version in the User Profile tab
placeholder: "e.g., 1.7.0"
validations:
required: true
- type: checkboxes
id: troubleshooting
attributes:
label: Troubleshooting
description: Please check all that apply
options:
- label: I have examined logs and tried to find the issue
- label: I have reviewed opened and closed issues
- label: I have tried restarting the application
- type: textarea
id: problem-description
attributes:
label: The Problem
description: Describe the bug in detail. Include as much information as possible with screenshots if applicable.
placeholder: "Describe what went wrong..."
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: How to Reproduce
description: Use as few steps as possible to reproduce the issue
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other context about the problem
placeholder: "Add any other context about the problem here..."

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Support Center
url: https://github.com/Termix-SSH/Support/issues
about: Report any feature requests or bugs in the support center
- name: Discord
url: https://discord.gg/jVQGdvHDrf
about: Official Termix Discord server for general discussion and quick support

View File

@@ -1,36 +0,0 @@
name: Feature request
description: Suggest an idea for Termix
title: "[FEATURE]"
labels: [enhancement]
assignees: []
body:
- type: input
id: title
attributes:
label: Title
description: Brief, descriptive title for the feature request
placeholder: "Brief description of the feature"
validations:
required: true
- type: textarea
id: related-issue
attributes:
label: Is it related to an issue?
description: Describe the problem this feature would solve
placeholder: "Describe what problem this feature would solve..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: The Solution
description: Describe your proposed solution in detail
placeholder: "Describe how you envision this feature working..."
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other context or screenshots about the feature request
placeholder: "Add any other context about the feature request here..."

View File

@@ -28,4 +28,4 @@ _(Optional: add before/after screenshots, GIFs, or console output)_
- [ ] Code follows project style guidelines - [ ] Code follows project style guidelines
- [ ] Supports mobile and desktop UI/app (if applicable) - [ ] Supports mobile and desktop UI/app (if applicable)
- [ ] I have read [Contributing.md](https://github.com/LukeGus/Termix/blob/main/CONTRIBUTING.md) - [ ] I have read [Contributing.md](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)

View File

@@ -18,7 +18,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: blacksmith-4vcpu-ubuntu-2404
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v5
@@ -30,13 +30,8 @@ jobs:
with: with:
platforms: arm64 platforms: arm64
- name: Set up Docker Buildx - name: Setup Blacksmith Builder
uses: docker/setup-buildx-action@v3 uses: useblacksmith/setup-docker-builder@v1
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
image=moby/buildkit:master
network=host
- name: Cache npm dependencies - name: Cache npm dependencies
uses: actions/cache@v4 uses: actions/cache@v4
@@ -99,7 +94,7 @@ jobs:
fi fi
- name: Build and Push Multi-Arch Docker Image - name: Build and Push Multi-Arch Docker Image
uses: docker/build-push-action@v6 uses: useblacksmith/build-push-action@v2
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
@@ -109,8 +104,6 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
build-args: | build-args: |
BUILDKIT_INLINE_CACHE=1 BUILDKIT_INLINE_CACHE=1
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 BUILDKIT_CONTEXT_KEEP_GIT_DIR=1

View File

@@ -12,6 +12,7 @@ on:
- all - all
- windows - windows
- linux - linux
- macos
jobs: jobs:
build-windows: build-windows:
@@ -58,7 +59,7 @@ jobs:
retention-days: 30 retention-days: 30
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == ''
steps: steps:
@@ -91,3 +92,105 @@ jobs:
name: Termix-Linux-Portable name: Termix-Linux-Portable
path: Termix-Linux-Portable.zip path: Termix-Linux-Portable.zip
retention-days: 30 retention-days: 30
build-macos:
runs-on: macos-latest
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == ''
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build macOS DMG
run: npm run build:mac-dmg
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
- name: Build macOS Zip
run: npm run build:mac-zip
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
- name: Upload macOS DMG Artifact
uses: actions/upload-artifact@v4
with:
name: Termix-macOS-DMG
path: release/*.dmg
retention-days: 30
- name: Upload macOS Zip Artifact
uses: actions/upload-artifact@v4
with:
name: Termix-macOS-Zip
path: release/*.zip
retention-days: 30
build-macos-mas:
runs-on: macos-latest
if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all'
needs: []
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Import Code Signing Certificates
if: github.event_name == 'workflow_dispatch' && (vars.MAC_BUILD_CERTIFICATE_BASE64 != '' || secrets.MAC_BUILD_CERTIFICATE_BASE64 != '')
env:
MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}
MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }}
MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Build macOS App Store Package
run: npm run build:mac-mas
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_IDENTITY_AUTO_DISCOVERY: false
- name: Upload macOS MAS Artifact
uses: actions/upload-artifact@v4
with:
name: Termix-macOS-MAS
path: release/mas/*.pkg
retention-days: 30
- name: Clean up keychain
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

35
.github/workflows/pr-check.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: PR Check
on:
pull_request:
branches: [main, dev-*]
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: |
rm -rf node_modules package-lock.json
npm install
- name: Run ESLint
run: npx eslint .
- name: Run Prettier check
run: npx prettier --check .
- name: Type check
run: npx tsc --noEmit
- name: Build
run: npm run build

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

2
.nvmrc
View File

@@ -1 +1 @@
22 20

View File

@@ -1,3 +1,23 @@
# Ignore artifacts: # Ignore artifacts:
build build
coverage coverage
dist
dist-ssr
release
# Dependencies
node_modules
package-lock.json
pnpm-lock.yaml
yarn.lock
# Database
db
# Environment
.env
# Misc
*.min.js
*.min.css
openapi.json

View File

@@ -1 +1,9 @@
{} {
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"arrowParens": "always",
"endOfLine": "lf"
}

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig"
]
}

View File

@@ -10,7 +10,7 @@
1. Clone the repository: 1. Clone the repository:
```sh ```sh
git clone https://github.com/LukeGus/Termix git clone https://github.com/Termix-SSH/Termix
``` ```
2. Install the dependencies: 2. Install the dependencies:
```sh ```sh
@@ -31,7 +31,7 @@ This will start the backend and the frontend Vite server. You can access Termix
## Contributing ## Contributing
1. **Fork the repository**: Click the "Fork" button at the top right of 1. **Fork the repository**: Click the "Fork" button at the top right of
the [repository page](https://github.com/LukeGus/Termix). the [repository page](https://github.com/Termix-SSH/Termix).
2. **Create a new branch**: 2. **Create a new branch**:
```sh ```sh
git checkout -b feature/my-new-feature git checkout -b feature/my-new-feature
@@ -101,6 +101,6 @@ This will start the backend and the frontend Vite server. You can access Termix
## Support ## Support
If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`.
channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
repo. channel, however, response times may be longer.

View File

@@ -5,9 +5,9 @@
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文 <img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文
</p> </p>
![GitHub Repo stars](https://img.shields.io/github/stars/LukeGus/Termix?style=flat&label=Stars) ![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) ![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) ![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a> <a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center"> <p align="center">
@@ -29,7 +29,7 @@
<br /> <br />
<p align="center"> <p align="center">
<a href="https://github.com/LukeGus/Termix"> <a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a> <img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p> </p>
@@ -39,34 +39,44 @@
# 概览 # 概览
<p align="center"> <p align="center">
<a href="https://github.com/LukeGus/Termix"> <a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a> <img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
</p> </p>
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案通过一个直观的界面管理你的服务器和基础设施。Termix Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案通过一个直观的界面管理你的服务器和基础设施。Termix
提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。 提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。
# 功能 # 功能
- **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统 - **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统
- **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控 - **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控
- **远程文件编辑器** - 直接在远程服务器编辑文件,支持语法高亮和文件管理功能(上传、删除、重命名等) - **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图片、音频和视频。无缝上传、下载、重命名、删除和移动文件。
- **SSH 主机管理器** - 保存、组织和管理 SSH 连接,支持标签和文件夹 - **SSH 主机管理器** - 保存、组织和管理 SSH 连接,支持标签和文件夹,轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
- **服务器统计** - 查看任意 SSH 服务器的 CPU、内存和硬盘使用情况 - **服务器统计** - 查看任意 SSH 服务器的 CPU、内存和硬盘使用情况
- **用户认证** - 安全的用户管理支持管理员控制、OIDC 和双因素认证TOTP - **用户认证** - 安全的用户管理支持管理员控制、OIDC 和双因素认证TOTP
- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁界面 - **数据库加密** - SQLite 数据库文件在静态时加密,支持自动加密/解密
- **语言支持** - 内置中英文支持 - **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据,支持增量同步
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁桌面/移动友好界面
- **语言支持** - 内置英语、中文和德语支持
- **平台支持** - 提供 Web 应用、桌面应用程序Windows 和 Linux以及 iOS 和 Android 专用移动应用。计划支持 macOS 和 iPadOS。
# 计划功能 # 计划功能
- **增强管理员控制** - 提供更精细的用户和管理员权限控制、共享主机等功能 查看 [项目](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果你想贡献代码,请参阅 [贡献指南](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。
- **主题定制** - 修改所有工具的主题风格
- **增强终端支持** - 添加更多终端协议,如 VNC 和 RDP有类似 Apache Guacamole 的 RDP 集成经验者请通过创建 issue 联系我)
- **移动端支持** - 支持移动应用或 Termix 网站移动版,让你在手机上管理服务器
# 安装 # 安装
访问 Termix [文档](https://docs.termix.site/install) 获取安装信息。或者可以参考以下示例 docker-compose 文件 支持的设备
- 网站(任何现代浏览器,如 Google、Safari 和 Firefox
- Windows应用程序
- Linux应用程序
- iOS应用程序
- Android应用程序
- iPadOS 和 macOS 正在开发中
访问 Termix [文档](https://docs.termix.site/install) 获取所有平台的安装信息。或者可以参考以下示例 docker-compose 文件:
```yaml ```yaml
services: services:
@@ -88,8 +98,9 @@ volumes:
# 支持 # 支持
如果你需要 Termix 的帮助,可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 如果你需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`
服务器并访问支持频道。你也可以 [GitHub](https://github.com/LukeGus/Termix/issues) 仓库提交 issue 或 pull request。 请尽可能详细地描述你的问题,最好使用英语。你也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持
频道,但响应时间可能较长。
# 展示 # 展示
@@ -99,17 +110,25 @@ volumes:
</p> </p>
<p align="center"> <p align="center">
<img src="./repo-images/Image 3.png" width="250" alt="Termix Demo 3"/> <img src="./repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="./repo-images/Image 4.png" width="250" alt="Termix Demo 4"/> <img src="./repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
<img src="./repo-images/Image 5.png" width="250" alt="Termix Demo 5"/>
</p> </p>
<p align="center"> <p align="center">
<video src="https://github.com/user-attachments/assets/f9caa061-10dc-4173-ae7d-c6d42f05cf56" width="800" controls> <img src="./repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="./repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</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>
你的浏览器不支持 video 标签。 你的浏览器不支持 video 标签。
</video> </video>
</p> </p>
# 许可证 # 许可证
根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。 根据 Apache License Version 2.0 发布。更多信息请参见 LICENSE。

View File

@@ -5,9 +5,9 @@
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> <a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a>
</p> </p>
![GitHub Repo stars](https://img.shields.io/github/stars/LukeGus/Termix?style=flat&label=Stars) ![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) ![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) ![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a> <a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center"> <p align="center">
@@ -29,7 +29,7 @@
<br /> <br />
<p align="center"> <p align="center">
<a href="https://github.com/LukeGus/Termix"> <a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a> <img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p> </p>
@@ -39,7 +39,7 @@ If you would like, you can support the project here!\
# Overview # Overview
<p align="center"> <p align="center">
<a href="https://github.com/LukeGus/Termix"> <a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a> <img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
</p> </p>
@@ -52,7 +52,7 @@ access, SSH tunneling capabilities, and remote file management, with many more t
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system - **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 - **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. - **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 - **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 - **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 - **Database Encryption** - SQLite database files encrypted at rest with automatic encryption/decryption
@@ -64,7 +64,7 @@ access, SSH tunneling capabilities, and remote file management, with many more t
# Planned Features # Planned Features
See [Projects](https://github.com/users/LukeGus/projects/3) for all planned features. If you are looking to contribute, see [Contributing](https://github.com/LukeGus/Termix/blob/main/CONTRIBUTING.md). See [Projects](https://github.com/orgs/Termix-SSH/projects/2) for all planned features. If you are looking to contribute, see [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Installation # Installation
@@ -100,9 +100,9 @@ volumes:
# Support # Support
If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`.
channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
repo. channel, however, response times may be longer.
# Show-off # Show-off

View File

@@ -2,4 +2,4 @@
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report any vulnerabilities to [GitHub Security](https://github.com/LukeGus/Termix/security/advisories). Please report any vulnerabilities to [GitHub Security](https://github.com/Termix-SSH/Termix/security/advisories).

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

31
build/notarize.js Normal file
View File

@@ -0,0 +1,31 @@
const { notarize } = require('@electron/notarize');
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
const appPath = `${appOutDir}/${appName}.app`;
const appleId = process.env.APPLE_ID;
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD;
const teamId = process.env.APPLE_TEAM_ID;
if (!appleId || !appleIdPassword || !teamId) {
return;
}
try {
await notarize({
appPath: appPath,
appleId: appleId,
appleIdPassword: appleIdPassword,
teamId: teamId,
});
} catch (error) {
throw error;
}
};

View File

@@ -58,5 +58,60 @@
"StartupWMClass": "termix" "StartupWMClass": "termix"
} }
} }
} },
"mac": {
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
},
{
"target": "zip",
"arch": ["x64", "arm64"]
},
{
"target": "mas",
"arch": ["x64", "arm64"]
}
],
"icon": "public/icon.icns",
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"type": "distribution",
"minimumSystemVersion": "10.15"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"artifactName": "${productName}-${version}-${arch}.${ext}",
"sign": false,
"writeUpdateInfo": false
},
"mas": {
"entitlements": "build/entitlements.mas.plist",
"entitlementsInherit": "build/entitlements.mas.inherit.plist",
"hardenedRuntime": false,
"gatekeeperAssess": false,
"asarUnpack": ["**/*.node"],
"type": "distribution",
"category": "public.app-category.developer-tools",
"extendInfo": {
"ElectronTeamID": "YOUR_TEAM_ID",
"ITSAppUsesNonExemptEncryption": false
}
},
"afterSign": "build/notarize.js"
} }

View File

@@ -106,11 +106,11 @@ ipcMain.handle("get-app-version", () => {
}); });
const GITHUB_API_BASE = "https://api.github.com"; const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "LukeGus"; const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix"; const REPO_NAME = "Termix";
const githubCache = new Map(); const githubCache = new Map();
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes const CACHE_DURATION = 30 * 60 * 1000;
async function fetchGitHubAPI(endpoint, cacheKey) { async function fetchGitHubAPI(endpoint, cacheKey) {
const cached = githubCache.get(cacheKey); const cached = githubCache.get(cacheKey);

561
lint-output-current.txt Normal file
View File

@@ -0,0 +1,561 @@
> termix@1.7.2 lint
> eslint .
C:\Users\29037\WebstormProjects\Termix\src\backend\database\routes\users.ts
705:8 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\file-manager.ts
169:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
435:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
465:13 error Empty block statement no-empty
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\server-stats.ts
64:34 error '_reject' is defined but never used @typescript-eslint/no-unused-vars
197:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1130:9 error 'now' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\terminal.ts
157:5 error 'userPayload' is assigned a value but never used @typescript-eslint/no-unused-vars
350:13 error 'cols' is assigned a value but never used @typescript-eslint/no-unused-vars
350:19 error 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\tunnel.ts
840:34 error 'data' is defined but never used @typescript-eslint/no-unused-vars
906:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1068:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1442:21 error 'hasSourcePassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1443:21 error 'hasSourceKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1444:21 error 'hasEndpointPassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1447:21 error 'hasEndpointKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1472:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\starter.ts
78:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
131:47 error 'promise' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auto-ssl-setup.ts
104:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\data-crypto.ts
100:31 error 'plaintextFields' is assigned a value but never used @typescript-eslint/no-unused-vars
455:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-file-encryption.ts
33:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
81:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
166:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
236:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
304:13 error 'currentFingerprint' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-migration.ts
247:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\lazy-field-encryption.ts
185:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
282:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\logger.ts
14:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\simple-db-ops.ts
134:5 error '_userId' is defined but never used @typescript-eslint/no-unused-vars
136:5 warning Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-explicit-any')
139:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
157:5 error '_tableName' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\ssh-key-utils.ts
52:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
243:20 error 'error' is defined but never used @typescript-eslint/no-unused-vars
321:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-crypto.ts
198:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
278:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
301:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
420:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
460:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-import.ts
162:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
216:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
272:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
359:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\theme-provider.tsx
66:14 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\badge.tsx
46:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\button.tsx
62:18 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\form.tsx
158:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\password-input.tsx
8:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\shadcn-io\status\index.tsx
21:3 error 'className' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sidebar.tsx
12:3 error 'Sheet' is defined but never used @typescript-eslint/no-unused-vars
13:3 error 'SheetContent' is defined but never used @typescript-eslint/no-unused-vars
14:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
29:7 error 'SIDEBAR_WIDTH_MOBILE' is assigned a value but never used @typescript-eslint/no-unused-vars
164:11 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:28 error 'openMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:40 error 'setOpenMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
724:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sonner.tsx
11:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
34:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\ui\textarea.tsx
5:18 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\version-check-modal.tsx
4:10 error 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars
4:21 error 'X' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'onDismiss' is defined but never used @typescript-eslint/no-unused-vars
20:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:6 warning React Hook useEffect has missing dependencies: 'checkForUpdates' and 'onContinue'. Either include them or remove the dependency array. If 'onContinue' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
50:9 error 'handleVersionDismiss' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\lib\frontend-logger.ts
20:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
221:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
247:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
268:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
282:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
301:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\main.tsx
12:10 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
56:10 error Fast refresh only works when a file has exports. Move your component(s) to a separate file react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Admin\AdminSettings.tsx
29:3 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
49:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
99:10 error 'securityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
99:31 error 'setSecurityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
127:6 warning React Hook React.useEffect has missing dependencies: 'fetchUsers' and 't'. Either include them or remove the dependency array react-hooks/exhaustive-deps
149:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
171:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
281:16 error 'err' is defined but never used @typescript-eslint/no-unused-vars
295:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
366:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
458:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialEditor.tsx
45:10 error 'credentials' is assigned a value but never used @typescript-eslint/no-unused-vars
47:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
98:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
158:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
201:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
224:6 warning React Hook useEffect has a missing dependency: 'editingCredential'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialSelector.tsx
38:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
48:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialViewer.tsx
67:6 warning React Hook useEffect has missing dependencies: 'fetchCredentialDetails' and 'fetchHostsUsing'. Either include them or remove the dependency array react-hooks/exhaustive-deps
73:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
82:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
100:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialsManager.tsx
15:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetFooter' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
18:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
21:3 error 'Select' is defined but never used @typescript-eslint/no-unused-vars
22:3 error 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars
24:3 error 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars
25:3 error 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars
40:3 error 'Pin' is defined but never used @typescript-eslint/no-unused-vars
78:29 error 'setViewingCredential' is assigned a value but never used @typescript-eslint/no-unused-vars
91:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
102:6 warning React Hook useEffect has a missing dependency: 'fetchCredentials'. Either include it or remove the dependency array react-hooks/exhaustive-deps
156:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
227:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
288:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
328:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
362:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManager.tsx
26:3 error 'Eye' is defined but never used @typescript-eslint/no-unused-vars
27:3 error 'Settings' is defined but never used @typescript-eslint/no-unused-vars
90:23 error 'setCurrentHost' is assigned a value but never used @typescript-eslint/no-unused-vars
148:26 error 'selectFile' is assigned a value but never used @typescript-eslint/no-unused-vars
148:38 error 'selectAll' is assigned a value but never used @typescript-eslint/no-unused-vars
151:11 error 'isDragging' is assigned a value but never used @typescript-eslint/no-unused-vars
208:6 warning React Hook useEffect has a missing dependency: 'initializeSSHConnection'. Either include it or remove the dependency array react-hooks/exhaustive-deps
386:5 warning React Hook useCallback has a missing dependency: 'handleCloseWithError'. Either include it or remove the dependency array react-hooks/exhaustive-deps
455:6 warning React Hook useEffect has a missing dependency: 'handleOpenTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
787:49 error 'editMode' is assigned a value but never used @typescript-eslint/no-unused-vars
837:12 error 'handleFileEdit' is defined but never used @typescript-eslint/no-unused-vars
841:12 error 'handleFileView' is defined but never used @typescript-eslint/no-unused-vars
1356:23 error 'index' is defined but never used @typescript-eslint/no-unused-vars
1665:6 warning React Hook useEffect has a missing dependency: 'loadPinnedFiles'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerContextMenu.tsx
16:3 error 'Share' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars
193:9 error 'hasDirectories' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerGrid.tsx
169:3 error 'onFileSelect' is defined but never used @typescript-eslint/no-unused-vars
191:3 error 'onSystemDragStart' is defined but never used @typescript-eslint/no-unused-vars
371:6 warning React Hook useEffect has missing dependencies: 'historyIndex' and 'navigationHistory'. Either include them or remove the dependency array react-hooks/exhaustive-deps
431:9 error 'handlePathInputKeyDown' is assigned a value but never used @typescript-eslint/no-unused-vars
483:5 warning React Hook useCallback has an unnecessary dependency: 'dragState.counter'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
620:34 error 'e' is defined but never used @typescript-eslint/no-unused-vars
674:5 warning React Hook useCallback has an unnecessary dependency: 'onDownload'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
810:6 warning React Hook useEffect has missing dependencies: 'hasClipboard' and 'onStartEdit'. Either include them or remove the dependency array. If 'onStartEdit' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerSidebar.tsx
79:3 error 'onLoadDirectory' is defined but never used @typescript-eslint/no-unused-vars
107:6 warning React Hook useEffect has a missing dependency: 'loadQuickAccessData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
113:6 warning React Hook useEffect has a missing dependency: 'loadDirectoryTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DiffViewer.tsx
64:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
99:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
160:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
206:6 warning React Hook useEffect has a missing dependency: 'loadFileContents'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DraggableWindow.tsx
3:17 error 'Square' is defined but never used @typescript-eslint/no-unused-vars
208:5 warning React Hook useCallback has an unnecessary dependency: 'position'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileViewer.tsx
63:8 error 'ReactPlayer' is defined but never used @typescript-eslint/no-unused-vars
293:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
314:10 error 'originalContent' is assigned a value but never used @typescript-eslint/no-unused-vars
329:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
381:9 warning The 'handleSave' function makes the dependencies of useEffect Hook (at line 411) change on every render. To fix this, wrap the definition of 'handleSave' in its own useCallback() Hook react-hooks/exhaustive-deps
979:29 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1100:32 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1384:33 error 'audio' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileWindow.tsx
59:53 error 'updateWindow' is assigned a value but never used @typescript-eslint/no-unused-vars
160:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
179:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook useEffect has missing dependencies: 'closeWindow', 'ensureSSHConnection', 'onFileNotFound', 't', and 'windowId'. Either include them or remove the dependency array. If 'onFileNotFound' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
232:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
261:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
338:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\TerminalWindow.tsx
41:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
53:9 error 'handleMinimize' is assigned a value but never used @typescript-eslint/no-unused-vars
77:3 error React Hook "React.useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\WindowManager.tsx
132:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManager.tsx
18:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
25:62 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:29 error 'updatedHost' is defined but never used @typescript-eslint/no-unused-vars
38:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerEditor.tsx
41:28 error 'WidgetType' is defined but never used @typescript-eslint/no-unused-vars
63:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
80:10 error 'hosts' is assigned a value but never used @typescript-eslint/no-unused-vars
83:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
84:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
305:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
390:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
423:6 warning React Hook useEffect has missing dependencies: 'editingHost' and 'form'. Either include them or remove the dependency array react-hooks/exhaustive-deps
443:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
536:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerViewer.tsx
83:6 warning React Hook useEffect has a missing dependency: 'fetchHosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
109:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
125:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
163:47 error 'actualAuthType' is defined but never used @typescript-eslint/no-unused-vars
169:13 error '_' is defined but never used @typescript-eslint/no-unused-vars
188:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
225:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
254:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
294:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
328:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
442:6 warning React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\Server.tsx
151:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
168:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
177:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
260:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\DiskWidget.tsx
15:39 error 'metricsHistory' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\NetworkWidget.tsx
14:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\ProcessesWidget.tsx
14:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\SystemWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\UptimeWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\SnippetsSidebar.tsx
55:6 warning React Hook useEffect has a missing dependency: 'fetchSnippets'. Either include it or remove the dependency array react-hooks/exhaustive-deps
63:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
97:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
128:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\Terminal.tsx
83:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
84:12 error 'isAuthenticated' is assigned a value but never used @typescript-eslint/no-unused-vars
227:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
230:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
493:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
529:37 error 'event' is defined but never used @typescript-eslint/no-unused-vars
713:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
753:8 warning React Hook useEffect has missing dependencies: 'connectToHost' and 'hardRefresh'. Either include them or remove the dependency array react-hooks/exhaustive-deps
772:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
784:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Tunnel\TunnelViewer.tsx
6:3 error 'TunnelConnection' is defined but never used @typescript-eslint/no-unused-vars
18:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
66:40 error '_host' is defined but never used @typescript-eslint/no-unused-vars
66:47 error '_index' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\DesktopApp.tsx
15:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
18:10 error 'view' is assigned a value but never used @typescript-eslint/no-unused-vars
19:10 error 'mountedViews' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Electron Only\ServerConfig.tsx
77:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
120:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\Homepage.tsx
7:10 error 'useTranslation' is defined but never used @typescript-eslint/no-unused-vars
28:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
29:10 error 'username' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAlertManager.tsx
20:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
27:6 warning React Hook useEffect has a missing dependency: 'fetchUserAlerts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
55:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
80:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
54:3 error 'dbError' is defined but never used @typescript-eslint/no-unused-vars
68:10 error 'visibility' is assigned a value but only used as a type @typescript-eslint/no-unused-vars
74:9 error 'toggleVisibility' is assigned a value but never used @typescript-eslint/no-unused-vars
78:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
163:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
213:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
236:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
262:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
281:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
385:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
418:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
431:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
465:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
481:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
518:9 error 'retryDatabaseConnection' is assigned a value but never used @typescript-eslint/no-unused-vars
531:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
545:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HompageUpdateLog.tsx
69:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
74:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\AppView.tsx
14:3 error 'LucideRefreshCcw' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'LucideRefreshCw' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'RefreshCcwDot' is defined but never used @typescript-eslint/no-unused-vars
117:6 warning React Hook useEffect has a missing dependency: 'hideThenFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
117:40 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
121:6 warning React Hook useEffect has a missing dependency: 'scheduleMeasureAndFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
133:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
142:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\Host.tsx
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\LeftSidebar.tsx
6:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
90:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
91:3 error 'getView' is defined but never used @typescript-eslint/no-unused-vars
152:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
215:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
218:6 warning React Hook React.useCallback has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
290:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
300:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
323:11 error 'jwt' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Tabs\TabContext.tsx
22:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
101:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
143:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\TopNavbar.tsx
8:3 error 'Accordion' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'AccordionContent' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'AccordionItem' is defined but never used @typescript-eslint/no-unused-vars
11:3 error 'AccordionTrigger' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\PasswordReset.tsx
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
83:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
113:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\TOTPSetup.tsx
69:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
89:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
108:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
125:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\UserProfile.tsx
11:24 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
42:6 warning React Hook useEffect has missing dependencies: 'fetchUserInfo' and 'fetchVersion'. Either include them or remove the dependency array react-hooks/exhaustive-deps
48:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
65:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\LeftSidebar.tsx
45:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Terminal\Terminal.tsx
15:10 error 'toast' is defined but never used @typescript-eslint/no-unused-vars
55:12 error 'isConnected' is assigned a value but never used @typescript-eslint/no-unused-vars
56:12 error 'isConnecting' is assigned a value but never used @typescript-eslint/no-unused-vars
57:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
160:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
163:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
394:8 warning React Hook useEffect has missing dependencies: 'hardRefresh', 'isAuthenticated', and 'setupWebSocketListeners'. Either include them or remove the dependency array react-hooks/exhaustive-deps
404:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
413:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
67:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
153:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
197:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
220:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
243:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
246:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
265:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
369:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
402:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
415:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
449:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
466:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\MobileApp.tsx
10:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
17:37 error 'removeTab' is assigned a value but never used @typescript-eslint/no-unused-vars
23:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
69:6 warning React Hook useEffect has a missing dependency: 'fitCurrentTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
98:6 warning React Hook React.useEffect has missing dependencies: 'fitCurrentTerminal' and 'tabs.length'. Either include them or remove the dependency array react-hooks/exhaustive-deps
131:23 error 'id' is defined but never used @typescript-eslint/no-unused-vars
135:24 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\LeftSidebar.tsx
46:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToDesktop.ts
120:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
140:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
229:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
254:35 error 'onSuccess' is assigned a value but never used @typescript-eslint/no-unused-vars
277:5 warning React Hook useCallback has unnecessary dependencies: 'sshHost' and 'sshSessionId'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToSystemDesktop.ts
26:3 error 'sshHost' is defined but never used @typescript-eslint/no-unused-vars
40:9 error 'getLastSaveDirectory' is assigned a value but never used @typescript-eslint/no-unused-vars
68:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
167:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
170:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
245:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
263:5 warning React Hook useCallback has missing dependencies: 'createFileBlob' and 'createZipBlob'. Either include them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\main-axios.ts
7:3 error 'Credential' is defined but never used @typescript-eslint/no-unused-vars
8:3 error 'CredentialData' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'HostInfo' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'ApiResponse' is defined but never used @typescript-eslint/no-unused-vars
346:3 error 'apiPort' is assigned a value but never used @typescript-eslint/no-unused-vars
994:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1031:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1068:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
✖ 385 problems (326 errors, 59 warnings)
0 errors and 1 warning potentially fixable with the `--fix` option.

561
lint-output-final.txt Normal file
View File

@@ -0,0 +1,561 @@
> termix@1.7.2 lint
> eslint .
C:\Users\29037\WebstormProjects\Termix\src\backend\database\routes\users.ts
705:8 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\file-manager.ts
169:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
435:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
465:13 error Empty block statement no-empty
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\server-stats.ts
64:34 error '_reject' is defined but never used @typescript-eslint/no-unused-vars
197:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1130:9 error 'now' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\terminal.ts
157:5 error 'userPayload' is assigned a value but never used @typescript-eslint/no-unused-vars
350:13 error 'cols' is assigned a value but never used @typescript-eslint/no-unused-vars
350:19 error 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\tunnel.ts
840:34 error 'data' is defined but never used @typescript-eslint/no-unused-vars
906:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1068:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1442:21 error 'hasSourcePassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1443:21 error 'hasSourceKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1444:21 error 'hasEndpointPassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1447:21 error 'hasEndpointKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1472:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\starter.ts
78:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
131:47 error 'promise' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auto-ssl-setup.ts
104:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\data-crypto.ts
100:31 error 'plaintextFields' is assigned a value but never used @typescript-eslint/no-unused-vars
455:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-file-encryption.ts
33:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
81:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
166:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
236:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
304:13 error 'currentFingerprint' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-migration.ts
247:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\lazy-field-encryption.ts
185:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
282:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\logger.ts
14:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\simple-db-ops.ts
134:5 error '_userId' is defined but never used @typescript-eslint/no-unused-vars
136:5 warning Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-explicit-any')
139:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
157:5 error '_tableName' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\ssh-key-utils.ts
52:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
243:20 error 'error' is defined but never used @typescript-eslint/no-unused-vars
321:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-crypto.ts
198:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
278:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
301:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
420:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
460:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-import.ts
162:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
216:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
272:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
359:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\theme-provider.tsx
66:14 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\badge.tsx
46:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\button.tsx
62:18 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\form.tsx
158:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\password-input.tsx
8:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\shadcn-io\status\index.tsx
21:3 error 'className' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sidebar.tsx
12:3 error 'Sheet' is defined but never used @typescript-eslint/no-unused-vars
13:3 error 'SheetContent' is defined but never used @typescript-eslint/no-unused-vars
14:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
29:7 error 'SIDEBAR_WIDTH_MOBILE' is assigned a value but never used @typescript-eslint/no-unused-vars
164:11 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:28 error 'openMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:40 error 'setOpenMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
724:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sonner.tsx
11:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
34:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\ui\textarea.tsx
5:18 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\version-check-modal.tsx
4:10 error 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars
4:21 error 'X' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'onDismiss' is defined but never used @typescript-eslint/no-unused-vars
20:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:6 warning React Hook useEffect has missing dependencies: 'checkForUpdates' and 'onContinue'. Either include them or remove the dependency array. If 'onContinue' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
50:9 error 'handleVersionDismiss' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\lib\frontend-logger.ts
20:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
221:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
247:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
268:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
282:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
301:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\main.tsx
12:10 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
56:10 error Fast refresh only works when a file has exports. Move your component(s) to a separate file react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Admin\AdminSettings.tsx
29:3 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
49:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
99:10 error 'securityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
99:31 error 'setSecurityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
127:6 warning React Hook React.useEffect has missing dependencies: 'fetchUsers' and 't'. Either include them or remove the dependency array react-hooks/exhaustive-deps
149:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
171:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
281:16 error 'err' is defined but never used @typescript-eslint/no-unused-vars
295:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
366:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
458:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialEditor.tsx
45:10 error 'credentials' is assigned a value but never used @typescript-eslint/no-unused-vars
47:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
98:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
158:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
201:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
224:6 warning React Hook useEffect has a missing dependency: 'editingCredential'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialSelector.tsx
38:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
48:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialViewer.tsx
67:6 warning React Hook useEffect has missing dependencies: 'fetchCredentialDetails' and 'fetchHostsUsing'. Either include them or remove the dependency array react-hooks/exhaustive-deps
73:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
82:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
100:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialsManager.tsx
15:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetFooter' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
18:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
21:3 error 'Select' is defined but never used @typescript-eslint/no-unused-vars
22:3 error 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars
24:3 error 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars
25:3 error 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars
40:3 error 'Pin' is defined but never used @typescript-eslint/no-unused-vars
78:29 error 'setViewingCredential' is assigned a value but never used @typescript-eslint/no-unused-vars
91:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
102:6 warning React Hook useEffect has a missing dependency: 'fetchCredentials'. Either include it or remove the dependency array react-hooks/exhaustive-deps
156:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
227:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
288:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
328:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
362:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManager.tsx
26:3 error 'Eye' is defined but never used @typescript-eslint/no-unused-vars
27:3 error 'Settings' is defined but never used @typescript-eslint/no-unused-vars
90:23 error 'setCurrentHost' is assigned a value but never used @typescript-eslint/no-unused-vars
148:26 error 'selectFile' is assigned a value but never used @typescript-eslint/no-unused-vars
148:38 error 'selectAll' is assigned a value but never used @typescript-eslint/no-unused-vars
151:11 error 'isDragging' is assigned a value but never used @typescript-eslint/no-unused-vars
208:6 warning React Hook useEffect has a missing dependency: 'initializeSSHConnection'. Either include it or remove the dependency array react-hooks/exhaustive-deps
386:5 warning React Hook useCallback has a missing dependency: 'handleCloseWithError'. Either include it or remove the dependency array react-hooks/exhaustive-deps
455:6 warning React Hook useEffect has a missing dependency: 'handleOpenTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
787:49 error 'editMode' is assigned a value but never used @typescript-eslint/no-unused-vars
837:12 error 'handleFileEdit' is defined but never used @typescript-eslint/no-unused-vars
841:12 error 'handleFileView' is defined but never used @typescript-eslint/no-unused-vars
1356:23 error 'index' is defined but never used @typescript-eslint/no-unused-vars
1665:6 warning React Hook useEffect has a missing dependency: 'loadPinnedFiles'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerContextMenu.tsx
16:3 error 'Share' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars
193:9 error 'hasDirectories' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerGrid.tsx
169:3 error 'onFileSelect' is defined but never used @typescript-eslint/no-unused-vars
191:3 error 'onSystemDragStart' is defined but never used @typescript-eslint/no-unused-vars
371:6 warning React Hook useEffect has missing dependencies: 'historyIndex' and 'navigationHistory'. Either include them or remove the dependency array react-hooks/exhaustive-deps
431:9 error 'handlePathInputKeyDown' is assigned a value but never used @typescript-eslint/no-unused-vars
483:5 warning React Hook useCallback has an unnecessary dependency: 'dragState.counter'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
620:34 error 'e' is defined but never used @typescript-eslint/no-unused-vars
674:5 warning React Hook useCallback has an unnecessary dependency: 'onDownload'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
810:6 warning React Hook useEffect has missing dependencies: 'hasClipboard' and 'onStartEdit'. Either include them or remove the dependency array. If 'onStartEdit' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerSidebar.tsx
79:3 error 'onLoadDirectory' is defined but never used @typescript-eslint/no-unused-vars
107:6 warning React Hook useEffect has a missing dependency: 'loadQuickAccessData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
113:6 warning React Hook useEffect has a missing dependency: 'loadDirectoryTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DiffViewer.tsx
64:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
99:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
160:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
206:6 warning React Hook useEffect has a missing dependency: 'loadFileContents'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DraggableWindow.tsx
3:17 error 'Square' is defined but never used @typescript-eslint/no-unused-vars
208:5 warning React Hook useCallback has an unnecessary dependency: 'position'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileViewer.tsx
63:8 error 'ReactPlayer' is defined but never used @typescript-eslint/no-unused-vars
293:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
314:10 error 'originalContent' is assigned a value but never used @typescript-eslint/no-unused-vars
329:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
381:9 warning The 'handleSave' function makes the dependencies of useEffect Hook (at line 411) change on every render. To fix this, wrap the definition of 'handleSave' in its own useCallback() Hook react-hooks/exhaustive-deps
979:29 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1100:32 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1384:33 error 'audio' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileWindow.tsx
59:53 error 'updateWindow' is assigned a value but never used @typescript-eslint/no-unused-vars
160:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
179:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook useEffect has missing dependencies: 'closeWindow', 'ensureSSHConnection', 'onFileNotFound', 't', and 'windowId'. Either include them or remove the dependency array. If 'onFileNotFound' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
232:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
261:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
338:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\TerminalWindow.tsx
41:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
53:9 error 'handleMinimize' is assigned a value but never used @typescript-eslint/no-unused-vars
77:3 error React Hook "React.useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\WindowManager.tsx
132:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManager.tsx
18:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
25:62 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:29 error 'updatedHost' is defined but never used @typescript-eslint/no-unused-vars
38:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerEditor.tsx
41:28 error 'WidgetType' is defined but never used @typescript-eslint/no-unused-vars
63:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
80:10 error 'hosts' is assigned a value but never used @typescript-eslint/no-unused-vars
83:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
84:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
305:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
390:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
423:6 warning React Hook useEffect has missing dependencies: 'editingHost' and 'form'. Either include them or remove the dependency array react-hooks/exhaustive-deps
443:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
536:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerViewer.tsx
83:6 warning React Hook useEffect has a missing dependency: 'fetchHosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
109:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
125:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
163:47 error 'actualAuthType' is defined but never used @typescript-eslint/no-unused-vars
169:13 error '_' is defined but never used @typescript-eslint/no-unused-vars
188:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
225:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
254:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
294:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
328:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
442:6 warning React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\Server.tsx
151:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
168:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
177:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
260:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\DiskWidget.tsx
15:39 error 'metricsHistory' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\NetworkWidget.tsx
14:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\ProcessesWidget.tsx
14:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\SystemWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\UptimeWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\SnippetsSidebar.tsx
55:6 warning React Hook useEffect has a missing dependency: 'fetchSnippets'. Either include it or remove the dependency array react-hooks/exhaustive-deps
63:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
97:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
128:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\Terminal.tsx
83:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
84:12 error 'isAuthenticated' is assigned a value but never used @typescript-eslint/no-unused-vars
227:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
230:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
493:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
529:37 error 'event' is defined but never used @typescript-eslint/no-unused-vars
713:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
753:8 warning React Hook useEffect has missing dependencies: 'connectToHost' and 'hardRefresh'. Either include them or remove the dependency array react-hooks/exhaustive-deps
772:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
784:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Tunnel\TunnelViewer.tsx
6:3 error 'TunnelConnection' is defined but never used @typescript-eslint/no-unused-vars
18:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
66:40 error '_host' is defined but never used @typescript-eslint/no-unused-vars
66:47 error '_index' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\DesktopApp.tsx
15:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
18:10 error 'view' is assigned a value but never used @typescript-eslint/no-unused-vars
19:10 error 'mountedViews' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Electron Only\ServerConfig.tsx
77:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
120:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\Homepage.tsx
7:10 error 'useTranslation' is defined but never used @typescript-eslint/no-unused-vars
28:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
29:10 error 'username' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAlertManager.tsx
20:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
27:6 warning React Hook useEffect has a missing dependency: 'fetchUserAlerts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
55:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
80:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
54:3 error 'dbError' is defined but never used @typescript-eslint/no-unused-vars
68:10 error 'visibility' is assigned a value but only used as a type @typescript-eslint/no-unused-vars
74:9 error 'toggleVisibility' is assigned a value but never used @typescript-eslint/no-unused-vars
78:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
163:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
213:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
236:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
262:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
281:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
385:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
418:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
431:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
465:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
481:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
518:9 error 'retryDatabaseConnection' is assigned a value but never used @typescript-eslint/no-unused-vars
531:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
545:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HompageUpdateLog.tsx
69:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
74:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\AppView.tsx
14:3 error 'LucideRefreshCcw' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'LucideRefreshCw' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'RefreshCcwDot' is defined but never used @typescript-eslint/no-unused-vars
117:6 warning React Hook useEffect has a missing dependency: 'hideThenFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
117:40 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
121:6 warning React Hook useEffect has a missing dependency: 'scheduleMeasureAndFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
133:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
142:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\Host.tsx
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\LeftSidebar.tsx
6:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
90:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
91:3 error 'getView' is defined but never used @typescript-eslint/no-unused-vars
152:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
215:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
218:6 warning React Hook React.useCallback has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
290:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
300:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
323:11 error 'jwt' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Tabs\TabContext.tsx
22:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
101:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
143:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\TopNavbar.tsx
8:3 error 'Accordion' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'AccordionContent' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'AccordionItem' is defined but never used @typescript-eslint/no-unused-vars
11:3 error 'AccordionTrigger' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\PasswordReset.tsx
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
83:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
113:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\TOTPSetup.tsx
69:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
89:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
108:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
125:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\UserProfile.tsx
11:24 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
42:6 warning React Hook useEffect has missing dependencies: 'fetchUserInfo' and 'fetchVersion'. Either include them or remove the dependency array react-hooks/exhaustive-deps
48:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
65:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\LeftSidebar.tsx
45:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Terminal\Terminal.tsx
15:10 error 'toast' is defined but never used @typescript-eslint/no-unused-vars
55:12 error 'isConnected' is assigned a value but never used @typescript-eslint/no-unused-vars
56:12 error 'isConnecting' is assigned a value but never used @typescript-eslint/no-unused-vars
57:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
160:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
163:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
394:8 warning React Hook useEffect has missing dependencies: 'hardRefresh', 'isAuthenticated', and 'setupWebSocketListeners'. Either include them or remove the dependency array react-hooks/exhaustive-deps
404:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
413:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
67:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
153:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
197:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
220:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
243:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
246:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
265:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
369:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
402:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
415:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
449:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
466:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\MobileApp.tsx
10:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
17:37 error 'removeTab' is assigned a value but never used @typescript-eslint/no-unused-vars
23:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
69:6 warning React Hook useEffect has a missing dependency: 'fitCurrentTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
98:6 warning React Hook React.useEffect has missing dependencies: 'fitCurrentTerminal' and 'tabs.length'. Either include them or remove the dependency array react-hooks/exhaustive-deps
131:23 error 'id' is defined but never used @typescript-eslint/no-unused-vars
135:24 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\LeftSidebar.tsx
46:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToDesktop.ts
120:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
140:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
229:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
254:35 error 'onSuccess' is assigned a value but never used @typescript-eslint/no-unused-vars
277:5 warning React Hook useCallback has unnecessary dependencies: 'sshHost' and 'sshSessionId'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToSystemDesktop.ts
26:3 error 'sshHost' is defined but never used @typescript-eslint/no-unused-vars
40:9 error 'getLastSaveDirectory' is assigned a value but never used @typescript-eslint/no-unused-vars
68:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
167:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
170:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
245:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
263:5 warning React Hook useCallback has missing dependencies: 'createFileBlob' and 'createZipBlob'. Either include them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\main-axios.ts
7:3 error 'Credential' is defined but never used @typescript-eslint/no-unused-vars
8:3 error 'CredentialData' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'HostInfo' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'ApiResponse' is defined but never used @typescript-eslint/no-unused-vars
346:3 error 'apiPort' is assigned a value but never used @typescript-eslint/no-unused-vars
994:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1031:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1068:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
✖ 385 problems (326 errors, 59 warnings)
0 errors and 1 warning potentially fixable with the `--fix` option.

593
lint-output-new.txt Normal file
View File

@@ -0,0 +1,593 @@
> termix@1.7.2 lint
> eslint .
C:\Users\29037\WebstormProjects\Termix\src\backend\database\routes\users.ts
688:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\file-manager.ts
169:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
435:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
465:13 error Empty block statement no-empty
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\server-stats.ts
64:34 error '_reject' is defined but never used @typescript-eslint/no-unused-vars
197:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1122:9 error 'now' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\terminal.ts
82:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
116:5 error 'userPayload' is assigned a value but never used @typescript-eslint/no-unused-vars
190:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
316:13 error 'cols' is assigned a value but never used @typescript-eslint/no-unused-vars
316:19 error 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars
633:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
779:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
788:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
800:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\tunnel.ts
830:34 error 'data' is defined but never used @typescript-eslint/no-unused-vars
896:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1053:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1427:21 error 'hasSourcePassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1428:21 error 'hasSourceKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1429:21 error 'hasEndpointPassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1432:21 error 'hasEndpointKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1457:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\starter.ts
78:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
131:47 error 'promise' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auth-manager.ts
166:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
197:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
198:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
205:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
218:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
260:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auto-ssl-setup.ts
104:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\data-crypto.ts
18:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
41:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
76:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
87:31 error 'plaintextFields' is assigned a value but never used @typescript-eslint/no-unused-vars
223:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
391:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
400:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
438:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-file-encryption.ts
33:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
81:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
166:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
236:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
304:13 error 'currentFingerprint' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-migration.ts
247:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\lazy-field-encryption.ts
185:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
282:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\logger.ts
14:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\simple-db-ops.ts
134:5 error '_userId' is defined but never used @typescript-eslint/no-unused-vars
154:5 error '_tableName' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\ssh-key-utils.ts
52:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
243:20 error 'error' is defined but never used @typescript-eslint/no-unused-vars
321:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-crypto.ts
198:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
278:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
301:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
420:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
460:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-import.ts
162:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
216:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
272:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
359:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\theme-provider.tsx
66:14 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\badge.tsx
46:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\button.tsx
62:18 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\form.tsx
158:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\password-input.tsx
8:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\shadcn-io\status\index.tsx
21:3 error 'className' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sidebar.tsx
12:3 error 'Sheet' is defined but never used @typescript-eslint/no-unused-vars
13:3 error 'SheetContent' is defined but never used @typescript-eslint/no-unused-vars
14:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
29:7 error 'SIDEBAR_WIDTH_MOBILE' is assigned a value but never used @typescript-eslint/no-unused-vars
164:11 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:28 error 'openMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:40 error 'setOpenMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
724:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sonner.tsx
11:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
34:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\ui\textarea.tsx
5:18 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\version-check-modal.tsx
4:10 error 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars
4:21 error 'X' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'onDismiss' is defined but never used @typescript-eslint/no-unused-vars
20:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:6 warning React Hook useEffect has missing dependencies: 'checkForUpdates' and 'onContinue'. Either include them or remove the dependency array. If 'onContinue' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
50:9 error 'handleVersionDismiss' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\lib\frontend-logger.ts
20:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
221:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
247:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
268:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
282:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
301:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\main.tsx
12:10 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
56:10 error Fast refresh only works when a file has exports. Move your component(s) to a separate file react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Admin\AdminSettings.tsx
29:3 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
49:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
99:10 error 'securityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
99:31 error 'setSecurityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
126:6 warning React Hook React.useEffect has missing dependencies: 'fetchUsers' and 't'. Either include them or remove the dependency array react-hooks/exhaustive-deps
147:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
168:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
275:16 error 'err' is defined but never used @typescript-eslint/no-unused-vars
289:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
360:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
452:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialEditor.tsx
45:10 error 'credentials' is assigned a value but never used @typescript-eslint/no-unused-vars
47:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
98:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
158:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
201:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
224:6 warning React Hook useEffect has a missing dependency: 'editingCredential'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialSelector.tsx
38:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
48:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialViewer.tsx
67:6 warning React Hook useEffect has missing dependencies: 'fetchCredentialDetails' and 'fetchHostsUsing'. Either include them or remove the dependency array react-hooks/exhaustive-deps
73:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
82:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
100:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialsManager.tsx
15:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetFooter' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
18:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
21:3 error 'Select' is defined but never used @typescript-eslint/no-unused-vars
22:3 error 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars
24:3 error 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars
25:3 error 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars
40:3 error 'Pin' is defined but never used @typescript-eslint/no-unused-vars
78:29 error 'setViewingCredential' is assigned a value but never used @typescript-eslint/no-unused-vars
91:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
102:6 warning React Hook useEffect has a missing dependency: 'fetchCredentials'. Either include it or remove the dependency array react-hooks/exhaustive-deps
156:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
227:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
288:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
328:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
362:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManager.tsx
26:3 error 'Eye' is defined but never used @typescript-eslint/no-unused-vars
27:3 error 'Settings' is defined but never used @typescript-eslint/no-unused-vars
90:23 error 'setCurrentHost' is assigned a value but never used @typescript-eslint/no-unused-vars
148:26 error 'selectFile' is assigned a value but never used @typescript-eslint/no-unused-vars
148:38 error 'selectAll' is assigned a value but never used @typescript-eslint/no-unused-vars
151:11 error 'isDragging' is assigned a value but never used @typescript-eslint/no-unused-vars
208:6 warning React Hook useEffect has a missing dependency: 'initializeSSHConnection'. Either include it or remove the dependency array react-hooks/exhaustive-deps
386:5 warning React Hook useCallback has a missing dependency: 'handleCloseWithError'. Either include it or remove the dependency array react-hooks/exhaustive-deps
455:6 warning React Hook useEffect has a missing dependency: 'handleOpenTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
787:49 error 'editMode' is assigned a value but never used @typescript-eslint/no-unused-vars
837:12 error 'handleFileEdit' is defined but never used @typescript-eslint/no-unused-vars
841:12 error 'handleFileView' is defined but never used @typescript-eslint/no-unused-vars
1356:23 error 'index' is defined but never used @typescript-eslint/no-unused-vars
1663:6 warning React Hook useEffect has a missing dependency: 'loadPinnedFiles'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerContextMenu.tsx
16:3 error 'Share' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars
193:9 error 'hasDirectories' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerGrid.tsx
169:3 error 'onFileSelect' is defined but never used @typescript-eslint/no-unused-vars
191:3 error 'onSystemDragStart' is defined but never used @typescript-eslint/no-unused-vars
371:6 warning React Hook useEffect has missing dependencies: 'historyIndex' and 'navigationHistory'. Either include them or remove the dependency array react-hooks/exhaustive-deps
431:9 error 'handlePathInputKeyDown' is assigned a value but never used @typescript-eslint/no-unused-vars
483:5 warning React Hook useCallback has an unnecessary dependency: 'dragState.counter'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
620:34 error 'e' is defined but never used @typescript-eslint/no-unused-vars
674:5 warning React Hook useCallback has an unnecessary dependency: 'onDownload'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
810:6 warning React Hook useEffect has missing dependencies: 'hasClipboard' and 'onStartEdit'. Either include them or remove the dependency array. If 'onStartEdit' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerSidebar.tsx
50:3 error 'onLoadDirectory' is defined but never used @typescript-eslint/no-unused-vars
78:6 warning React Hook useEffect has a missing dependency: 'loadQuickAccessData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
84:6 warning React Hook useEffect has a missing dependency: 'loadDirectoryTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
91:61 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
101:49 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
110:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
235:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
238:54 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
306:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DiffViewer.tsx
64:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
99:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
160:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
206:6 warning React Hook useEffect has a missing dependency: 'loadFileContents'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DraggableWindow.tsx
3:17 error 'Square' is defined but never used @typescript-eslint/no-unused-vars
208:5 warning React Hook useCallback has an unnecessary dependency: 'position'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileViewer.tsx
63:8 error 'ReactPlayer' is defined but never used @typescript-eslint/no-unused-vars
293:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
314:10 error 'originalContent' is assigned a value but never used @typescript-eslint/no-unused-vars
329:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
381:9 warning The 'handleSave' function makes the dependencies of useEffect Hook (at line 411) change on every render. To fix this, wrap the definition of 'handleSave' in its own useCallback() Hook react-hooks/exhaustive-deps
979:29 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1100:32 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1384:33 error 'audio' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileWindow.tsx
59:53 error 'updateWindow' is assigned a value but never used @typescript-eslint/no-unused-vars
160:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
179:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook useEffect has missing dependencies: 'closeWindow', 'ensureSSHConnection', 'onFileNotFound', 't', and 'windowId'. Either include them or remove the dependency array. If 'onFileNotFound' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
232:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
261:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
338:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\TerminalWindow.tsx
41:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
53:9 error 'handleMinimize' is assigned a value but never used @typescript-eslint/no-unused-vars
77:3 error React Hook "React.useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\WindowManager.tsx
132:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManager.tsx
18:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
25:62 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:29 error 'updatedHost' is defined but never used @typescript-eslint/no-unused-vars
38:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerEditor.tsx
41:28 error 'WidgetType' is defined but never used @typescript-eslint/no-unused-vars
63:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
80:10 error 'hosts' is assigned a value but never used @typescript-eslint/no-unused-vars
83:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
84:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
305:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
390:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
423:6 warning React Hook useEffect has missing dependencies: 'editingHost' and 'form'. Either include them or remove the dependency array react-hooks/exhaustive-deps
443:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
536:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerViewer.tsx
83:6 warning React Hook useEffect has a missing dependency: 'fetchHosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
109:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
125:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
163:47 error 'actualAuthType' is defined but never used @typescript-eslint/no-unused-vars
169:13 error '_' is defined but never used @typescript-eslint/no-unused-vars
188:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
225:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
254:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
294:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
328:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
442:6 warning React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\Server.tsx
17:8 error 'StatsConfig' is defined but never used @typescript-eslint/no-unused-vars
31:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
47:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
128:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
145:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
154:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
166:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
196:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
230:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
239:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
294:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\DiskWidget.tsx
15:39 error 'metricsHistory' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\NetworkWidget.tsx
14:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\ProcessesWidget.tsx
14:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\SystemWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\UptimeWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\SnippetsSidebar.tsx
55:6 warning React Hook useEffect has a missing dependency: 'fetchSnippets'. Either include it or remove the dependency array react-hooks/exhaustive-deps
63:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
97:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
128:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\Terminal.tsx
79:10 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
80:10 error 'isAuthenticated' is assigned a value but never used @typescript-eslint/no-unused-vars
216:5 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
219:12 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
477:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
513:35 error 'event' is defined but never used @typescript-eslint/no-unused-vars
697:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
736:6 warning React Hook useEffect has missing dependencies: 'connectToHost' and 'hardRefresh'. Either include them or remove the dependency array react-hooks/exhaustive-deps
755:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
767:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Tunnel\TunnelViewer.tsx
6:3 error 'TunnelConnection' is defined but never used @typescript-eslint/no-unused-vars
18:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
66:40 error '_host' is defined but never used @typescript-eslint/no-unused-vars
66:47 error '_index' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\DesktopApp.tsx
15:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
18:10 error 'view' is assigned a value but never used @typescript-eslint/no-unused-vars
19:10 error 'mountedViews' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Electron Only\ServerConfig.tsx
77:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
120:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\Homepage.tsx
7:10 error 'useTranslation' is defined but never used @typescript-eslint/no-unused-vars
28:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
29:10 error 'username' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAlertManager.tsx
20:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
27:6 warning React Hook useEffect has a missing dependency: 'fetchUserAlerts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
55:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
80:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
54:3 error 'dbError' is defined but never used @typescript-eslint/no-unused-vars
68:10 error 'visibility' is assigned a value but only used as a type @typescript-eslint/no-unused-vars
74:9 error 'toggleVisibility' is assigned a value but never used @typescript-eslint/no-unused-vars
78:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
163:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
213:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
236:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
262:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
281:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
385:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
418:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
431:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
465:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
481:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
518:9 error 'retryDatabaseConnection' is assigned a value but never used @typescript-eslint/no-unused-vars
531:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
545:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HompageUpdateLog.tsx
69:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
74:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\AppView.tsx
14:3 error 'LucideRefreshCcw' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'LucideRefreshCw' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'RefreshCcwDot' is defined but never used @typescript-eslint/no-unused-vars
117:6 warning React Hook useEffect has a missing dependency: 'hideThenFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
117:40 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
121:6 warning React Hook useEffect has a missing dependency: 'scheduleMeasureAndFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
133:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
142:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\Host.tsx
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\LeftSidebar.tsx
6:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
90:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
91:3 error 'getView' is defined but never used @typescript-eslint/no-unused-vars
152:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
215:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
218:6 warning React Hook React.useCallback has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
290:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
300:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
323:11 error 'jwt' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Tabs\TabContext.tsx
22:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
101:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
143:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\TopNavbar.tsx
8:3 error 'Accordion' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'AccordionContent' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'AccordionItem' is defined but never used @typescript-eslint/no-unused-vars
11:3 error 'AccordionTrigger' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\PasswordReset.tsx
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
83:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
113:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\TOTPSetup.tsx
69:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
89:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
108:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
125:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\UserProfile.tsx
11:24 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
42:6 warning React Hook useEffect has missing dependencies: 'fetchUserInfo' and 'fetchVersion'. Either include them or remove the dependency array react-hooks/exhaustive-deps
48:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
65:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\LeftSidebar.tsx
45:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Terminal\Terminal.tsx
15:10 error 'toast' is defined but never used @typescript-eslint/no-unused-vars
57:10 error 'isConnected' is assigned a value but never used @typescript-eslint/no-unused-vars
58:10 error 'isConnecting' is assigned a value but never used @typescript-eslint/no-unused-vars
59:10 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
155:5 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
158:12 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
384:6 warning React Hook useEffect has missing dependencies: 'hardRefresh', 'isAuthenticated', and 'setupWebSocketListeners'. Either include them or remove the dependency array react-hooks/exhaustive-deps
394:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
403:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
67:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
153:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
197:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
220:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
243:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
246:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
265:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
369:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
402:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
415:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
449:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
466:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\MobileApp.tsx
10:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
17:37 error 'removeTab' is assigned a value but never used @typescript-eslint/no-unused-vars
23:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
69:6 warning React Hook useEffect has a missing dependency: 'fitCurrentTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
98:6 warning React Hook React.useEffect has missing dependencies: 'fitCurrentTerminal' and 'tabs.length'. Either include them or remove the dependency array react-hooks/exhaustive-deps
131:23 error 'id' is defined but never used @typescript-eslint/no-unused-vars
135:24 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\LeftSidebar.tsx
46:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToDesktop.ts
120:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
140:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
229:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
254:35 error 'onSuccess' is assigned a value but never used @typescript-eslint/no-unused-vars
277:5 warning React Hook useCallback has unnecessary dependencies: 'sshHost' and 'sshSessionId'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToSystemDesktop.ts
26:3 error 'sshHost' is defined but never used @typescript-eslint/no-unused-vars
40:9 error 'getLastSaveDirectory' is assigned a value but never used @typescript-eslint/no-unused-vars
68:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
167:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
170:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
245:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
263:5 warning React Hook useCallback has missing dependencies: 'createFileBlob' and 'createZipBlob'. Either include them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\main-axios.ts
7:3 error 'Credential' is defined but never used @typescript-eslint/no-unused-vars
8:3 error 'CredentialData' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'HostInfo' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'ApiResponse' is defined but never used @typescript-eslint/no-unused-vars
328:3 error 'apiPort' is assigned a value but never used @typescript-eslint/no-unused-vars
925:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
962:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
999:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
✖ 416 problems (358 errors, 58 warnings)

631
lint-output.txt Normal file
View File

@@ -0,0 +1,631 @@
> termix@1.7.2 lint
> eslint .
C:\Users\29037\WebstormProjects\Termix\src\backend\database\routes\users.ts
688:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\file-manager.ts
169:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
435:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
465:13 error Empty block statement no-empty
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\server-stats.ts
64:34 error '_reject' is defined but never used @typescript-eslint/no-unused-vars
197:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1122:9 error 'now' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\terminal.ts
82:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
116:5 error 'userPayload' is assigned a value but never used @typescript-eslint/no-unused-vars
190:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
316:13 error 'cols' is assigned a value but never used @typescript-eslint/no-unused-vars
316:19 error 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars
633:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
779:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
788:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
800:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\tunnel.ts
830:34 error 'data' is defined but never used @typescript-eslint/no-unused-vars
896:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1053:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1427:21 error 'hasSourcePassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1428:21 error 'hasSourceKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1429:21 error 'hasEndpointPassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1432:21 error 'hasEndpointKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1457:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\starter.ts
78:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
131:47 error 'promise' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auth-manager.ts
166:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
197:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
198:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
205:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
218:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
260:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\auto-ssl-setup.ts
104:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\data-crypto.ts
18:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
41:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
76:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
87:31 error 'plaintextFields' is assigned a value but never used @typescript-eslint/no-unused-vars
223:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
391:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
400:6 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
438:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-file-encryption.ts
33:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
81:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
166:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
236:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
304:13 error 'currentFingerprint' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-migration.ts
247:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\lazy-field-encryption.ts
185:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
282:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\logger.ts
14:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\simple-db-ops.ts
134:5 error '_userId' is defined but never used @typescript-eslint/no-unused-vars
154:5 error '_tableName' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\ssh-key-utils.ts
52:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
243:20 error 'error' is defined but never used @typescript-eslint/no-unused-vars
321:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-crypto.ts
198:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
278:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
301:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
420:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
460:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-export.ts
21:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
22:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
24:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
25:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
26:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
86:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
188:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-import.ts
162:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
216:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
272:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
359:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\theme-provider.tsx
66:14 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\badge.tsx
46:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\button.tsx
62:18 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\form.tsx
158:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\password-input.tsx
8:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\shadcn-io\status\index.tsx
21:3 error 'className' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sidebar.tsx
12:3 error 'Sheet' is defined but never used @typescript-eslint/no-unused-vars
13:3 error 'SheetContent' is defined but never used @typescript-eslint/no-unused-vars
14:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
29:7 error 'SIDEBAR_WIDTH_MOBILE' is assigned a value but never used @typescript-eslint/no-unused-vars
164:11 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:28 error 'openMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:40 error 'setOpenMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
724:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sonner.tsx
11:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
34:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\ui\textarea.tsx
5:18 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\version-check-modal.tsx
4:10 error 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars
4:21 error 'X' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'onDismiss' is defined but never used @typescript-eslint/no-unused-vars
20:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:6 warning React Hook useEffect has missing dependencies: 'checkForUpdates' and 'onContinue'. Either include them or remove the dependency array. If 'onContinue' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
50:9 error 'handleVersionDismiss' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\lib\frontend-logger.ts
20:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
221:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
247:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
268:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
282:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
301:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\main.tsx
12:10 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
56:10 error Fast refresh only works when a file has exports. Move your component(s) to a separate file react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Admin\AdminSettings.tsx
29:3 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
49:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
99:10 error 'securityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
99:31 error 'setSecurityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
126:6 warning React Hook React.useEffect has missing dependencies: 'fetchUsers' and 't'. Either include them or remove the dependency array react-hooks/exhaustive-deps
147:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
168:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
275:16 error 'err' is defined but never used @typescript-eslint/no-unused-vars
289:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
360:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
452:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialEditor.tsx
45:10 error 'credentials' is assigned a value but never used @typescript-eslint/no-unused-vars
47:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
98:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
158:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
201:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
224:6 warning React Hook useEffect has a missing dependency: 'editingCredential'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialSelector.tsx
38:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
48:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialViewer.tsx
67:6 warning React Hook useEffect has missing dependencies: 'fetchCredentialDetails' and 'fetchHostsUsing'. Either include them or remove the dependency array react-hooks/exhaustive-deps
73:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
82:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
100:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialsManager.tsx
15:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetFooter' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
18:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
21:3 error 'Select' is defined but never used @typescript-eslint/no-unused-vars
22:3 error 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars
24:3 error 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars
25:3 error 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars
40:3 error 'Pin' is defined but never used @typescript-eslint/no-unused-vars
78:29 error 'setViewingCredential' is assigned a value but never used @typescript-eslint/no-unused-vars
91:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
102:6 warning React Hook useEffect has a missing dependency: 'fetchCredentials'. Either include it or remove the dependency array react-hooks/exhaustive-deps
156:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
227:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
288:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
328:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
362:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManager.tsx
26:3 error 'Eye' is defined but never used @typescript-eslint/no-unused-vars
27:3 error 'Settings' is defined but never used @typescript-eslint/no-unused-vars
90:23 error 'setCurrentHost' is assigned a value but never used @typescript-eslint/no-unused-vars
148:26 error 'selectFile' is assigned a value but never used @typescript-eslint/no-unused-vars
148:38 error 'selectAll' is assigned a value but never used @typescript-eslint/no-unused-vars
151:11 error 'isDragging' is assigned a value but never used @typescript-eslint/no-unused-vars
208:6 warning React Hook useEffect has a missing dependency: 'initializeSSHConnection'. Either include it or remove the dependency array react-hooks/exhaustive-deps
386:5 warning React Hook useCallback has a missing dependency: 'handleCloseWithError'. Either include it or remove the dependency array react-hooks/exhaustive-deps
455:6 warning React Hook useEffect has a missing dependency: 'handleOpenTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
787:49 error 'editMode' is assigned a value but never used @typescript-eslint/no-unused-vars
837:12 error 'handleFileEdit' is defined but never used @typescript-eslint/no-unused-vars
841:12 error 'handleFileView' is defined but never used @typescript-eslint/no-unused-vars
1356:23 error 'index' is defined but never used @typescript-eslint/no-unused-vars
1663:6 warning React Hook useEffect has a missing dependency: 'loadPinnedFiles'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerContextMenu.tsx
16:3 error 'Share' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars
193:9 error 'hasDirectories' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerGrid.tsx
169:3 error 'onFileSelect' is defined but never used @typescript-eslint/no-unused-vars
191:3 error 'onSystemDragStart' is defined but never used @typescript-eslint/no-unused-vars
371:6 warning React Hook useEffect has missing dependencies: 'historyIndex' and 'navigationHistory'. Either include them or remove the dependency array react-hooks/exhaustive-deps
431:9 error 'handlePathInputKeyDown' is assigned a value but never used @typescript-eslint/no-unused-vars
483:5 warning React Hook useCallback has an unnecessary dependency: 'dragState.counter'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
620:34 error 'e' is defined but never used @typescript-eslint/no-unused-vars
674:5 warning React Hook useCallback has an unnecessary dependency: 'onDownload'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
810:6 warning React Hook useEffect has missing dependencies: 'hasClipboard' and 'onStartEdit'. Either include them or remove the dependency array. If 'onStartEdit' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerSidebar.tsx
50:3 error 'onLoadDirectory' is defined but never used @typescript-eslint/no-unused-vars
78:6 warning React Hook useEffect has a missing dependency: 'loadQuickAccessData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
84:6 warning React Hook useEffect has a missing dependency: 'loadDirectoryTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
91:61 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
101:49 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
110:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
235:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
238:54 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
306:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DiffViewer.tsx
64:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
99:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
160:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
206:6 warning React Hook useEffect has a missing dependency: 'loadFileContents'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DraggableWindow.tsx
3:17 error 'Square' is defined but never used @typescript-eslint/no-unused-vars
208:5 warning React Hook useCallback has an unnecessary dependency: 'position'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileViewer.tsx
63:8 error 'ReactPlayer' is defined but never used @typescript-eslint/no-unused-vars
293:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
314:10 error 'originalContent' is assigned a value but never used @typescript-eslint/no-unused-vars
329:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
381:9 warning The 'handleSave' function makes the dependencies of useEffect Hook (at line 411) change on every render. To fix this, wrap the definition of 'handleSave' in its own useCallback() Hook react-hooks/exhaustive-deps
979:29 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1100:32 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1384:33 error 'audio' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileWindow.tsx
59:53 error 'updateWindow' is assigned a value but never used @typescript-eslint/no-unused-vars
160:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
179:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook useEffect has missing dependencies: 'closeWindow', 'ensureSSHConnection', 'onFileNotFound', 't', and 'windowId'. Either include them or remove the dependency array. If 'onFileNotFound' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
232:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
261:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
338:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\TerminalWindow.tsx
41:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
53:9 error 'handleMinimize' is assigned a value but never used @typescript-eslint/no-unused-vars
77:3 error React Hook "React.useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\WindowManager.tsx
132:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManager.tsx
18:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
25:62 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:29 error 'updatedHost' is defined but never used @typescript-eslint/no-unused-vars
38:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerEditor.tsx
41:28 error 'WidgetType' is defined but never used @typescript-eslint/no-unused-vars
63:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
80:10 error 'hosts' is assigned a value but never used @typescript-eslint/no-unused-vars
83:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
84:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
305:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
390:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
423:6 warning React Hook useEffect has missing dependencies: 'editingHost' and 'form'. Either include them or remove the dependency array react-hooks/exhaustive-deps
443:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
536:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerViewer.tsx
83:6 warning React Hook useEffect has a missing dependency: 'fetchHosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
109:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
125:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
163:47 error 'actualAuthType' is defined but never used @typescript-eslint/no-unused-vars
169:13 error '_' is defined but never used @typescript-eslint/no-unused-vars
188:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
225:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
254:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
294:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
328:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
442:6 warning React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\Server.tsx
17:8 error 'StatsConfig' is defined but never used @typescript-eslint/no-unused-vars
31:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
47:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
128:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
145:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
154:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
166:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
196:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
230:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
239:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
294:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\DiskWidget.tsx
15:39 error 'metricsHistory' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\NetworkWidget.tsx
14:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\ProcessesWidget.tsx
14:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\SystemWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\UptimeWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\SnippetsSidebar.tsx
55:6 warning React Hook useEffect has a missing dependency: 'fetchSnippets'. Either include it or remove the dependency array react-hooks/exhaustive-deps
63:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
97:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
128:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\Terminal.tsx
79:10 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
80:10 error 'isAuthenticated' is assigned a value but never used @typescript-eslint/no-unused-vars
216:5 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
219:12 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
477:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
513:35 error 'event' is defined but never used @typescript-eslint/no-unused-vars
697:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
736:6 warning React Hook useEffect has missing dependencies: 'connectToHost' and 'hardRefresh'. Either include them or remove the dependency array react-hooks/exhaustive-deps
755:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
767:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Tunnel\TunnelViewer.tsx
6:3 error 'TunnelConnection' is defined but never used @typescript-eslint/no-unused-vars
18:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
66:40 error '_host' is defined but never used @typescript-eslint/no-unused-vars
66:47 error '_index' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\DesktopApp.tsx
15:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
18:10 error 'view' is assigned a value but never used @typescript-eslint/no-unused-vars
19:10 error 'mountedViews' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Electron Only\ServerConfig.tsx
77:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
120:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\Homepage.tsx
7:10 error 'useTranslation' is defined but never used @typescript-eslint/no-unused-vars
28:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
29:10 error 'username' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAlertManager.tsx
20:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
27:6 warning React Hook useEffect has a missing dependency: 'fetchUserAlerts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
55:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
80:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
54:3 error 'dbError' is defined but never used @typescript-eslint/no-unused-vars
68:10 error 'visibility' is assigned a value but only used as a type @typescript-eslint/no-unused-vars
74:9 error 'toggleVisibility' is assigned a value but never used @typescript-eslint/no-unused-vars
78:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
163:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
213:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
236:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
262:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
281:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
385:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
418:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
431:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
465:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
481:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
518:9 error 'retryDatabaseConnection' is assigned a value but never used @typescript-eslint/no-unused-vars
531:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
545:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HompageUpdateLog.tsx
69:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
74:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\AppView.tsx
14:3 error 'LucideRefreshCcw' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'LucideRefreshCw' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'RefreshCcwDot' is defined but never used @typescript-eslint/no-unused-vars
28:75 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
97:6 warning React Hook useEffect has a missing dependency: 'hideThenFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
97:40 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
101:6 warning React Hook useEffect has a missing dependency: 'scheduleMeasureAndFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
113:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
122:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
137:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
253:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
264:10 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
267:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
407:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\Host.tsx
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\LeftSidebar.tsx
6:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
60:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
90:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
91:3 error 'getView' is defined but never used @typescript-eslint/no-unused-vars
115:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
121:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
131:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
141:51 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
146:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
209:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
209:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook React.useCallback has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
284:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
294:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
317:11 error 'jwt' is assigned a value but never used @typescript-eslint/no-unused-vars
322:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Tabs\TabContext.tsx
22:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
101:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
143:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\TopNavbar.tsx
8:3 error 'Accordion' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'AccordionContent' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'AccordionItem' is defined but never used @typescript-eslint/no-unused-vars
11:3 error 'AccordionTrigger' is defined but never used @typescript-eslint/no-unused-vars
38:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
195:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
209:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
218:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
226:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
232:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\PasswordReset.tsx
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
83:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
113:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\TOTPSetup.tsx
69:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
89:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
108:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
125:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\UserProfile.tsx
11:24 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
42:6 warning React Hook useEffect has missing dependencies: 'fetchUserInfo' and 'fetchVersion'. Either include them or remove the dependency array react-hooks/exhaustive-deps
48:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
65:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\LeftSidebar.tsx
45:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Terminal\Terminal.tsx
15:10 error 'toast' is defined but never used @typescript-eslint/no-unused-vars
18:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
23:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
35:10 error 'isConnected' is assigned a value but never used @typescript-eslint/no-unused-vars
36:10 error 'isConnecting' is assigned a value but never used @typescript-eslint/no-unused-vars
37:10 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
73:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
74:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
133:5 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
136:12 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
278:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
279:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
311:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
362:6 warning React Hook useEffect has missing dependencies: 'hardRefresh', 'isAuthenticated', and 'setupWebSocketListeners'. Either include them or remove the dependency array react-hooks/exhaustive-deps
372:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
381:6 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
67:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
153:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
197:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
220:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
243:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
246:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
265:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
369:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
402:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
415:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
449:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
466:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\MobileApp.tsx
10:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
17:37 error 'removeTab' is assigned a value but never used @typescript-eslint/no-unused-vars
23:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
69:6 warning React Hook useEffect has a missing dependency: 'fitCurrentTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
98:6 warning React Hook React.useEffect has missing dependencies: 'fitCurrentTerminal' and 'tabs.length'. Either include them or remove the dependency array react-hooks/exhaustive-deps
131:23 error 'id' is defined but never used @typescript-eslint/no-unused-vars
135:24 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\LeftSidebar.tsx
46:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToDesktop.ts
120:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
140:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
229:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
254:35 error 'onSuccess' is assigned a value but never used @typescript-eslint/no-unused-vars
277:5 warning React Hook useCallback has unnecessary dependencies: 'sshHost' and 'sshSessionId'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToSystemDesktop.ts
26:3 error 'sshHost' is defined but never used @typescript-eslint/no-unused-vars
40:9 error 'getLastSaveDirectory' is assigned a value but never used @typescript-eslint/no-unused-vars
68:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
167:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
170:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
245:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
263:5 warning React Hook useCallback has missing dependencies: 'createFileBlob' and 'createZipBlob'. Either include them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\main-axios.ts
7:3 error 'Credential' is defined but never used @typescript-eslint/no-unused-vars
8:3 error 'CredentialData' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'HostInfo' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'ApiResponse' is defined but never used @typescript-eslint/no-unused-vars
328:3 error 'apiPort' is assigned a value but never used @typescript-eslint/no-unused-vars
925:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
962:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
999:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
✖ 452 problems (394 errors, 58 warnings)

534
lint-progress.txt Normal file
View File

@@ -0,0 +1,534 @@
> termix@1.7.2 lint
> eslint .
C:\Users\29037\WebstormProjects\Termix\src\backend\database\routes\users.ts
705:8 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\file-manager.ts
169:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
435:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
465:13 error Empty block statement no-empty
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\server-stats.ts
64:34 error '_reject' is defined but never used @typescript-eslint/no-unused-vars
197:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1130:9 error 'now' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\terminal.ts
157:5 error 'userPayload' is assigned a value but never used @typescript-eslint/no-unused-vars
350:13 error 'cols' is assigned a value but never used @typescript-eslint/no-unused-vars
350:19 error 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\ssh\tunnel.ts
840:34 error 'data' is defined but never used @typescript-eslint/no-unused-vars
906:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1068:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
1442:21 error 'hasSourcePassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1443:21 error 'hasSourceKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1444:21 error 'hasEndpointPassword' is assigned a value but never used @typescript-eslint/no-unused-vars
1447:21 error 'hasEndpointKey' is assigned a value but never used @typescript-eslint/no-unused-vars
1472:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\starter.ts
131:47 error '_promise' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-file-encryption.ts
33:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
81:72 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
166:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
236:12 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
304:13 error 'currentFingerprint' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\database-migration.ts
247:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\lazy-field-encryption.ts
185:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
282:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\logger.ts
14:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\backend\utils\user-data-import.ts
162:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
216:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
272:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
359:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\theme-provider.tsx
66:14 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\badge.tsx
46:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\button.tsx
62:18 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\form.tsx
158:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\password-input.tsx
8:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\shadcn-io\status\index.tsx
21:3 error 'className' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sidebar.tsx
12:3 error 'Sheet' is defined but never used @typescript-eslint/no-unused-vars
13:3 error 'SheetContent' is defined but never used @typescript-eslint/no-unused-vars
14:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
29:7 error 'SIDEBAR_WIDTH_MOBILE' is assigned a value but never used @typescript-eslint/no-unused-vars
164:11 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:28 error 'openMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
164:40 error 'setOpenMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
724:3 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\components\ui\sonner.tsx
11:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
28:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
34:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\components\ui\textarea.tsx
5:18 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type
C:\Users\29037\WebstormProjects\Termix\src\components\ui\version-check-modal.tsx
4:10 error 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars
4:21 error 'X' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'onDismiss' is defined but never used @typescript-eslint/no-unused-vars
20:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
30:6 warning React Hook useEffect has missing dependencies: 'checkForUpdates' and 'onContinue'. Either include them or remove the dependency array. If 'onContinue' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
50:9 error 'handleVersionDismiss' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\lib\frontend-logger.ts
20:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
221:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
247:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
268:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
282:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
301:11 error 'shortUrl' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\main.tsx
12:10 error 'isMobile' is assigned a value but never used @typescript-eslint/no-unused-vars
56:10 error Fast refresh only works when a file has exports. Move your component(s) to a separate file react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Admin\AdminSettings.tsx
29:3 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
49:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
99:10 error 'securityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
99:31 error 'setSecurityInitialized' is assigned a value but never used @typescript-eslint/no-unused-vars
127:6 warning React Hook React.useEffect has missing dependencies: 'fetchUsers' and 't'. Either include them or remove the dependency array react-hooks/exhaustive-deps
149:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
171:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
281:16 error 'err' is defined but never used @typescript-eslint/no-unused-vars
295:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
366:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
458:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialEditor.tsx
45:10 error 'credentials' is assigned a value but never used @typescript-eslint/no-unused-vars
47:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
98:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
158:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
201:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
224:6 warning React Hook useEffect has a missing dependency: 'editingCredential'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialSelector.tsx
38:16 error 'error' is defined but never used @typescript-eslint/no-unused-vars
48:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialViewer.tsx
67:6 warning React Hook useEffect has missing dependencies: 'fetchCredentialDetails' and 'fetchHostsUsing'. Either include them or remove the dependency array react-hooks/exhaustive-deps
73:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
82:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
100:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Credentials\CredentialsManager.tsx
15:3 error 'SheetDescription' is defined but never used @typescript-eslint/no-unused-vars
16:3 error 'SheetFooter' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'SheetHeader' is defined but never used @typescript-eslint/no-unused-vars
18:3 error 'SheetTitle' is defined but never used @typescript-eslint/no-unused-vars
21:3 error 'Select' is defined but never used @typescript-eslint/no-unused-vars
22:3 error 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars
24:3 error 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars
25:3 error 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars
40:3 error 'Pin' is defined but never used @typescript-eslint/no-unused-vars
78:29 error 'setViewingCredential' is assigned a value but never used @typescript-eslint/no-unused-vars
91:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
102:6 warning React Hook useEffect has a missing dependency: 'fetchCredentials'. Either include it or remove the dependency array react-hooks/exhaustive-deps
156:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
227:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
288:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
328:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
362:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManager.tsx
26:3 error 'Eye' is defined but never used @typescript-eslint/no-unused-vars
27:3 error 'Settings' is defined but never used @typescript-eslint/no-unused-vars
90:23 error 'setCurrentHost' is assigned a value but never used @typescript-eslint/no-unused-vars
148:26 error 'selectFile' is assigned a value but never used @typescript-eslint/no-unused-vars
148:38 error 'selectAll' is assigned a value but never used @typescript-eslint/no-unused-vars
151:11 error 'isDragging' is assigned a value but never used @typescript-eslint/no-unused-vars
208:6 warning React Hook useEffect has a missing dependency: 'initializeSSHConnection'. Either include it or remove the dependency array react-hooks/exhaustive-deps
386:5 warning React Hook useCallback has a missing dependency: 'handleCloseWithError'. Either include it or remove the dependency array react-hooks/exhaustive-deps
455:6 warning React Hook useEffect has a missing dependency: 'handleOpenTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
787:49 error 'editMode' is assigned a value but never used @typescript-eslint/no-unused-vars
837:12 error 'handleFileEdit' is defined but never used @typescript-eslint/no-unused-vars
841:12 error 'handleFileView' is defined but never used @typescript-eslint/no-unused-vars
1356:23 error 'index' is defined but never used @typescript-eslint/no-unused-vars
1665:6 warning React Hook useEffect has a missing dependency: 'loadPinnedFiles'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerContextMenu.tsx
16:3 error 'Share' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars
193:9 error 'hasDirectories' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerGrid.tsx
169:3 error 'onFileSelect' is defined but never used @typescript-eslint/no-unused-vars
191:3 error 'onSystemDragStart' is defined but never used @typescript-eslint/no-unused-vars
371:6 warning React Hook useEffect has missing dependencies: 'historyIndex' and 'navigationHistory'. Either include them or remove the dependency array react-hooks/exhaustive-deps
431:9 error 'handlePathInputKeyDown' is assigned a value but never used @typescript-eslint/no-unused-vars
483:5 warning React Hook useCallback has an unnecessary dependency: 'dragState.counter'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
620:34 error 'e' is defined but never used @typescript-eslint/no-unused-vars
674:5 warning React Hook useCallback has an unnecessary dependency: 'onDownload'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
810:6 warning React Hook useEffect has missing dependencies: 'hasClipboard' and 'onStartEdit'. Either include them or remove the dependency array. If 'onStartEdit' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\FileManagerSidebar.tsx
79:3 error 'onLoadDirectory' is defined but never used @typescript-eslint/no-unused-vars
107:6 warning React Hook useEffect has a missing dependency: 'loadQuickAccessData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
113:6 warning React Hook useEffect has a missing dependency: 'loadDirectoryTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DiffViewer.tsx
64:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
99:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
160:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
206:6 warning React Hook useEffect has a missing dependency: 'loadFileContents'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\DraggableWindow.tsx
3:17 error 'Square' is defined but never used @typescript-eslint/no-unused-vars
208:5 warning React Hook useCallback has an unnecessary dependency: 'position'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileViewer.tsx
63:8 error 'ReactPlayer' is defined but never used @typescript-eslint/no-unused-vars
293:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
314:10 error 'originalContent' is assigned a value but never used @typescript-eslint/no-unused-vars
329:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
381:9 warning The 'handleSave' function makes the dependencies of useEffect Hook (at line 411) change on every render. To fix this, wrap the definition of 'handleSave' in its own useCallback() Hook react-hooks/exhaustive-deps
979:29 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1100:32 error 'node' is defined but never used @typescript-eslint/no-unused-vars
1384:33 error 'audio' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\FileWindow.tsx
59:53 error 'updateWindow' is assigned a value but never used @typescript-eslint/no-unused-vars
160:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
179:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
212:6 warning React Hook useEffect has missing dependencies: 'closeWindow', 'ensureSSHConnection', 'onFileNotFound', 't', and 'windowId'. Either include them or remove the dependency array. If 'onFileNotFound' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
232:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
261:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
338:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\TerminalWindow.tsx
41:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
53:9 error 'handleMinimize' is assigned a value but never used @typescript-eslint/no-unused-vars
77:3 error React Hook "React.useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\File Manager\components\WindowManager.tsx
132:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManager.tsx
18:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
25:62 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:29 error 'updatedHost' is defined but never used @typescript-eslint/no-unused-vars
38:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerEditor.tsx
41:28 error 'WidgetType' is defined but never used @typescript-eslint/no-unused-vars
63:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
80:10 error 'hosts' is assigned a value but never used @typescript-eslint/no-unused-vars
83:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
84:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
305:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
390:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
423:6 warning React Hook useEffect has missing dependencies: 'editingHost' and 'form'. Either include them or remove the dependency array react-hooks/exhaustive-deps
443:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
536:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Host Manager\HostManagerViewer.tsx
83:6 warning React Hook useEffect has a missing dependency: 'fetchHosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
109:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
125:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
163:47 error 'actualAuthType' is defined but never used @typescript-eslint/no-unused-vars
169:13 error '_' is defined but never used @typescript-eslint/no-unused-vars
188:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
225:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
254:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
294:28 error 'e' is defined but never used @typescript-eslint/no-unused-vars
328:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
442:6 warning React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\Server.tsx
151:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
168:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
177:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
260:6 warning React Hook React.useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\DiskWidget.tsx
15:39 error 'metricsHistory' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\NetworkWidget.tsx
14:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
33:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\ProcessesWidget.tsx
14:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\SystemWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Server\widgets\UptimeWidget.tsx
14:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\SnippetsSidebar.tsx
55:6 warning React Hook useEffect has a missing dependency: 'fetchSnippets'. Either include it or remove the dependency array react-hooks/exhaustive-deps
63:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
97:18 error 'err' is defined but never used @typescript-eslint/no-unused-vars
128:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Terminal\Terminal.tsx
83:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
84:12 error 'isAuthenticated' is assigned a value but never used @typescript-eslint/no-unused-vars
227:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
230:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
493:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
529:37 error 'event' is defined but never used @typescript-eslint/no-unused-vars
713:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
753:8 warning React Hook useEffect has missing dependencies: 'connectToHost' and 'hardRefresh'. Either include them or remove the dependency array react-hooks/exhaustive-deps
772:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
784:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Apps\Tunnel\TunnelViewer.tsx
6:3 error 'TunnelConnection' is defined but never used @typescript-eslint/no-unused-vars
18:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
66:40 error '_host' is defined but never used @typescript-eslint/no-unused-vars
66:47 error '_index' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\DesktopApp.tsx
15:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
18:10 error 'view' is assigned a value but never used @typescript-eslint/no-unused-vars
19:10 error 'mountedViews' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Electron Only\ServerConfig.tsx
77:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
120:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\Homepage.tsx
7:10 error 'useTranslation' is defined but never used @typescript-eslint/no-unused-vars
28:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
29:10 error 'username' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAlertManager.tsx
20:10 error 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars
27:6 warning React Hook useEffect has a missing dependency: 'fetchUserAlerts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
55:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
80:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
54:3 error 'dbError' is defined but never used @typescript-eslint/no-unused-vars
68:10 error 'visibility' is assigned a value but only used as a type @typescript-eslint/no-unused-vars
74:9 error 'toggleVisibility' is assigned a value but never used @typescript-eslint/no-unused-vars
78:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
163:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
213:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
236:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
259:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
262:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
281:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
319:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
385:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
418:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
431:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
465:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
481:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
518:9 error 'retryDatabaseConnection' is assigned a value but never used @typescript-eslint/no-unused-vars
531:14 error 'error' is defined but never used @typescript-eslint/no-unused-vars
545:18 error 'error' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Homepage\HompageUpdateLog.tsx
69:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
74:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\AppView.tsx
14:3 error 'LucideRefreshCcw' is defined but never used @typescript-eslint/no-unused-vars
15:3 error 'LucideRefreshCw' is defined but never used @typescript-eslint/no-unused-vars
17:3 error 'RefreshCcwDot' is defined but never used @typescript-eslint/no-unused-vars
117:6 warning React Hook useEffect has a missing dependency: 'hideThenFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
117:40 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
121:6 warning React Hook useEffect has a missing dependency: 'scheduleMeasureAndFit'. Either include it or remove the dependency array react-hooks/exhaustive-deps
133:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
142:6 warning React Hook useEffect has a missing dependency: 'fitActiveAndNotify'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Hosts\Host.tsx
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\LeftSidebar.tsx
6:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
90:3 error 'onSelectView' is defined but never used @typescript-eslint/no-unused-vars
91:3 error 'getView' is defined but never used @typescript-eslint/no-unused-vars
152:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
215:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
218:6 warning React Hook React.useCallback has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
290:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
300:6 warning React Hook React.useMemo has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
323:11 error 'jwt' is assigned a value but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\Tabs\TabContext.tsx
22:53 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
101:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
143:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\Navigation\TopNavbar.tsx
8:3 error 'Accordion' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'AccordionContent' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'AccordionItem' is defined but never used @typescript-eslint/no-unused-vars
11:3 error 'AccordionTrigger' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\PasswordReset.tsx
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
83:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
113:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\TOTPSetup.tsx
69:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
89:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
108:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
125:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Desktop\User\UserProfile.tsx
11:24 error 'Key' is defined but never used @typescript-eslint/no-unused-vars
42:6 warning React Hook useEffect has missing dependencies: 'fetchUserInfo' and 'fetchVersion'. Either include them or remove the dependency array react-hooks/exhaustive-deps
48:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
65:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\LeftSidebar.tsx
45:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Apps\Terminal\Terminal.tsx
15:10 error 'toast' is defined but never used @typescript-eslint/no-unused-vars
55:12 error 'isConnected' is assigned a value but never used @typescript-eslint/no-unused-vars
56:12 error 'isConnecting' is assigned a value but never used @typescript-eslint/no-unused-vars
57:12 error 'connectionError' is assigned a value but never used @typescript-eslint/no-unused-vars
160:7 warning React Hook useImperativeHandle has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
163:14 error 'handleWindowResize' is defined but never used @typescript-eslint/no-unused-vars
394:8 warning React Hook useEffect has missing dependencies: 'hardRefresh', 'isAuthenticated', and 'setupWebSocketListeners'. Either include them or remove the dependency array react-hooks/exhaustive-deps
404:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
413:8 warning React Hook useEffect has a missing dependency: 'hardRefresh'. Either include it or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Homepage\HomepageAuth.tsx
22:3 error 'setCookie' is defined but never used @typescript-eslint/no-unused-vars
23:3 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
67:10 error 'error' is assigned a value but never used @typescript-eslint/no-unused-vars
153:6 warning React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps
197:8 error 'meRes' is never reassigned. Use 'const' instead prefer-const
220:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
243:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
246:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
265:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
303:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
369:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
402:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
415:11 error 'token' is assigned a value but never used @typescript-eslint/no-unused-vars
449:17 error 'err' is defined but never used @typescript-eslint/no-unused-vars
466:6 warning React Hook useEffect has missing dependencies: 'onAuthSuccess', 'setDbError', 'setIsAdmin', 'setLoggedIn', 'setUserId', 'setUsername', and 't'. Either include them or remove the dependency array. If 'setLoggedIn' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\MobileApp.tsx
10:23 error 'getCookie' is defined but never used @typescript-eslint/no-unused-vars
17:37 error 'removeTab' is assigned a value but never used @typescript-eslint/no-unused-vars
23:10 error 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars
69:6 warning React Hook useEffect has a missing dependency: 'fitCurrentTerminal'. Either include it or remove the dependency array react-hooks/exhaustive-deps
98:6 warning React Hook React.useEffect has missing dependencies: 'fitCurrentTerminal' and 'tabs.length'. Either include them or remove the dependency array react-hooks/exhaustive-deps
131:23 error 'id' is defined but never used @typescript-eslint/no-unused-vars
135:24 error 'err' is defined but never used @typescript-eslint/no-unused-vars
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\FolderCard.tsx
26:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Hosts\Host.tsx
5:10 error 'Server' is defined but never used @typescript-eslint/no-unused-vars
32:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
49:5 error 'intervalId' is never reassigned. Use 'const' instead prefer-const
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\LeftSidebar.tsx
46:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
78:24 error 'setHostsLoading' is assigned a value but never used @typescript-eslint/no-unused-vars
93:14 error 'err' is defined but never used @typescript-eslint/no-unused-vars
93:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\Mobile\Navigation\Tabs\TabContext.tsx
24:17 error Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
37:11 error 't' is assigned a value but never used @typescript-eslint/no-unused-vars
61:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToDesktop.ts
120:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
140:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
229:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
249:5 warning React Hook useCallback has an unnecessary dependency: 'sshHost'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
254:35 error 'onSuccess' is assigned a value but never used @typescript-eslint/no-unused-vars
277:5 warning React Hook useCallback has unnecessary dependencies: 'sshHost' and 'sshSessionId'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\hooks\useDragToSystemDesktop.ts
26:3 error 'sshHost' is defined but never used @typescript-eslint/no-unused-vars
40:9 error 'getLastSaveDirectory' is assigned a value but never used @typescript-eslint/no-unused-vars
68:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
167:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
170:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
190:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
245:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
263:5 warning React Hook useCallback has missing dependencies: 'createFileBlob' and 'createZipBlob'. Either include them or remove the dependency array react-hooks/exhaustive-deps
C:\Users\29037\WebstormProjects\Termix\src\ui\main-axios.ts
7:3 error 'Credential' is defined but never used @typescript-eslint/no-unused-vars
8:3 error 'CredentialData' is defined but never used @typescript-eslint/no-unused-vars
9:3 error 'HostInfo' is defined but never used @typescript-eslint/no-unused-vars
10:3 error 'ApiResponse' is defined but never used @typescript-eslint/no-unused-vars
346:3 error 'apiPort' is assigned a value but never used @typescript-eslint/no-unused-vars
994:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1031:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
1068:12 error 'error' is defined but never used @typescript-eslint/no-unused-vars
✖ 369 problems (311 errors, 58 warnings)

5370
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,18 @@
{ {
"name": "termix", "name": "termix",
"private": true, "private": true,
"version": "1.7.3", "version": "1.8.0",
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa", "author": "Karmaa",
"main": "electron/main.cjs", "main": "electron/main.cjs",
"type": "module", "type": "module",
"scripts": { "scripts": {
"clean": "npx prettier . --write", "clean": "npx prettier . --write",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"type-check": "tsc --noEmit",
"dev": "vite", "dev": "vite",
"build": "vite build && tsc -p tsconfig.node.json", "build": "vite build && tsc -p tsconfig.node.json",
"build:backend": "tsc -p tsconfig.node.json", "build:backend": "tsc -p tsconfig.node.json",
@@ -19,8 +24,13 @@
"build:linux-portable": "npm run build && electron-builder --linux --dir", "build:linux-portable": "npm run build && electron-builder --linux --dir",
"build:linux-appimage": "npm run build && electron-builder --linux AppImage", "build:linux-appimage": "npm run build && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && electron-builder --linux tar.gz", "build:linux-targz": "npm run build && electron-builder --linux tar.gz",
"build:mac-dmg": "npm run build && electron-builder --mac dmg",
"build:mac-zip": "npm run build && electron-builder --mac zip",
"build:mac-mas": "npm run build && electron-builder --mac mas",
"build:mac-universal": "npm run build && electron-builder --mac --universal",
"test:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-test.js", "test:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-test.js",
"migrate:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-migration.js" "migrate:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-migration.js",
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.7", "@codemirror/autocomplete": "^6.18.7",
@@ -95,6 +105,7 @@
"react-simple-keyboard": "^3.8.120", "react-simple-keyboard": "^3.8.120",
"react-syntax-highlighter": "^15.6.6", "react-syntax-highlighter": "^15.6.6",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
@@ -105,6 +116,9 @@
"zod": "^4.0.5" "zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@eslint/js": "^9.34.0", "@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -123,9 +137,19 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.3",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"typescript-eslint": "^8.40.0", "typescript-eslint": "^8.40.0",
"vite": "^7.1.5" "vite": "^7.1.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
} }
} }

View File

@@ -6,6 +6,7 @@ import userRoutes from "./routes/users.js";
import sshRoutes from "./routes/ssh.js"; import sshRoutes from "./routes/ssh.js";
import alertRoutes from "./routes/alerts.js"; import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js"; import credentialsRoutes from "./routes/credentials.js";
import snippetsRoutes from "./routes/snippets.js";
import cors from "cors"; import cors from "cors";
import fetch from "node-fetch"; import fetch from "node-fetch";
import fs from "fs"; import fs from "fs";
@@ -31,6 +32,12 @@ import {
sshCredentialUsage, sshCredentialUsage,
settings, settings,
} from "./db/schema.js"; } from "./db/schema.js";
import type {
CacheEntry,
GitHubRelease,
GitHubAPIResponse,
AuthenticatedRequest,
} from "../../types/index.js";
import { getDb } from "./db/index.js"; import { getDb } from "./db/index.js";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
@@ -105,17 +112,11 @@ const upload = multer({
}, },
}); });
interface CacheEntry {
data: any;
timestamp: number;
expiresAt: number;
}
class GitHubCache { class GitHubCache {
private cache: Map<string, CacheEntry> = new Map(); private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_DURATION = 30 * 60 * 1000; private readonly CACHE_DURATION = 30 * 60 * 1000;
set(key: string, data: any): void { set<T>(key: string, data: T): void {
const now = Date.now(); const now = Date.now();
this.cache.set(key, { this.cache.set(key, {
data, data,
@@ -124,7 +125,7 @@ class GitHubCache {
}); });
} }
get(key: string): any | null { get<T>(key: string): T | null {
const entry = this.cache.get(key); const entry = this.cache.get(key);
if (!entry) { if (!entry) {
return null; return null;
@@ -135,44 +136,26 @@ class GitHubCache {
return null; return null;
} }
return entry.data; return entry.data as T;
} }
} }
const githubCache = new GitHubCache(); const githubCache = new GitHubCache();
const GITHUB_API_BASE = "https://api.github.com"; const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "LukeGus"; const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix"; const REPO_NAME = "Termix";
interface GitHubRelease { async function fetchGitHubAPI<T>(
id: number;
tag_name: string;
name: string;
body: string;
published_at: string;
html_url: string;
assets: Array<{
id: number;
name: string;
size: number;
download_count: number;
browser_download_url: string;
}>;
prerelease: boolean;
draft: boolean;
}
async function fetchGitHubAPI(
endpoint: string, endpoint: string,
cacheKey: string, cacheKey: string,
): Promise<any> { ): Promise<GitHubAPIResponse<T>> {
const cachedData = githubCache.get(cacheKey); const cachedEntry = githubCache.get<CacheEntry<T>>(cacheKey);
if (cachedData) { if (cachedEntry) {
return { return {
data: cachedData, data: cachedEntry.data,
cached: true, cached: true,
cache_age: Date.now() - cachedData.timestamp, cache_age: Date.now() - cachedEntry.timestamp,
}; };
} }
@@ -191,8 +174,13 @@ async function fetchGitHubAPI(
); );
} }
const data = await response.json(); const data = (await response.json()) as T;
githubCache.set(cacheKey, data); const cacheData: CacheEntry<T> = {
data,
timestamp: Date.now(),
expiresAt: Date.now() + 30 * 60 * 1000,
};
githubCache.set(cacheKey, cacheData);
return { return {
data: data, data: data,
@@ -257,7 +245,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
localVersion = foundVersion; localVersion = foundVersion;
break; break;
} }
} catch (error) { } catch {
continue; continue;
} }
} }
@@ -272,7 +260,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
try { try {
const cacheKey = "latest_release"; const cacheKey = "latest_release";
const releaseData = await fetchGitHubAPI( const releaseData = await fetchGitHubAPI<GitHubRelease>(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
cacheKey, cacheKey,
); );
@@ -323,12 +311,12 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
); );
const cacheKey = `releases_rss_${page}_${per_page}`; const cacheKey = `releases_rss_${page}_${per_page}`;
const releasesData = await fetchGitHubAPI( const releasesData = await fetchGitHubAPI<GitHubRelease[]>(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
cacheKey, cacheKey,
); );
const rssItems = releasesData.data.map((release: GitHubRelease) => ({ const rssItems = releasesData.data.map((release) => ({
id: release.id, id: release.id,
title: release.name || release.tag_name, title: release.name || release.tag_name,
description: release.body, description: release.body,
@@ -372,7 +360,6 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
app.get("/encryption/status", requireAdmin, async (req, res) => { app.get("/encryption/status", requireAdmin, async (req, res) => {
try { try {
const authManager = AuthManager.getInstance();
const securityStatus = { const securityStatus = {
initialized: true, initialized: true,
system: { hasSecret: true, isValid: true }, system: { hasSecret: true, isValid: true },
@@ -417,8 +404,6 @@ app.post("/encryption/initialize", requireAdmin, async (req, res) => {
app.post("/encryption/regenerate", requireAdmin, async (req, res) => { app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
try { try {
const authManager = AuthManager.getInstance();
apiLogger.warn("System JWT secret regenerated via API", { apiLogger.warn("System JWT secret regenerated via API", {
operation: "jwt_regenerate_api", operation: "jwt_regenerate_api",
}); });
@@ -440,8 +425,6 @@ app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => { app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
try { try {
const authManager = AuthManager.getInstance();
apiLogger.warn("JWT secret regenerated via API", { apiLogger.warn("JWT secret regenerated via API", {
operation: "jwt_secret_regenerate_api", operation: "jwt_secret_regenerate_api",
}); });
@@ -462,7 +445,7 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
app.post("/database/export", authenticateJWT, async (req, res) => { app.post("/database/export", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -695,7 +678,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.authType, decrypted.authType,
decrypted.password || null, decrypted.password || null,
decrypted.key || null, decrypted.key || null,
decrypted.keyPassword || null, decrypted.key_password || null,
decrypted.keyType || null, decrypted.keyType || null,
decrypted.autostartPassword || null, decrypted.autostartPassword || null,
decrypted.autostartKey || null, decrypted.autostartKey || null,
@@ -738,9 +721,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username, decrypted.username,
decrypted.password || null, decrypted.password || null,
decrypted.key || null, decrypted.key || null,
decrypted.privateKey || null, decrypted.private_key || null,
decrypted.publicKey || null, decrypted.public_key || null,
decrypted.keyPassword || null, decrypted.key_password || null,
decrypted.keyType || null, decrypted.keyType || null,
decrypted.detectedKeyType || null, decrypted.detectedKeyType || null,
decrypted.usageCount || 0, decrypted.usageCount || 0,
@@ -916,7 +899,7 @@ app.post(
return res.status(400).json({ error: "No file uploaded" }); return res.status(400).json({ error: "No file uploaded" });
} }
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -968,7 +951,7 @@ app.post(
try { try {
importDb = new Database(req.file.path, { readonly: true }); importDb = new Database(req.file.path, { readonly: true });
const tables = importDb importDb
.prepare("SELECT name FROM sqlite_master WHERE type='table'") .prepare("SELECT name FROM sqlite_master WHERE type='table'")
.all(); .all();
} catch (sqliteError) { } catch (sqliteError) {
@@ -1059,7 +1042,7 @@ app.post(
); );
} }
} }
} catch (tableError) { } catch {
apiLogger.info("ssh_data table not found in import file, skipping"); apiLogger.info("ssh_data table not found in import file, skipping");
} }
@@ -1120,7 +1103,7 @@ app.post(
); );
} }
} }
} catch (tableError) { } catch {
apiLogger.info( apiLogger.info(
"ssh_credentials table not found in import file, skipping", "ssh_credentials table not found in import file, skipping",
); );
@@ -1191,7 +1174,7 @@ app.post(
); );
gemini-code-assist[bot] commented 2025-10-10 03:54:21 +00:00 (Migrated from github.com)
Review

medium

This statement has no effect as the result of .all() is not used. It seems to be a leftover from debugging or a refactoring mistake. It should be removed to avoid confusion and unnecessary database operations.

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) This statement has no effect as the result of `.all()` is not used. It seems to be a leftover from debugging or a refactoring mistake. It should be removed to avoid confusion and unnecessary database operations.
} }
} }
} catch (tableError) { } catch {
apiLogger.info(`${table} table not found in import file, skipping`); apiLogger.info(`${table} table not found in import file, skipping`);
} }
} }
@@ -1229,7 +1212,7 @@ app.post(
); );
} }
} }
} catch (tableError) { } catch {
apiLogger.info( apiLogger.info(
"dismissed_alerts table not found in import file, skipping", "dismissed_alerts table not found in import file, skipping",
); );
@@ -1270,7 +1253,7 @@ app.post(
); );
} }
} }
} catch (tableError) { } catch {
apiLogger.info("settings table not found in import file, skipping"); apiLogger.info("settings table not found in import file, skipping");
} }
} else { } else {
@@ -1288,7 +1271,7 @@ app.post(
try { try {
fs.unlinkSync(req.file.path); fs.unlinkSync(req.file.path);
} catch (cleanupError) { } catch {
apiLogger.warn("Failed to clean up uploaded file", { apiLogger.warn("Failed to clean up uploaded file", {
operation: "file_cleanup_warning", operation: "file_cleanup_warning",
filePath: req.file.path, filePath: req.file.path,
@@ -1314,7 +1297,7 @@ app.post(
if (req.file?.path && fs.existsSync(req.file.path)) { if (req.file?.path && fs.existsSync(req.file.path)) {
try { try {
fs.unlinkSync(req.file.path); fs.unlinkSync(req.file.path);
} catch (cleanupError) { } catch {
apiLogger.warn("Failed to clean up uploaded file after error", { apiLogger.warn("Failed to clean up uploaded file after error", {
operation: "file_cleanup_error", operation: "file_cleanup_error",
filePath: req.file.path, filePath: req.file.path,
@@ -1324,7 +1307,7 @@ app.post(
apiLogger.error("SQLite import failed", error, { apiLogger.error("SQLite import failed", error, {
operation: "sqlite_import_api_failed", operation: "sqlite_import_api_failed",
userId: (req as any).userId, userId: (req as AuthenticatedRequest).userId,
}); });
res.status(500).json({ res.status(500).json({
error: "Failed to import SQLite data", error: "Failed to import SQLite data",
@@ -1336,12 +1319,8 @@ app.post(
app.post("/database/export/preview", authenticateJWT, async (req, res) => { app.post("/database/export/preview", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { const { scope = "user_data", includeCredentials = true } = req.body;
format = "encrypted",
scope = "user_data",
includeCredentials = true,
} = req.body;
const exportData = await UserDataExport.exportUserData(userId, { const exportData = await UserDataExport.exportUserData(userId, {
format: "encrypted", format: "encrypted",
@@ -1411,13 +1390,15 @@ app.use("/users", userRoutes);
app.use("/ssh", sshRoutes); app.use("/ssh", sshRoutes);
app.use("/alerts", alertRoutes); app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes); app.use("/credentials", credentialsRoutes);
app.use("/snippets", snippetsRoutes);
app.use( app.use(
( (
err: unknown, err: unknown,
req: express.Request, req: express.Request,
res: express.Response, res: express.Response,
next: express.NextFunction, // eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: express.NextFunction,
) => { ) => {
apiLogger.error("Unhandled error in request", err, { apiLogger.error("Unhandled error in request", err, {
operation: "error_handler", operation: "error_handler",
@@ -1430,7 +1411,6 @@ app.use(
); );
const HTTP_PORT = 30001; const HTTP_PORT = 30001;
const HTTPS_PORT = process.env.SSL_PORT || 8443;
async function initializeSecurity() { async function initializeSecurity() {
try { try {
@@ -1443,13 +1423,6 @@ async function initializeSecurity() {
if (!isValid) { if (!isValid) {
throw new Error("Security system validation failed"); throw new Error("Security system validation failed");
} }
const securityStatus = {
initialized: true,
system: { hasSecret: true, isValid: true },
activeSessions: {},
activeSessionCount: 0,
};
} catch (error) { } catch (error) {
databaseLogger.error("Failed to initialize security system", error, { databaseLogger.error("Failed to initialize security system", error, {
operation: "security_init_error", operation: "security_init_error",
@@ -1481,13 +1454,17 @@ app.get(
if (status.hasUnencryptedDb) { if (status.hasUnencryptedDb) {
try { try {
unencryptedSize = fs.statSync(dbPath).size; unencryptedSize = fs.statSync(dbPath).size;
} catch (error) {} } catch {
// Ignore file access errors
}
} }
if (status.hasEncryptedDb) { if (status.hasEncryptedDb) {
try { try {
encryptedSize = fs.statSync(encryptedDbPath).size; encryptedSize = fs.statSync(encryptedDbPath).size;
} catch (error) {} } catch {
// Ignore file access errors
}
} }
res.json({ res.json({

View File

@@ -23,7 +23,7 @@ const enableFileEncryption = process.env.DB_FILE_ENCRYPTION !== "false";
const dbPath = path.join(dataDir, "db.sqlite"); const dbPath = path.join(dataDir, "db.sqlite");
const encryptedDbPath = `${dbPath}.encrypted`; const encryptedDbPath = `${dbPath}.encrypted`;
let actualDbPath = ":memory:"; const actualDbPath = ":memory:";
let memoryDatabase: Database.Database; let memoryDatabase: Database.Database;
let isNewDatabase = false; let isNewDatabase = false;
let sqlite: Database.Database; let sqlite: Database.Database;
@@ -31,7 +31,8 @@ let sqlite: Database.Database;
async function initializeDatabaseAsync(): Promise<void> { async function initializeDatabaseAsync(): Promise<void> {
const systemCrypto = SystemCrypto.getInstance(); const systemCrypto = SystemCrypto.getInstance();
const dbKey = await systemCrypto.getDatabaseKey(); // Ensure database key is initialized
await systemCrypto.getDatabaseKey();
if (enableFileEncryption) { if (enableFileEncryption) {
try { try {
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) { if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
@@ -165,6 +166,7 @@ async function initializeCompleteDatabase(): Promise<void> {
tunnel_connections TEXT, tunnel_connections TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1, enable_file_manager INTEGER NOT NULL DEFAULT 1,
default_path TEXT, default_path TEXT,
stats_config TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (user_id) REFERENCES users (id)
@@ -242,6 +244,17 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (user_id) REFERENCES users (id)
); );
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
);
`); `);
migrateSchema(); migrateSchema();
@@ -277,7 +290,7 @@ const addColumnIfNotExists = (
FROM ${table} LIMIT 1`, FROM ${table} LIMIT 1`,
) )
.get(); .get();
} catch (e) { } catch {
try { try {
sqlite.exec(`ALTER TABLE ${table} sqlite.exec(`ALTER TABLE ${table}
ADD COLUMN ${column} ${definition};`); ADD COLUMN ${column} ${definition};`);
@@ -361,6 +374,7 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT"); addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT"); addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT"); addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
addColumnIfNotExists("ssh_data", "stats_config", "TEXT");
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
@@ -476,21 +490,29 @@ async function cleanupDatabase() {
for (const file of files) { for (const file of files) {
try { try {
fs.unlinkSync(path.join(tempDir, file)); fs.unlinkSync(path.join(tempDir, file));
} catch {} } catch {
// Ignore cleanup errors
}
} }
try { try {
fs.rmdirSync(tempDir); fs.rmdirSync(tempDir);
} catch {} } catch {
// Ignore cleanup errors
}
} }
} catch (error) {} } catch {
// Ignore cleanup errors
}
} }
process.on("exit", () => { process.on("exit", () => {
if (sqlite) { if (sqlite) {
try { try {
sqlite.close(); sqlite.close();
} catch {} } catch {
// Ignore close errors on exit
}
} }
}); });

View File

@@ -65,6 +65,7 @@ export const sshData = sqliteTable("ssh_data", {
.notNull() .notNull()
.default(true), .default(true),
defaultPath: text("default_path"), defaultPath: text("default_path"),
statsConfig: text("stats_config"),
createdAt: text("created_at") createdAt: text("created_at")
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),
@@ -172,3 +173,19 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", {
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),
}); });
export const snippets = sqliteTable("snippets", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id),
name: text("name").notNull(),
content: text("content").notNull(),
description: text("description"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});

View File

@@ -1,3 +1,8 @@
import type {
AuthenticatedRequest,
CacheEntry,
TermixAlert,
} from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { dismissedAlerts } from "../db/schema.js"; import { dismissedAlerts } from "../db/schema.js";
@@ -6,17 +11,11 @@ import fetch from "node-fetch";
import { authLogger } from "../../utils/logger.js"; import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
interface CacheEntry {
data: any;
timestamp: number;
expiresAt: number;
}
class AlertCache { class AlertCache {
private cache: Map<string, CacheEntry> = new Map(); private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_DURATION = 5 * 60 * 1000; private readonly CACHE_DURATION = 5 * 60 * 1000;
set(key: string, data: any): void { set<T>(key: string, data: T): void {
const now = Date.now(); const now = Date.now();
this.cache.set(key, { this.cache.set(key, {
data, data,
@@ -25,7 +24,7 @@ class AlertCache {
}); });
} }
get(key: string): any | null { get<T>(key: string): T | null {
const entry = this.cache.get(key); const entry = this.cache.get(key);
if (!entry) { if (!entry) {
return null; return null;
@@ -36,31 +35,20 @@ class AlertCache {
return null; return null;
} }
return entry.data; return entry.data as T;
} }
} }
const alertCache = new AlertCache(); const alertCache = new AlertCache();
const GITHUB_RAW_BASE = "https://raw.githubusercontent.com"; const GITHUB_RAW_BASE = "https://raw.githubusercontent.com";
const REPO_OWNER = "LukeGus"; const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix-Docs"; const REPO_NAME = "Docs";
const ALERTS_FILE = "main/termix-alerts.json"; const ALERTS_FILE = "main/termix-alerts.json";
interface TermixAlert {
id: string;
title: string;
message: string;
expiresAt: string;
priority?: "low" | "medium" | "high" | "critical";
type?: "info" | "warning" | "error" | "success";
actionUrl?: string;
actionText?: string;
}
async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> { async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
const cacheKey = "termix_alerts"; const cacheKey = "termix_alerts";
const cachedData = alertCache.get(cacheKey); const cachedData = alertCache.get<TermixAlert[]>(cacheKey);
if (cachedData) { if (cachedData) {
return cachedData; return cachedData;
} }
@@ -115,7 +103,7 @@ const authenticateJWT = authManager.createAuthMiddleware();
// GET /alerts // GET /alerts
router.get("/", authenticateJWT, async (req, res) => { router.get("/", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const allAlerts = await fetchAlertsFromGitHub(); const allAlerts = await fetchAlertsFromGitHub();
@@ -148,7 +136,7 @@ router.get("/", authenticateJWT, async (req, res) => {
router.post("/dismiss", authenticateJWT, async (req, res) => { router.post("/dismiss", authenticateJWT, async (req, res) => {
try { try {
const { alertId } = req.body; const { alertId } = req.body;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!alertId) { if (!alertId) {
authLogger.warn("Missing alertId in dismiss request", { userId }); authLogger.warn("Missing alertId in dismiss request", { userId });
@@ -170,7 +158,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
return res.status(409).json({ error: "Alert already dismissed" }); return res.status(409).json({ error: "Alert already dismissed" });
} }
const result = await db.insert(dismissedAlerts).values({ await db.insert(dismissedAlerts).values({
userId, userId,
alertId, alertId,
}); });
@@ -186,7 +174,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
// GET /alerts/dismissed/:userId // GET /alerts/dismissed/:userId
router.get("/dismissed", authenticateJWT, async (req, res) => { router.get("/dismissed", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const dismissedAlertRecords = await db const dismissedAlertRecords = await db
.select({ .select({
@@ -211,7 +199,7 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
router.delete("/dismiss", authenticateJWT, async (req, res) => { router.delete("/dismiss", authenticateJWT, async (req, res) => {
try { try {
const { alertId } = req.body; const { alertId } = req.body;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!alertId) { if (!alertId) {
return res.status(400).json({ error: "Alert ID is required" }); return res.status(400).json({ error: "Alert ID is required" });

View File

@@ -1,16 +1,15 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm"; import { eq, and, desc, sql } from "drizzle-orm";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response } from "express";
import jwt from "jsonwebtoken";
import { authLogger } from "../../utils/logger.js"; import { authLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
import { import {
parseSSHKey, parseSSHKey,
parsePublicKey, parsePublicKey,
detectKeyType,
validateKeyPair, validateKeyPair,
} from "../../utils/ssh-key-utils.js"; } from "../../utils/ssh-key-utils.js";
import crypto from "crypto"; import crypto from "crypto";
@@ -29,7 +28,11 @@ function generateSSHKeyPair(
} { } {
try { try {
let ssh2Type = keyType; let ssh2Type = keyType;
const options: any = {}; const options: {
bits?: number;
passphrase?: string;
cipher?: string;
} = {};
if (keyType === "ssh-rsa") { if (keyType === "ssh-rsa") {
ssh2Type = "rsa"; ssh2Type = "rsa";
@@ -46,6 +49,7 @@ function generateSSHKeyPair(
options.cipher = "aes128-cbc"; options.cipher = "aes128-cbc";
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options); const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options);
return { return {
@@ -64,7 +68,7 @@ function generateSSHKeyPair(
const router = express.Router(); const router = express.Router();
function isNonEmptyString(val: any): val is string { function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0; return typeof val === "string" && val.trim().length > 0;
} }
@@ -79,7 +83,7 @@ router.post(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { const {
name, name,
description, description,
@@ -226,7 +230,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential fetch"); authLogger.warn("Invalid userId for credential fetch");
@@ -259,7 +263,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential folder fetch"); authLogger.warn("Invalid userId for credential folder fetch");
@@ -297,7 +301,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !id) {
@@ -328,19 +332,19 @@ router.get(
const output = formatCredentialOutput(credential); const output = formatCredentialOutput(credential);
if (credential.password) { if (credential.password) {
(output as any).password = credential.password; output.password = credential.password;
} }
if (credential.key) { if (credential.key) {
(output as any).key = credential.key; output.key = credential.key;
} }
if (credential.private_key) { if (credential.private_key) {
(output as any).privateKey = credential.private_key; output.privateKey = credential.private_key;
} }
if (credential.public_key) { if (credential.public_key) {
(output as any).publicKey = credential.public_key; output.publicKey = credential.public_key;
} }
if (credential.key_password) { if (credential.key_password) {
(output as any).keyPassword = credential.key_password; output.keyPassword = credential.key_password;
} }
res.json(output); res.json(output);
@@ -361,7 +365,7 @@ router.put(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
const updateData = req.body; const updateData = req.body;
@@ -385,7 +389,7 @@ router.put(
return res.status(404).json({ error: "Credential not found" }); return res.status(404).json({ error: "Credential not found" });
} }
const updateFields: any = {}; const updateFields: Record<string, string | null | undefined> = {};
if (updateData.name !== undefined) if (updateData.name !== undefined)
updateFields.name = updateData.name.trim(); updateFields.name = updateData.name.trim();
@@ -497,7 +501,7 @@ router.delete(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !id) {
@@ -596,7 +600,7 @@ router.post(
"/:id/apply-to-host/:hostId", "/:id/apply-to-host/:hostId",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId, hostId } = req.params; const { id: credentialId, hostId } = req.params;
if (!isNonEmptyString(userId) || !credentialId || !hostId) { if (!isNonEmptyString(userId) || !credentialId || !hostId) {
@@ -629,8 +633,8 @@ router.post(
.update(sshData) .update(sshData)
.set({ .set({
credentialId: parseInt(credentialId), credentialId: parseInt(credentialId),
username: credential.username, username: credential.username as string,
authType: credential.auth_type || credential.authType, authType: (credential.auth_type || credential.authType) as string,
password: null, password: null,
key: null, key: null,
key_password: null, key_password: null,
@@ -675,7 +679,7 @@ router.get(
"/:id/hosts", "/:id/hosts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
if (!isNonEmptyString(userId) || !credentialId) { if (!isNonEmptyString(userId) || !credentialId) {
@@ -707,7 +711,9 @@ router.get(
}, },
); );
function formatCredentialOutput(credential: any): any { function formatCredentialOutput(
credential: Record<string, unknown>,
): Record<string, unknown> {
return { return {
id: credential.id, id: credential.id,
name: credential.name, name: credential.name,
@@ -731,7 +737,9 @@ function formatCredentialOutput(credential: any): any {
}; };
} }
function formatSSHHostOutput(host: any): any { function formatSSHHostOutput(
host: Record<string, unknown>,
): Record<string, unknown> {
return { return {
id: host.id, id: host.id,
userId: host.userId, userId: host.userId,
@@ -751,7 +759,7 @@ function formatSSHHostOutput(host: any): any {
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
enableTunnel: !!host.enableTunnel, enableTunnel: !!host.enableTunnel,
tunnelConnections: host.tunnelConnections tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections) ? JSON.parse(host.tunnelConnections as string)
: [], : [],
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath, defaultPath: host.defaultPath,
@@ -766,7 +774,7 @@ router.put(
"/folders/rename", "/folders/rename",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body; const { oldName, newName } = req.body;
if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) { if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) {
@@ -970,7 +978,7 @@ router.post(
try { try {
let privateKeyObj; let privateKeyObj;
let parseAttempts = []; const parseAttempts = [];
try { try {
privateKeyObj = crypto.createPrivateKey({ privateKeyObj = crypto.createPrivateKey({
@@ -1093,7 +1101,9 @@ router.post(
finalPublicKey = `${keyType} ${base64Data}`; finalPublicKey = `${keyType} ${base64Data}`;
formatType = "ssh"; formatType = "ssh";
} }
} catch (sshError) {} } catch {
// Ignore validation errors
}
const response = { const response = {
success: true, success: true,
@@ -1117,15 +1127,15 @@ router.post(
); );
async function deploySSHKeyToHost( async function deploySSHKeyToHost(
hostConfig: any, hostConfig: Record<string, unknown>,
publicKey: string, publicKey: string,
credentialData: any, // eslint-disable-next-line @typescript-eslint/no-unused-vars
_credentialData: Record<string, unknown>,
): Promise<{ success: boolean; message?: string; error?: string }> { ): Promise<{ success: boolean; message?: string; error?: string }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const conn = new Client(); const conn = new Client();
let connectionTimeout: NodeJS.Timeout;
connectionTimeout = setTimeout(() => { const connectionTimeout = setTimeout(() => {
conn.destroy(); conn.destroy();
resolve({ success: false, error: "Connection timeout" }); resolve({ success: false, error: "Connection timeout" });
}, 120000); }, 120000);
@@ -1158,7 +1168,9 @@ async function deploySSHKeyToHost(
} }
}); });
stream.on("data", (data) => {}); stream.on("data", () => {
// Ignore output
});
}, },
); );
}); });
@@ -1175,7 +1187,9 @@ async function deploySSHKeyToHost(
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
} }
} catch (e) {} } catch {
// Ignore parse errors
}
const keyParts = actualPublicKey.trim().split(" "); const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) { if (keyParts.length < 2) {
@@ -1202,7 +1216,7 @@ async function deploySSHKeyToHost(
output += data.toString(); output += data.toString();
}); });
stream.on("close", (code) => { stream.on("close", () => {
clearTimeout(checkTimeout); clearTimeout(checkTimeout);
const exists = output.trim() === "0"; const exists = output.trim() === "0";
resolveCheck(exists); resolveCheck(exists);
@@ -1229,7 +1243,9 @@ async function deploySSHKeyToHost(
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
} }
} catch (e) {} } catch {
// Ignore parse errors
}
const escapedKey = actualPublicKey const escapedKey = actualPublicKey
.replace(/\\/g, "\\\\") .replace(/\\/g, "\\\\")
@@ -1269,7 +1285,9 @@ async function deploySSHKeyToHost(
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
} }
} catch (e) {} } catch {
// Ignore parse errors
}
const keyParts = actualPublicKey.trim().split(" "); const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) { if (keyParts.length < 2) {
@@ -1295,7 +1313,7 @@ async function deploySSHKeyToHost(
output += data.toString(); output += data.toString();
}); });
stream.on("close", (code) => { stream.on("close", () => {
clearTimeout(verifyTimeout); clearTimeout(verifyTimeout);
const verified = output.trim() === "0"; const verified = output.trim() === "0";
resolveVerify(verified); resolveVerify(verified);
@@ -1356,7 +1374,7 @@ async function deploySSHKeyToHost(
}); });
try { try {
const connectionConfig: any = { const connectionConfig: Record<string, unknown> = {
host: hostConfig.ip, host: hostConfig.ip,
port: hostConfig.port || 22, port: hostConfig.port || 22,
username: hostConfig.username, username: hostConfig.username,
@@ -1403,14 +1421,15 @@ async function deploySSHKeyToHost(
connectionConfig.password = hostConfig.password; connectionConfig.password = hostConfig.password;
} else if (hostConfig.authType === "key" && hostConfig.privateKey) { } else if (hostConfig.authType === "key" && hostConfig.privateKey) {
try { try {
const privateKey = hostConfig.privateKey as string;
if ( if (
!hostConfig.privateKey.includes("-----BEGIN") || !privateKey.includes("-----BEGIN") ||
!hostConfig.privateKey.includes("-----END") !privateKey.includes("-----END")
) { ) {
throw new Error("Invalid private key format"); throw new Error("Invalid private key format");
} }
const cleanKey = hostConfig.privateKey const cleanKey = privateKey
.trim() .trim()
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.replace(/\r/g, "\n"); .replace(/\r/g, "\n");
@@ -1465,7 +1484,7 @@ router.post(
} }
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
@@ -1521,7 +1540,7 @@ router.post(
const hostData = targetHost[0]; const hostData = targetHost[0];
let hostConfig = { const hostConfig = {
ip: hostData.ip, ip: hostData.ip,
port: hostData.port, port: hostData.port,
username: hostData.username, username: hostData.username,
@@ -1532,7 +1551,7 @@ router.post(
}; };
if (hostData.authType === "credential" && hostData.credentialId) { if (hostData.authType === "credential" && hostData.credentialId) {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!userId) { if (!userId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -1546,7 +1565,7 @@ router.post(
db db
.select() .select()
.from(sshCredentials) .from(sshCredentials)
.where(eq(sshCredentials.id, hostData.credentialId)) .where(eq(sshCredentials.id, hostData.credentialId as number))
.limit(1), .limit(1),
"ssh_credentials", "ssh_credentials",
userId, userId,
@@ -1571,7 +1590,7 @@ router.post(
error: "Host credential not found", error: "Host credential not found",
}); });
} }
} catch (error) { } catch {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: "Failed to resolve host credentials", error: "Failed to resolve host credentials",
@@ -1581,7 +1600,7 @@ router.post(
const deployResult = await deploySSHKeyToHost( const deployResult = await deploySSHKeyToHost(
hostConfig, hostConfig,
credData.publicKey, credData.publicKey as string,
credData, credData,
); );

View File

@@ -0,0 +1,260 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express";
import { db } from "../db/index.js";
import { snippets } from "../db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
const router = express.Router();
function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Get all snippets for the authenticated user
// GET /snippets
router.get(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippets fetch");
return res.status(400).json({ error: "Invalid userId" });
}
try {
const result = await db
.select()
.from(snippets)
.where(eq(snippets.userId, userId))
.orderBy(desc(snippets.updatedAt));
res.json(result);
} catch (err) {
authLogger.error("Failed to fetch snippets", err);
res.status(500).json({ error: "Failed to fetch snippets" });
}
},
);
// Get a specific snippet by ID
// GET /snippets/:id
router.get(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const snippetId = parseInt(id, 10);
if (!isNonEmptyString(userId) || isNaN(snippetId)) {
authLogger.warn("Invalid request for snippet fetch: invalid ID", {
userId,
id,
});
return res.status(400).json({ error: "Invalid request parameters" });
}
try {
const result = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (result.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
res.json(result[0]);
} catch (err) {
authLogger.error("Failed to fetch snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to fetch snippet",
});
}
},
);
// Create a new snippet
// POST /snippets
router.post(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { name, content, description } = req.body;
if (
!isNonEmptyString(userId) ||
!isNonEmptyString(name) ||
!isNonEmptyString(content)
) {
authLogger.warn("Invalid snippet creation data validation failed", {
operation: "snippet_create",
userId,
hasName: !!name,
hasContent: !!content,
});
return res.status(400).json({ error: "Name and content are required" });
}
try {
const insertData = {
userId,
name: name.trim(),
content: content.trim(),
description: description?.trim() || null,
};
const result = await db.insert(snippets).values(insertData).returning();
authLogger.success(`Snippet created: ${name} by user ${userId}`, {
operation: "snippet_create_success",
userId,
snippetId: result[0].id,
name,
});
res.status(201).json(result[0]);
} catch (err) {
authLogger.error("Failed to create snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to create snippet",
});
}
},
);
// Update a snippet
// PUT /snippets/:id
router.put(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const updateData = req.body;
if (!isNonEmptyString(userId) || !id) {
authLogger.warn("Invalid request for snippet update");
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
const updateFields: Partial<{
updatedAt: ReturnType<typeof sql.raw>;
name: string;
content: string;
description: string | null;
}> = {
updatedAt: sql`CURRENT_TIMESTAMP`,
};
if (updateData.name !== undefined)
updateFields.name = updateData.name.trim();
if (updateData.content !== undefined)
updateFields.content = updateData.content.trim();
if (updateData.description !== undefined)
updateFields.description = updateData.description?.trim() || null;
await db
.update(snippets)
.set(updateFields)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
const updated = await db
.select()
.from(snippets)
.where(eq(snippets.id, parseInt(id)));
authLogger.success(
`Snippet updated: ${updated[0].name} by user ${userId}`,
{
operation: "snippet_update_success",
userId,
snippetId: parseInt(id),
name: updated[0].name,
},
);
res.json(updated[0]);
} catch (err) {
authLogger.error("Failed to update snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to update snippet",
});
}
},
);
// Delete a snippet
// DELETE /snippets/:id
router.delete(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
authLogger.warn("Invalid request for snippet delete");
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
await db
.delete(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
authLogger.success(
`Snippet deleted: ${existing[0].name} by user ${userId}`,
{
operation: "snippet_delete_success",
userId,
snippetId: parseInt(id),
name: existing[0].name,
},
);
res.json({ success: true });
} catch (err) {
authLogger.error("Failed to delete snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to delete snippet",
});
}
},
);
export default router;

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { import {
@@ -9,8 +10,7 @@ import {
fileManagerShortcuts, fileManagerShortcuts,
} from "../db/schema.js"; } from "../db/schema.js";
import { eq, and, desc, isNotNull, or } from "drizzle-orm"; import { eq, and, desc, isNotNull, or } from "drizzle-orm";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response } from "express";
import jwt from "jsonwebtoken";
import multer from "multer"; import multer from "multer";
import { sshLogger } from "../../utils/logger.js"; import { sshLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js";
@@ -23,11 +23,11 @@ const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
function isNonEmptyString(value: any): value is string { function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0; return typeof value === "string" && value.trim().length > 0;
} }
function isValidPort(port: any): port is number { function isValidPort(port: unknown): port is number {
return typeof port === "number" && port > 0 && port <= 65535; return typeof port === "number" && port > 0 && port <= 65535;
} }
@@ -75,7 +75,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
: []; : [];
const hasAutoStartTunnels = tunnelConnections.some( const hasAutoStartTunnels = tunnelConnections.some(
(tunnel: any) => tunnel.autoStart, (tunnel: Record<string, unknown>) => tunnel.autoStart,
); );
if (!hasAutoStartTunnels) { if (!hasAutoStartTunnels) {
@@ -100,7 +100,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
credentialId: host.credentialId, credentialId: host.credentialId,
enableTunnel: true, enableTunnel: true,
tunnelConnections: tunnelConnections.filter( tunnelConnections: tunnelConnections.filter(
(tunnel: any) => tunnel.autoStart, (tunnel: Record<string, unknown>) => tunnel.autoStart,
), ),
pin: !!host.pin, pin: !!host.pin,
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
@@ -184,8 +184,8 @@ router.post(
requireDataAccess, requireDataAccess,
upload.single("key"), upload.single("key"),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
let hostData: any; let hostData: Record<string, unknown>;
if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) { if (req.body.data) {
@@ -234,6 +234,7 @@ router.post(
enableFileManager, enableFileManager,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
statsConfig,
} = hostData; } = hostData;
if ( if (
!isNonEmptyString(userId) || !isNonEmptyString(userId) ||
@@ -251,7 +252,7 @@ router.post(
} }
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
userId: userId, userId: userId,
name, name,
folder: folder || null, folder: folder || null,
@@ -269,6 +270,7 @@ router.post(
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
}; };
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
@@ -320,9 +322,12 @@ router.post(
enableTerminal: !!createdHost.enableTerminal, enableTerminal: !!createdHost.enableTerminal,
enableTunnel: !!createdHost.enableTunnel, enableTunnel: !!createdHost.enableTunnel,
tunnelConnections: createdHost.tunnelConnections tunnelConnections: createdHost.tunnelConnections
? JSON.parse(createdHost.tunnelConnections) ? JSON.parse(createdHost.tunnelConnections as string)
: [], : [],
enableFileManager: !!createdHost.enableFileManager, enableFileManager: !!createdHost.enableFileManager,
statsConfig: createdHost.statsConfig
? JSON.parse(createdHost.statsConfig as string)
: undefined,
}; };
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -332,7 +337,7 @@ router.post(
{ {
operation: "host_create_success", operation: "host_create_success",
userId, userId,
hostId: createdHost.id, hostId: createdHost.id as number,
name, name,
ip, ip,
port, port,
@@ -363,8 +368,8 @@ router.put(
upload.single("key"), upload.single("key"),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
let hostData: any; let hostData: Record<string, unknown>;
if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) { if (req.body.data) {
@@ -415,6 +420,7 @@ router.put(
enableFileManager, enableFileManager,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
statsConfig,
} = hostData; } = hostData;
if ( if (
!isNonEmptyString(userId) || !isNonEmptyString(userId) ||
@@ -434,7 +440,7 @@ router.put(
} }
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
name, name,
folder, folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "", tags: Array.isArray(tags) ? tags.join(",") : tags || "",
@@ -451,6 +457,7 @@ router.put(
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
}; };
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
@@ -520,9 +527,12 @@ router.put(
enableTerminal: !!updatedHost.enableTerminal, enableTerminal: !!updatedHost.enableTerminal,
enableTunnel: !!updatedHost.enableTunnel, enableTunnel: !!updatedHost.enableTunnel,
tunnelConnections: updatedHost.tunnelConnections tunnelConnections: updatedHost.tunnelConnections
? JSON.parse(updatedHost.tunnelConnections) ? JSON.parse(updatedHost.tunnelConnections as string)
: [], : [],
enableFileManager: !!updatedHost.enableFileManager, enableFileManager: !!updatedHost.enableFileManager,
statsConfig: updatedHost.statsConfig
? JSON.parse(updatedHost.statsConfig as string)
: undefined,
}; };
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -559,7 +569,7 @@ router.put(
// Route: Get SSH data for the authenticated user (requires JWT) // Route: Get SSH data for the authenticated user (requires JWT)
// GET /ssh/host // GET /ssh/host
router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
sshLogger.warn("Invalid userId for SSH data fetch", { sshLogger.warn("Invalid userId for SSH data fetch", {
operation: "host_fetch", operation: "host_fetch",
@@ -575,7 +585,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
); );
const result = await Promise.all( const result = await Promise.all(
data.map(async (row: any) => { data.map(async (row: Record<string, unknown>) => {
const baseHost = { const baseHost = {
...row, ...row,
tags: tags:
@@ -588,9 +598,12 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
enableTerminal: !!row.enableTerminal, enableTerminal: !!row.enableTerminal,
enableTunnel: !!row.enableTunnel, enableTunnel: !!row.enableTunnel,
tunnelConnections: row.tunnelConnections tunnelConnections: row.tunnelConnections
? JSON.parse(row.tunnelConnections) ? JSON.parse(row.tunnelConnections as string)
: [], : [],
enableFileManager: !!row.enableFileManager, enableFileManager: !!row.enableFileManager,
statsConfig: row.statsConfig
? JSON.parse(row.statsConfig as string)
: undefined,
}; };
return (await resolveHostCredentials(baseHost)) || baseHost; return (await resolveHostCredentials(baseHost)) || baseHost;
@@ -614,7 +627,7 @@ router.get(
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", { sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", {
@@ -655,6 +668,9 @@ router.get(
? JSON.parse(host.tunnelConnections) ? JSON.parse(host.tunnelConnections)
: [], : [],
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
statsConfig: host.statsConfig
? JSON.parse(host.statsConfig)
: undefined,
}; };
res.json((await resolveHostCredentials(result)) || result); res.json((await resolveHostCredentials(result)) || result);
@@ -677,7 +693,7 @@ router.get(
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
return res.status(400).json({ error: "Invalid userId or hostId" }); return res.status(400).json({ error: "Invalid userId or hostId" });
@@ -711,7 +727,7 @@ router.get(
authType: resolvedHost.authType, authType: resolvedHost.authType,
password: resolvedHost.password || null, password: resolvedHost.password || null,
key: resolvedHost.key || null, key: resolvedHost.key || null,
keyPassword: resolvedHost.keyPassword || null, keyPassword: resolvedHost.key_password || null,
keyType: resolvedHost.keyType || null, keyType: resolvedHost.keyType || null,
folder: resolvedHost.folder, folder: resolvedHost.folder,
tags: tags:
@@ -724,7 +740,7 @@ router.get(
enableFileManager: !!resolvedHost.enableFileManager, enableFileManager: !!resolvedHost.enableFileManager,
defaultPath: resolvedHost.defaultPath, defaultPath: resolvedHost.defaultPath,
tunnelConnections: resolvedHost.tunnelConnections tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections) ? JSON.parse(resolvedHost.tunnelConnections as string)
: [], : [],
}; };
@@ -752,7 +768,7 @@ router.delete(
"/db/host/:id", "/db/host/:id",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.params.id; const hostId = req.params.id;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
@@ -816,7 +832,7 @@ router.delete(
), ),
); );
const result = await db await db
.delete(sshData) .delete(sshData)
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
@@ -851,7 +867,7 @@ router.get(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -893,7 +909,7 @@ router.post(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -942,8 +958,8 @@ router.delete(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for recent file deletion"); sshLogger.warn("Invalid data for recent file deletion");
@@ -975,7 +991,7 @@ router.get(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -1016,7 +1032,7 @@ router.post(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1062,8 +1078,8 @@ router.delete(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for pinned file deletion"); sshLogger.warn("Invalid data for pinned file deletion");
@@ -1095,7 +1111,7 @@ router.get(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -1136,7 +1152,7 @@ router.post(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1182,8 +1198,8 @@ router.delete(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for shortcut deletion"); sshLogger.warn("Invalid data for shortcut deletion");
@@ -1209,21 +1225,26 @@ router.delete(
}, },
); );
async function resolveHostCredentials(host: any): Promise<any> { async function resolveHostCredentials(
host: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try { try {
if (host.credentialId && host.userId) { if (host.credentialId && host.userId) {
const credentialId = host.credentialId as number;
const userId = host.userId as string;
const credentials = await SimpleDBOps.select( const credentials = await SimpleDBOps.select(
db db
.select() .select()
.from(sshCredentials) .from(sshCredentials)
.where( .where(
and( and(
eq(sshCredentials.id, host.credentialId), eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, host.userId), eq(sshCredentials.userId, userId),
), ),
), ),
"ssh_credentials", "ssh_credentials",
host.userId, userId,
); );
if (credentials.length > 0) { if (credentials.length > 0) {
@@ -1239,6 +1260,7 @@ async function resolveHostCredentials(host: any): Promise<any> {
}; };
} }
} }
const result = { ...host }; const result = { ...host };
if (host.key_password !== undefined) { if (host.key_password !== undefined) {
if (result.keyPassword === undefined) { if (result.keyPassword === undefined) {
@@ -1261,7 +1283,7 @@ router.put(
"/folders/rename", "/folders/rename",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body; const { oldName, newName } = req.body;
if (!isNonEmptyString(userId) || !oldName || !newName) { if (!isNonEmptyString(userId) || !oldName || !newName) {
@@ -1326,7 +1348,7 @@ router.post(
"/bulk-import", "/bulk-import",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hosts } = req.body; const { hosts } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) { if (!Array.isArray(hosts) || hosts.length === 0) {
@@ -1398,7 +1420,7 @@ router.post(
continue; continue;
} }
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
userId: userId, userId: userId,
name: hostData.name || `${hostData.username}@${hostData.ip}`, name: hostData.name || `${hostData.username}@${hostData.ip}`,
folder: hostData.folder || "Default", folder: hostData.folder || "Default",
@@ -1411,7 +1433,7 @@ router.post(
credentialId: credentialId:
hostData.authType === "credential" ? hostData.credentialId : null, hostData.authType === "credential" ? hostData.credentialId : null,
key: hostData.authType === "key" ? hostData.key : null, key: hostData.authType === "key" ? hostData.key : null,
key_password: keyPassword:
hostData.authType === "key" hostData.authType === "key"
? hostData.keyPassword || hostData.key_password || null ? hostData.keyPassword || hostData.key_password || null
: null, : null,
@@ -1425,6 +1447,9 @@ router.post(
tunnelConnections: hostData.tunnelConnections tunnelConnections: hostData.tunnelConnections
? JSON.stringify(hostData.tunnelConnections) ? JSON.stringify(hostData.tunnelConnections)
: "[]", : "[]",
statsConfig: hostData.statsConfig
? JSON.stringify(hostData.statsConfig)
: null,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
@@ -1455,7 +1480,7 @@ router.post(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body; const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") { if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1519,7 +1544,7 @@ router.post(
const tunnelConnections = JSON.parse(config.tunnelConnections); const tunnelConnections = JSON.parse(config.tunnelConnections);
const resolvedConnections = await Promise.all( const resolvedConnections = await Promise.all(
tunnelConnections.map(async (tunnel: any) => { tunnelConnections.map(async (tunnel: Record<string, unknown>) => {
if ( if (
tunnel.autoStart && tunnel.autoStart &&
tunnel.endpointHost && tunnel.endpointHost &&
@@ -1567,7 +1592,7 @@ router.post(
} }
} }
const updateResult = await db await db
.update(sshData) .update(sshData)
.set({ .set({
autostartPassword: decryptedConfig.password || null, autostartPassword: decryptedConfig.password || null,
@@ -1608,7 +1633,7 @@ router.delete(
"/autostart/disable", "/autostart/disable",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body; const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") { if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1624,7 +1649,7 @@ router.delete(
} }
try { try {
const result = await db await db
.update(sshData) .update(sshData)
.set({ .set({
autostartPassword: null, autostartPassword: null,
@@ -1654,7 +1679,7 @@ router.get(
"/autostart/status", "/autostart/status",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const autostartConfigs = await db const autostartConfigs = await db

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import crypto from "crypto"; import crypto from "crypto";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
@@ -27,48 +28,48 @@ async function verifyOIDCToken(
idToken: string, idToken: string,
issuerUrl: string, issuerUrl: string,
clientId: string, clientId: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
const normalizedIssuerUrl = issuerUrl.endsWith("/")
? issuerUrl.slice(0, -1)
: issuerUrl;
const possibleIssuers = [
issuerUrl,
normalizedIssuerUrl,
issuerUrl.replace(/\/application\/o\/[^/]+$/, ""),
normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, ""),
];
const jwksUrls = [
`${normalizedIssuerUrl}/.well-known/jwks.json`,
`${normalizedIssuerUrl}/jwks/`,
`${normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, "")}/.well-known/jwks.json`,
];
try { try {
const normalizedIssuerUrl = issuerUrl.endsWith("/") const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
? issuerUrl.slice(0, -1) const discoveryResponse = await fetch(discoveryUrl);
: issuerUrl; if (discoveryResponse.ok) {
const possibleIssuers = [ const discovery = (await discoveryResponse.json()) as Record<
issuerUrl, string,
normalizedIssuerUrl, unknown
issuerUrl.replace(/\/application\/o\/[^\/]+$/, ""), >;
normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ""), if (discovery.jwks_uri) {
]; jwksUrls.unshift(discovery.jwks_uri as string);
const jwksUrls = [
`${normalizedIssuerUrl}/.well-known/jwks.json`,
`${normalizedIssuerUrl}/jwks/`,
`${normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, "")}/.well-known/jwks.json`,
];
try {
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) {
const discovery = (await discoveryResponse.json()) as any;
if (discovery.jwks_uri) {
jwksUrls.unshift(discovery.jwks_uri);
}
} }
} catch (discoveryError) {
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
} }
} catch (discoveryError) {
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
}
let jwks: any = null; let jwks: Record<string, unknown> | null = null;
let jwksUrl: string | null = null;
for (const url of jwksUrls) { for (const url of jwksUrls) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
const jwksData = (await response.json()) as any; const jwksData = (await response.json()) as Record<string, unknown>;;
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
jwks = jwksData; jwks = jwksData;
jwksUrl = url;
break; break;
} else { } else {
authLogger.error( authLogger.error(
@@ -77,62 +78,54 @@ async function verifyOIDCToken(
} }
} else { } else {
} }
} catch (error) { } catch {
continue; continue;
} }
} }
if (!jwks) { if (!jwks) {
throw new Error("Failed to fetch JWKS from any URL"); throw new Error("Failed to fetch JWKS from any URL");
}
if (!jwks.keys || !Array.isArray(jwks.keys)) {
throw new Error(
`Invalid JWKS response structure. Expected 'keys' array, got: ${JSON.stringify(jwks)}`,
);
}
const header = JSON.parse(
Buffer.from(idToken.split(".")[0], "base64").toString(),
);
const keyId = header.kid;
const publicKey = jwks.keys.find((key: any) => key.kid === keyId);
if (!publicKey) {
throw new Error(
`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(", ")}`,
);
}
const { importJWK, jwtVerify } = await import("jose");
const key = await importJWK(publicKey);
const { payload } = await jwtVerify(idToken, key, {
issuer: possibleIssuers,
audience: clientId,
});
return payload;
} catch (error) {
throw error;
} }
if (!jwks.keys || !Array.isArray(jwks.keys)) {
throw new Error(
`Invalid JWKS response structure. Expected 'keys' array, got: ${JSON.stringify(jwks)}`,
);
}
const header = JSON.parse(
Buffer.from(idToken.split(".")[0], "base64").toString(),
);
const keyId = header.kid;
const publicKey = jwks.keys.find(
(key: Record<string, unknown>) => key.kid === keyId,
);
if (!publicKey) {
throw new Error(
`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: Record<string, unknown>) => k.kid).join(", ")}`,
);
}
const { importJWK, jwtVerify } = await import("jose");
const key = await importJWK(publicKey);
const { payload } = await jwtVerify(idToken, key, {
issuer: possibleIssuers,
audience: clientId,
});
return payload;
} }
const router = express.Router(); const router = express.Router();
function isNonEmptyString(val: any): val is string { function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0; return typeof val === "string" && val.trim().length > 0;
} }
interface JWTPayload {
userId: string;
iat?: number;
exp?: number;
}
const authenticateJWT = authManager.createAuthMiddleware(); const authenticateJWT = authManager.createAuthMiddleware();
const requireAdmin = authManager.createAdminMiddleware(); const requireAdmin = authManager.createAdminMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Route: Create traditional user (username/password) // Route: Create traditional user (username/password)
// POST /users/create // POST /users/create
@@ -141,7 +134,7 @@ router.post("/create", async (req, res) => {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get(); .get();
if (row && (row as any).value !== "true") { if (row && (row as Record<string, unknown>).value !== "true") {
return res return res
.status(403) .status(403)
.json({ error: "Registration is currently disabled" }); .json({ error: "Registration is currently disabled" });
@@ -186,7 +179,7 @@ router.post("/create", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
isFirstUser = ((countResult as any)?.count || 0) === 0; isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const saltRounds = parseInt(process.env.SALT || "10", 10); const saltRounds = parseInt(process.env.SALT || "10", 10);
const password_hash = await bcrypt.hash(password, saltRounds); const password_hash = await bcrypt.hash(password, saltRounds);
@@ -250,7 +243,7 @@ router.post("/create", async (req, res) => {
// Route: Create OIDC provider configuration (admin only) // Route: Create OIDC provider configuration (admin only)
// POST /users/oidc-config // POST /users/oidc-config
router.post("/oidc-config", authenticateJWT, async (req, res) => { router.post("/oidc-config", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -390,7 +383,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
// Route: Disable OIDC configuration (admin only) // Route: Disable OIDC configuration (admin only)
// DELETE /users/oidc-config // DELETE /users/oidc-config
router.delete("/oidc-config", authenticateJWT, async (req, res) => { router.delete("/oidc-config", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -420,7 +413,7 @@ router.get("/oidc-config", async (req, res) => {
return res.json(null); return res.json(null);
} }
let config = JSON.parse((row as any).value); let config = JSON.parse((row as Record<string, unknown>).value as string);
if (config.client_secret) { if (config.client_secret) {
if (config.client_secret.startsWith("encrypted:")) { if (config.client_secret.startsWith("encrypted:")) {
@@ -450,7 +443,7 @@ router.get("/oidc-config", async (req, res) => {
} else { } else {
config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]"; config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
} }
} catch (decryptError) { } catch {
authLogger.warn("Failed to decrypt OIDC config for admin", { authLogger.warn("Failed to decrypt OIDC config for admin", {
operation: "oidc_config_decrypt_failed", operation: "oidc_config_decrypt_failed",
userId, userId,
@@ -497,13 +490,13 @@ router.get("/oidc/authorize", async (req, res) => {
return res.status(404).json({ error: "OIDC not configured" }); return res.status(404).json({ error: "OIDC not configured" });
} }
const config = JSON.parse((row as any).value); const config = JSON.parse((row as Record<string, unknown>).value as string);
const state = nanoid(); const state = nanoid();
const nonce = nanoid(); const nonce = nanoid();
let origin = let origin =
req.get("Origin") || req.get("Origin") ||
req.get("Referer")?.replace(/\/[^\/]*$/, "") || req.get("Referer")?.replace(/\/[^/]*$/, "") ||
"http://localhost:5173"; "http://localhost:5173";
if (origin.includes("localhost")) { if (origin.includes("localhost")) {
@@ -552,7 +545,8 @@ router.get("/oidc/callback", async (req, res) => {
.status(400) .status(400)
.json({ error: "Invalid state parameter - redirect URI not found" }); .json({ error: "Invalid state parameter - redirect URI not found" });
} }
const redirectUri = (storedRedirectRow as any).value; const redirectUri = (storedRedirectRow as Record<string, unknown>)
.value as string;
try { try {
const storedNonce = db.$client const storedNonce = db.$client
@@ -576,7 +570,9 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(500).json({ error: "OIDC not configured" }); return res.status(500).json({ error: "OIDC not configured" });
} }
const config = JSON.parse((configRow as any).value); const config = JSON.parse(
(configRow as Record<string, unknown>).value as string,
);
const tokenResponse = await fetch(config.token_url, { const tokenResponse = await fetch(config.token_url, {
method: "POST", method: "POST",
@@ -602,26 +598,26 @@ router.get("/oidc/callback", async (req, res) => {
.json({ error: "Failed to exchange authorization code" }); .json({ error: "Failed to exchange authorization code" });
} }
const tokenData = (await tokenResponse.json()) as any; const tokenData = (await tokenResponse.json()) as Record<string, unknown>;
let userInfo: any = null; let userInfo: Record<string, unknown> = null;
let userInfoUrls: string[] = []; const userInfoUrls: string[] = [];
const normalizedIssuerUrl = config.issuer_url.endsWith("/") const normalizedIssuerUrl = config.issuer_url.endsWith("/")
? config.issuer_url.slice(0, -1) ? config.issuer_url.slice(0, -1)
: config.issuer_url; : config.issuer_url;
const baseUrl = normalizedIssuerUrl.replace( const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, "");
/\/application\/o\/[^\/]+$/,
"",
);
try { try {
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
const discoveryResponse = await fetch(discoveryUrl); const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) { if (discoveryResponse.ok) {
const discovery = (await discoveryResponse.json()) as any; const discovery = (await discoveryResponse.json()) as Record<
string,
unknown
>;
if (discovery.userinfo_endpoint) { if (discovery.userinfo_endpoint) {
userInfoUrls.push(discovery.userinfo_endpoint); userInfoUrls.push(discovery.userinfo_endpoint as string);
} }
} }
} catch (discoveryError) { } catch (discoveryError) {
@@ -646,13 +642,14 @@ router.get("/oidc/callback", async (req, res) => {
if (tokenData.id_token) { if (tokenData.id_token) {
try { try {
userInfo = await verifyOIDCToken( userInfo = await verifyOIDCToken(
tokenData.id_token, tokenData.id_token as string,
config.issuer_url, config.issuer_url,
config.client_id, config.client_id,
); );
} catch (error) { } catch {
// Fallback to manual decoding
try { try {
const parts = tokenData.id_token.split("."); const parts = (tokenData.id_token as string).split(".");
if (parts.length === 3) { if (parts.length === 3) {
const payload = JSON.parse( const payload = JSON.parse(
Buffer.from(parts[1], "base64").toString(), Buffer.from(parts[1], "base64").toString(),
@@ -675,7 +672,10 @@ router.get("/oidc/callback", async (req, res) => {
}); });
if (userInfoResponse.ok) { if (userInfoResponse.ok) {
userInfo = await userInfoResponse.json(); userInfo = (await userInfoResponse.json()) as Record<
string,
unknown
>;
break; break;
} else { } else {
authLogger.error( authLogger.error(
@@ -698,24 +698,25 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(400).json({ error: "Failed to get user information" }); return res.status(400).json({ error: "Failed to get user information" });
} }
const getNestedValue = (obj: any, path: string): any => { const getNestedValue = (
obj: Record<string, unknown>,
path: string,
): unknown => {
if (!path || !obj) return null; if (!path || !obj) return null;
return path.split(".").reduce((current, key) => current?.[key], obj); return path.split(".").reduce((current, key) => current?.[key], obj);
}; };
const identifier = const identifier = (getNestedValue(userInfo, config.identifier_path) ||
getNestedValue(userInfo, config.identifier_path) ||
userInfo[config.identifier_path] || userInfo[config.identifier_path] ||
userInfo.sub || userInfo.sub ||
userInfo.email || userInfo.email ||
userInfo.preferred_username; userInfo.preferred_username) as string;
const name = const name = (getNestedValue(userInfo, config.name_path) ||
getNestedValue(userInfo, config.name_path) ||
userInfo[config.name_path] || userInfo[config.name_path] ||
userInfo.name || userInfo.name ||
userInfo.given_name || userInfo.given_name ||
identifier; identifier) as string;
if (!identifier) { if (!identifier) {
authLogger.error( authLogger.error(
@@ -739,7 +740,7 @@ router.get("/oidc/callback", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
isFirstUser = ((countResult as any)?.count || 0) === 0; isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const id = nanoid(); const id = nanoid();
await db.insert(users).values({ await db.insert(users).values({
@@ -749,14 +750,14 @@ router.get("/oidc/callback", async (req, res) => {
is_admin: isFirstUser, is_admin: isFirstUser,
is_oidc: true, is_oidc: true,
oidc_identifier: identifier, oidc_identifier: identifier,
client_id: config.client_id, client_id: String(config.client_id),
client_secret: config.client_secret, client_secret: String(config.client_secret),
issuer_url: config.issuer_url, issuer_url: String(config.issuer_url),
authorization_url: config.authorization_url, authorization_url: String(config.authorization_url),
token_url: config.token_url, token_url: String(config.token_url),
identifier_path: config.identifier_path, identifier_path: String(config.identifier_path),
name_path: config.name_path, name_path: String(config.name_path),
scopes: config.scopes, scopes: String(config.scopes),
}); });
try { try {
@@ -801,7 +802,10 @@ router.get("/oidc/callback", async (req, res) => {
expiresIn: "50d", expiresIn: "50d",
}); });
let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); let frontendUrl = (redirectUri as string).replace(
"/users/oidc/callback",
"",
);
if (frontendUrl.includes("localhost")) { if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173"; frontendUrl = "http://localhost:5173";
@@ -820,7 +824,10 @@ router.get("/oidc/callback", async (req, res) => {
} catch (err) { } catch (err) {
authLogger.error("OIDC callback failed", err); authLogger.error("OIDC callback failed", err);
let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); let frontendUrl = (redirectUri as string).replace(
"/users/oidc/callback",
"",
);
if (frontendUrl.includes("localhost")) { if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173"; frontendUrl = "http://localhost:5173";
@@ -847,6 +854,23 @@ router.post("/login", async (req, res) => {
return res.status(400).json({ error: "Invalid username or password" }); return res.status(400).json({ error: "Invalid username or password" });
} }
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
.get();
if (row && (row as { value: string }).value !== "true") {
return res
.status(403)
.json({ error: "Password authentication is currently disabled" });
}
} catch (e) {
authLogger.error("Failed to check password login status", {
operation: "login_check",
error: e,
});
return res.status(500).json({ error: "Failed to check login status" });
}
try { try {
const user = await db const user = await db
.select() .select()
@@ -893,7 +917,8 @@ router.post("/login", async (req, res) => {
if (kekSalt.length === 0) { if (kekSalt.length === 0) {
await authManager.registerUser(userRecord.id, password); await authManager.registerUser(userRecord.id, password);
} }
} catch (setupError) {} } catch {
}
const dataUnlocked = await authManager.authenticateUser( const dataUnlocked = await authManager.authenticateUser(
userRecord.id, userRecord.id,
@@ -926,7 +951,7 @@ router.post("/login", async (req, res) => {
dataUnlocked: true, dataUnlocked: true,
}); });
const response: any = { const response: Record<string, unknown> = {
success: true, success: true,
is_admin: !!userRecord.is_admin, is_admin: !!userRecord.is_admin,
username: userRecord.username, username: userRecord.username,
@@ -957,7 +982,7 @@ router.post("/login", async (req, res) => {
// POST /users/logout // POST /users/logout
router.post("/logout", async (req, res) => { router.post("/logout", async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (userId) { if (userId) {
authManager.logoutUser(userId); authManager.logoutUser(userId);
@@ -979,7 +1004,7 @@ router.post("/logout", async (req, res) => {
// Route: Get current user's info using JWT // Route: Get current user's info using JWT
// GET /users/me // GET /users/me
router.get("/me", authenticateJWT, async (req: Request, res: Response) => { router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId in JWT for /users/me"); authLogger.warn("Invalid userId in JWT for /users/me");
return res.status(401).json({ error: "Invalid userId" }); return res.status(401).json({ error: "Invalid userId" });
@@ -1014,7 +1039,7 @@ router.get("/setup-required", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
const count = (countResult as any)?.count || 0; const count = (countResult as { count?: number })?.count || 0;
res.json({ res.json({
setup_required: count === 0, setup_required: count === 0,
@@ -1028,7 +1053,7 @@ router.get("/setup-required", async (req, res) => {
// Route: Count users (admin only - for dashboard statistics) // Route: Count users (admin only - for dashboard statistics)
// GET /users/count // GET /users/count
router.get("/count", authenticateJWT, async (req, res) => { router.get("/count", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user[0] || !user[0].is_admin) { if (!user[0] || !user[0].is_admin) {
@@ -1038,7 +1063,7 @@ router.get("/count", authenticateJWT, async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
const count = (countResult as any)?.count || 0; const count = (countResult as { count?: number })?.count || 0;
res.json({ count }); res.json({ count });
} catch (err) { } catch (err) {
authLogger.error("Failed to count users", err); authLogger.error("Failed to count users", err);
@@ -1065,7 +1090,9 @@ router.get("/registration-allowed", async (req, res) => {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get(); .get();
res.json({ allowed: row ? (row as any).value === "true" : true }); res.json({
allowed: row ? (row as Record<string, unknown>).value === "true" : true,
});
} catch (err) { } catch (err) {
authLogger.error("Failed to get registration allowed", err); authLogger.error("Failed to get registration allowed", err);
res.status(500).json({ error: "Failed to get registration allowed" }); res.status(500).json({ error: "Failed to get registration allowed" });
@@ -1075,7 +1102,7 @@ router.get("/registration-allowed", async (req, res) => {
// Route: Set registration allowed status (admin only) // Route: Set registration allowed status (admin only)
// PATCH /users/registration-allowed // PATCH /users/registration-allowed
router.patch("/registration-allowed", authenticateJWT, async (req, res) => { router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1095,10 +1122,51 @@ router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
} }
}); });
// Route: Get password login allowed status (public - needed for login page)
// GET /users/password-login-allowed
router.get("/password-login-allowed", async (req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
.get();
res.json({
allowed: row ? (row as { value: string }).value === "true" : true,
});
} catch (err) {
authLogger.error("Failed to get password login allowed", err);
res.status(500).json({ error: "Failed to get password login allowed" });
}
});
// Route: Set password login allowed status (admin only)
// PATCH /users/password-login-allowed
router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) {
return res.status(403).json({ error: "Not authorized" });
}
const { allowed } = req.body;
if (typeof allowed !== "boolean") {
return res.status(400).json({ error: "Invalid value for allowed" });
}
db.$client
.prepare(
"UPDATE settings SET value = ? WHERE key = 'allow_password_login'",
)
.run(allowed ? "true" : "false");
res.json({ allowed });
} catch (err) {
authLogger.error("Failed to set password login allowed", err);
res.status(500).json({ error: "Failed to set password login allowed" });
}
});
// Route: Delete user account // Route: Delete user account
// DELETE /users/delete-account // DELETE /users/delete-account
router.delete("/delete-account", authenticateJWT, async (req, res) => { router.delete("/delete-account", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!isNonEmptyString(password)) { if (!isNonEmptyString(password)) {
@@ -1134,7 +1202,7 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => {
const adminCount = db.$client const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get(); .get();
if ((adminCount as any)?.count <= 1) { if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res return res
.status(403) .status(403)
.json({ error: "Cannot delete the last admin user" }); .json({ error: "Cannot delete the last admin user" });
@@ -1224,7 +1292,9 @@ router.post("/verify-reset-code", async (req, res) => {
.json({ error: "No reset code found for this user" }); .json({ error: "No reset code found for this user" });
} }
const resetData = JSON.parse((resetDataRow as any).value); const resetData = JSON.parse(
(resetDataRow as Record<string, unknown>).value as string,
);
const now = new Date(); const now = new Date();
const expiresAt = new Date(resetData.expiresAt); const expiresAt = new Date(resetData.expiresAt);
@@ -1282,7 +1352,9 @@ router.post("/complete-reset", async (req, res) => {
return res.status(400).json({ error: "No temporary token found" }); return res.status(400).json({ error: "No temporary token found" });
} }
const tempTokenData = JSON.parse((tempTokenRow as any).value); const tempTokenData = JSON.parse(
(tempTokenRow as Record<string, unknown>).value as string,
);
const now = new Date(); const now = new Date();
const expiresAt = new Date(tempTokenData.expiresAt); const expiresAt = new Date(tempTokenData.expiresAt);
@@ -1315,48 +1387,8 @@ router.post("/complete-reset", async (req, res) => {
.where(eq(users.username, username)); .where(eq(users.username, username));
try { try {
const hasActiveSession = authManager.isUserUnlocked(userId); await authManager.registerUser(userId, newPassword);
authManager.logoutUser(userId);
if (hasActiveSession) {
const success = await authManager.resetUserPasswordWithPreservedDEK(
userId,
newPassword,
);
if (!success) {
authLogger.warn(
`Failed to preserve DEK during password reset for ${username}. Creating new DEK - data will be lost.`,
{
operation: "password_reset_preserve_failed",
userId,
username,
},
);
await authManager.registerUser(userId, newPassword);
authManager.logoutUser(userId);
} else {
authLogger.success(
`Password reset completed for user: ${username}. Data preserved using existing session.`,
{
operation: "password_reset_data_preserved",
userId,
username,
},
);
}
} else {
await authManager.registerUser(userId, newPassword);
authManager.logoutUser(userId);
authLogger.warn(
`Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`,
{
operation: "password_reset_data_inaccessible",
userId,
username,
},
);
}
await db await db
.update(users) .update(users)
@@ -1366,6 +1398,15 @@ router.post("/complete-reset", async (req, res) => {
totp_backup_codes: null, totp_backup_codes: null,
}) })
.where(eq(users.id, userId)); .where(eq(users.id, userId));
authLogger.warn(
`Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`,
{
operation: "password_reset_data_inaccessible",
userId,
username,
},
);
} catch (encryptionError) { } catch (encryptionError) {
authLogger.error( authLogger.error(
"Failed to re-encrypt user data after password reset", "Failed to re-encrypt user data after password reset",
@@ -1382,6 +1423,8 @@ router.post("/complete-reset", async (req, res) => {
}); });
} }
authLogger.success(`Password successfully reset for user: ${username}`);
db.$client db.$client
.prepare("DELETE FROM settings WHERE key = ?") .prepare("DELETE FROM settings WHERE key = ?")
.run(`reset_code_${username}`); .run(`reset_code_${username}`);
@@ -1399,7 +1442,7 @@ router.post("/complete-reset", async (req, res) => {
// Route: List all users (admin only) // Route: List all users (admin only)
// GET /users/list // GET /users/list
router.get("/list", authenticateJWT, async (req, res) => { router.get("/list", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1425,7 +1468,7 @@ router.get("/list", authenticateJWT, async (req, res) => {
// Route: Make user admin (admin only) // Route: Make user admin (admin only)
// POST /users/make-admin // POST /users/make-admin
router.post("/make-admin", authenticateJWT, async (req, res) => { router.post("/make-admin", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1468,7 +1511,7 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
// Route: Remove admin status (admin only) // Route: Remove admin status (admin only)
// POST /users/remove-admin // POST /users/remove-admin
router.post("/remove-admin", authenticateJWT, async (req, res) => { router.post("/remove-admin", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1587,7 +1630,7 @@ router.post("/totp/verify-login", async (req, res) => {
backupCodes = userRecord.totp_backup_codes backupCodes = userRecord.totp_backup_codes
? JSON.parse(userRecord.totp_backup_codes) ? JSON.parse(userRecord.totp_backup_codes)
: []; : [];
} catch (parseError) { } catch {
backupCodes = []; backupCodes = [];
} }
@@ -1625,7 +1668,7 @@ router.post("/totp/verify-login", async (req, res) => {
}); });
} }
const response: any = { const response: Record<string, unknown> = {
success: true, success: true,
is_admin: !!userRecord.is_admin, is_admin: !!userRecord.is_admin,
username: userRecord.username, username: userRecord.username,
@@ -1655,7 +1698,7 @@ router.post("/totp/verify-login", async (req, res) => {
// Route: Setup TOTP // Route: Setup TOTP
// POST /users/totp/setup // POST /users/totp/setup
router.post("/totp/setup", authenticateJWT, async (req, res) => { router.post("/totp/setup", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
@@ -1694,7 +1737,7 @@ router.post("/totp/setup", authenticateJWT, async (req, res) => {
// Route: Enable TOTP // Route: Enable TOTP
// POST /users/totp/enable // POST /users/totp/enable
router.post("/totp/enable", authenticateJWT, async (req, res) => { router.post("/totp/enable", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { totp_code } = req.body; const { totp_code } = req.body;
if (!totp_code) { if (!totp_code) {
@@ -1753,7 +1796,7 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
// Route: Disable TOTP // Route: Disable TOTP
// POST /users/totp/disable // POST /users/totp/disable
router.post("/totp/disable", authenticateJWT, async (req, res) => { router.post("/totp/disable", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body; const { password, totp_code } = req.body;
if (!password && !totp_code) { if (!password && !totp_code) {
@@ -1811,7 +1854,7 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
// Route: Generate new backup codes // Route: Generate new backup codes
// POST /users/totp/backup-codes // POST /users/totp/backup-codes
router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body; const { password, totp_code } = req.body;
if (!password && !totp_code) { if (!password && !totp_code) {
@@ -1869,7 +1912,7 @@ router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
// Route: Delete user (admin only) // Route: Delete user (admin only)
// DELETE /users/delete-user // DELETE /users/delete-user
router.delete("/delete-user", authenticateJWT, async (req, res) => { router.delete("/delete-user", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1898,7 +1941,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
const adminCount = db.$client const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get(); .get();
if ((adminCount as any)?.count <= 1) { if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res return res
.status(403) .status(403)
.json({ error: "Cannot delete the last admin user" }); .json({ error: "Cannot delete the last admin user" });
@@ -1955,7 +1998,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
// Route: User data unlock - used when session expires // Route: User data unlock - used when session expires
// POST /users/unlock-data // POST /users/unlock-data
router.post("/unlock-data", authenticateJWT, async (req, res) => { router.post("/unlock-data", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -1988,7 +2031,7 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
// Route: Check user data unlock status // Route: Check user data unlock status
// GET /users/data-status // GET /users/data-status
router.get("/data-status", authenticateJWT, async (req, res) => { router.get("/data-status", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const isUnlocked = authManager.isUserUnlocked(userId); const isUnlocked = authManager.isUserUnlocked(userId);
@@ -2010,7 +2053,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
// Route: Change user password (re-encrypt data keys) // Route: Change user password (re-encrypt data keys)
// POST /users/change-password // POST /users/change-password
router.post("/change-password", authenticateJWT, async (req, res) => { router.post("/change-password", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { currentPassword, newPassword } = req.body; const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {

View File

@@ -8,6 +8,7 @@ import { eq, and } from "drizzle-orm";
import { fileLogger } from "../utils/logger.js"; import { fileLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
function isExecutableFile(permissions: string, fileName: string): boolean { function isExecutableFile(permissions: string, fileName: string): boolean {
const hasExecutePermission = const hasExecutePermission =
@@ -94,14 +95,25 @@ interface SSHSession {
timeout?: NodeJS.Timeout; timeout?: NodeJS.Timeout;
} }
interface PendingTOTPSession {
client: SSHClient;
finish: (responses: string[]) => void;
config: import("ssh2").ConnectConfig;
createdAt: number;
sessionId: string;
}
const sshSessions: Record<string, SSHSession> = {}; const sshSessions: Record<string, SSHSession> = {};
const pendingTOTPSessions: Record<string, PendingTOTPSession> = {};
function cleanupSession(sessionId: string) { function cleanupSession(sessionId: string) {
const session = sshSessions[sessionId]; const session = sshSessions[sessionId];
if (session) { if (session) {
try { try {
session.client.end(); session.client.end();
} catch {} } catch {
// Ignore connection close errors
}
clearTimeout(session.timeout); clearTimeout(session.timeout);
delete sshSessions[sessionId]; delete sshSessions[sessionId];
} }
@@ -155,7 +167,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
credentialId, credentialId,
} = req.body; } = req.body;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!userId) { if (!userId) {
fileLogger.error("SSH connection rejected: no authenticated user", { fileLogger.error("SSH connection rejected: no authenticated user", {
@@ -235,10 +247,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
); );
} }
const config: any = { const config: Record<string, unknown> = {
host: ip, host: ip,
port: port || 22, port: port || 22,
username, username,
tryKeyboard: true,
readyTimeout: 60000, readyTimeout: 60000,
keepaliveInterval: 30000, keepaliveInterval: 30000,
keepaliveCountMax: 3, keepaliveCountMax: 3,
@@ -364,9 +377,153 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
cleanupSession(sessionId); cleanupSession(sessionId);
}); });
client.on(
"keyboard-interactive",
(
name: string,
instructions: string,
instructionsLang: string,
prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void,
) => {
fileLogger.info("Keyboard-interactive authentication requested", {
operation: "file_keyboard_interactive",
hostId,
sessionId,
promptsCount: prompts.length,
});
const totpPrompt = prompts.find((p) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
p.prompt,
),
);
if (totpPrompt) {
if (responseSent) return;
responseSent = true;
pendingTOTPSessions[sessionId] = {
client,
finish,
config,
createdAt: Date.now(),
sessionId,
};
res.json({
requires_totp: true,
sessionId,
prompt: totpPrompt.prompt,
});
} else {
if (resolvedCredentials.password) {
const responses = prompts.map(
() => resolvedCredentials.password || "",
);
finish(responses);
} else {
finish(prompts.map(() => ""));
}
}
},
);
client.connect(config); client.connect(config);
}); });
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (!userId) {
fileLogger.error("TOTP verification rejected: no authenticated user", {
operation: "file_totp_auth",
sessionId,
});
return res.status(401).json({ error: "Authentication required" });
}
if (!sessionId || !totpCode) {
return res.status(400).json({ error: "Session ID and TOTP code required" });
}
const session = pendingTOTPSessions[sessionId];
if (!session) {
fileLogger.warn("TOTP session not found or expired", {
operation: "file_totp_verify",
sessionId,
userId,
});
return res
.status(404)
.json({ error: "TOTP session expired. Please reconnect." });
}
delete pendingTOTPSessions[sessionId];
if (Date.now() - session.createdAt > 120000) {
try {
session.client.end();
} catch {
// Ignore errors when closing timed out session
}
return res
.status(408)
.json({ error: "TOTP session timeout. Please reconnect." });
}
session.finish([totpCode]);
let responseSent = false;
session.client.on("ready", () => {
if (responseSent) return;
responseSent = true;
sshSessions[sessionId] = {
client: session.client,
isConnected: true,
lastActive: Date.now(),
};
scheduleSessionCleanup(sessionId);
fileLogger.success("TOTP verification successful", {
operation: "file_totp_verify",
sessionId,
userId,
});
res.json({
status: "success",
message: "TOTP verified, SSH connection established",
});
});
session.client.on("error", (err) => {
if (responseSent) return;
responseSent = true;
fileLogger.error("TOTP verification failed", {
operation: "file_totp_verify",
sessionId,
userId,
error: err.message,
});
res.status(401).json({ status: "error", message: "Invalid TOTP code" });
});
setTimeout(() => {
if (!responseSent) {
responseSent = true;
res.status(408).json({ error: "TOTP verification timeout" });
}
}, 60000);
});
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body; const { sessionId } = req.body;
cleanupSession(sessionId); cleanupSession(sessionId);
@@ -455,13 +612,12 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const parts = line.split(/\s+/); const parts = line.split(/\s+/);
if (parts.length >= 9) { if (parts.length >= 9) {
const permissions = parts[0]; const permissions = parts[0];
const linkCount = parts[1];
const owner = parts[2]; const owner = parts[2];
const group = parts[3]; const group = parts[3];
const size = parseInt(parts[4], 10); const size = parseInt(parts[4], 10);
let dateStr = ""; let dateStr = "";
let nameStartIndex = 8; const nameStartIndex = 8;
if (parts[5] && parts[6] && parts[7]) { if (parts[5] && parts[6] && parts[7]) {
dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`; dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`;
@@ -694,7 +850,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
}); });
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const { sessionId, path: filePath, content, hostId, userId } = req.body; const { sessionId, path: filePath, content } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -881,14 +1037,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
}); });
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const { const { sessionId, path: filePath, content, fileName } = req.body;
sessionId,
path: filePath,
content,
fileName,
hostId,
userId,
} = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1022,8 +1171,6 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
} }
if (chunks.length === 1) { if (chunks.length === 1) {
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
@@ -1088,13 +1235,11 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
}); });
}); });
} else { } else {
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
let writeCommand = `> '${escapedPath}'`; let writeCommand = `> '${escapedPath}'`;
chunks.forEach((chunk, index) => { chunks.forEach((chunk) => {
writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`; writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`;
}); });
@@ -1177,14 +1322,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
}); });
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const { const { sessionId, path: filePath, fileName } = req.body;
sessionId,
path: filePath,
fileName,
content = "",
hostId,
userId,
} = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1285,7 +1423,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
}); });
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const { sessionId, path: folderPath, folderName, hostId, userId } = req.body; const { sessionId, path: folderPath, folderName } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1386,7 +1524,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
}); });
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const { sessionId, path: itemPath, isDirectory, hostId, userId } = req.body; const { sessionId, path: itemPath, isDirectory } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1488,7 +1626,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
}); });
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const { sessionId, oldPath, newName, hostId, userId } = req.body; const { sessionId, oldPath, newName } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1596,7 +1734,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
}); });
app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
const { sessionId, oldPath, newPath, hostId, userId } = req.body; const { sessionId, oldPath, newPath } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sessionId) { if (!sessionId) {
@@ -1985,7 +2123,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
}); });
app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const { sessionId, filePath, hostId, userId } = req.body; const { sessionId, filePath } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sshConn || !sshConn.isConnected) { if (!sshConn || !sshConn.isConnected) {
@@ -2022,7 +2160,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
checkResult += data.toString(); checkResult += data.toString();
}); });
checkStream.on("close", (code) => { checkStream.on("close", () => {
if (!checkResult.includes("EXECUTABLE")) { if (!checkResult.includes("EXECUTABLE")) {
return res.status(400).json({ error: "File is not executable" }); return res.status(400).json({ error: "File is not executable" });
} }

View File

@@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm";
import { statsLogger } from "../utils/logger.js"; import { statsLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
interface PooledConnection { interface PooledConnection {
client: Client; client: Client;
@@ -60,7 +61,7 @@ class SSHConnectionPool {
return client; return client;
} }
return new Promise((resolve, reject) => { return new Promise((resolve) => {
const checkAvailable = () => { const checkAvailable = () => {
const available = connections.find((conn) => !conn.inUse); const available = connections.find((conn) => !conn.inUse);
if (available) { if (available) {
@@ -95,6 +96,44 @@ class SSHConnectionPool {
reject(err); reject(err);
}); });
client.on(
"keyboard-interactive",
(
name: string,
instructions: string,
instructionsLang: string,
prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void,
) => {
const totpPrompt = prompts.find((p) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
p.prompt,
),
);
if (totpPrompt) {
statsLogger.warn(
`Server Stats cannot handle TOTP for host ${host.ip}. Connection will fail.`,
{
operation: "server_stats_totp_detected",
hostId: host.id,
},
);
client.end();
reject(
new Error(
"TOTP authentication required but not supported in Server Stats",
),
);
} else if (host.password) {
const responses = prompts.map(() => host.password || "");
finish(responses);
} else {
finish(prompts.map(() => ""));
}
},
);
try { try {
client.connect(buildSshConfig(host)); client.connect(buildSshConfig(host));
} catch (err) { } catch (err) {
@@ -123,7 +162,9 @@ class SSHConnectionPool {
if (!conn.inUse && now - conn.lastUsed > maxAge) { if (!conn.inUse && now - conn.lastUsed > maxAge) {
try { try {
conn.client.end(); conn.client.end();
} catch {} } catch {
// Ignore errors when closing stale connections
}
return false; return false;
} }
return true; return true;
@@ -143,7 +184,9 @@ class SSHConnectionPool {
for (const conn of connections) { for (const conn of connections) {
try { try {
conn.client.end(); conn.client.end();
} catch {} } catch {
// Ignore errors when closing connections during cleanup
}
} }
} }
this.connections.clear(); this.connections.clear();
@@ -151,7 +194,7 @@ class SSHConnectionPool {
} }
class RequestQueue { class RequestQueue {
private queues = new Map<number, Array<() => Promise<any>>>(); private queues = new Map<number, Array<() => Promise<unknown>>>();
private processing = new Set<number>(); private processing = new Set<number>();
async queueRequest<T>(hostId: number, request: () => Promise<T>): Promise<T> { async queueRequest<T>(hostId: number, request: () => Promise<T>): Promise<T> {
@@ -181,7 +224,9 @@ class RequestQueue {
if (request) { if (request) {
try { try {
await request(); await request();
} catch (error) {} } catch {
// Ignore errors from queued requests
}
} }
} }
@@ -193,7 +238,7 @@ class RequestQueue {
} }
interface CachedMetrics { interface CachedMetrics {
data: any; data: unknown;
timestamp: number; timestamp: number;
hostId: number; hostId: number;
} }
@@ -202,7 +247,7 @@ class MetricsCache {
private cache = new Map<number, CachedMetrics>(); private cache = new Map<number, CachedMetrics>();
private ttl = 30000; private ttl = 30000;
get(hostId: number): any | null { get(hostId: number): unknown | null {
const cached = this.cache.get(hostId); const cached = this.cache.get(hostId);
if (cached && Date.now() - cached.timestamp < this.ttl) { if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data; return cached.data;
@@ -210,7 +255,7 @@ class MetricsCache {
return null; return null;
} }
set(hostId: number, data: any): void { set(hostId: number, data: unknown): void {
this.cache.set(hostId, { this.cache.set(hostId, {
data, data,
timestamp: Date.now(), timestamp: Date.now(),
@@ -253,7 +298,8 @@ interface SSHHostWithCredentials {
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: any[]; tunnelConnections: unknown[];
statsConfig?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
userId: string; userId: string;
@@ -387,11 +433,11 @@ async function fetchHostById(
} }
async function resolveHostCredentials( async function resolveHostCredentials(
host: any, host: Record<string, unknown>,
userId: string, userId: string,
): Promise<SSHHostWithCredentials | undefined> { ): Promise<SSHHostWithCredentials | undefined> {
try { try {
const baseHost: any = { const baseHost: Record<string, unknown> = {
id: host.id, id: host.id,
name: host.name, name: host.name,
ip: host.ip, ip: host.ip,
@@ -411,8 +457,9 @@ async function resolveHostCredentials(
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath || "/", defaultPath: host.defaultPath || "/",
tunnelConnections: host.tunnelConnections tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections) ? JSON.parse(host.tunnelConnections as string)
: [], : [],
statsConfig: host.statsConfig || undefined,
createdAt: host.createdAt, createdAt: host.createdAt,
updatedAt: host.updatedAt, updatedAt: host.updatedAt,
userId: host.userId, userId: host.userId,
@@ -426,7 +473,7 @@ async function resolveHostCredentials(
.from(sshCredentials) .from(sshCredentials)
.where( .where(
and( and(
eq(sshCredentials.id, host.credentialId), eq(sshCredentials.id, host.credentialId as number),
eq(sshCredentials.userId, userId), eq(sshCredentials.userId, userId),
), ),
), ),
@@ -466,7 +513,7 @@ async function resolveHostCredentials(
addLegacyCredentials(baseHost, host); addLegacyCredentials(baseHost, host);
} }
return baseHost; return baseHost as unknown as SSHHostWithCredentials;
} catch (error) { } catch (error) {
statsLogger.error( statsLogger.error(
`Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, `Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -475,7 +522,10 @@ async function resolveHostCredentials(
} }
} }
function addLegacyCredentials(baseHost: any, host: any): void { function addLegacyCredentials(
baseHost: Record<string, unknown>,
host: Record<string, unknown>,
): void {
baseHost.password = host.password || null; baseHost.password = host.password || null;
baseHost.key = host.key || null; baseHost.key = host.key || null;
baseHost.keyPassword = host.key_password || host.keyPassword || null; baseHost.keyPassword = host.key_password || host.keyPassword || null;
@@ -487,6 +537,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
host: host.ip, host: host.ip,
port: host.port || 22, port: host.port || 22,
username: host.username || "root", username: host.username || "root",
tryKeyboard: true,
readyTimeout: 10_000, readyTimeout: 10_000,
algorithms: { algorithms: {
kex: [ kex: [
@@ -526,7 +577,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
if (!host.password) { if (!host.password) {
throw new Error(`No password available for host ${host.ip}`); throw new Error(`No password available for host ${host.ip}`);
} }
(base as any).password = host.password; (base as Record<string, unknown>).password = host.password;
} else if (host.authType === "key") { } else if (host.authType === "key") {
if (!host.key) { if (!host.key) {
throw new Error(`No SSH key available for host ${host.ip}`); throw new Error(`No SSH key available for host ${host.ip}`);
@@ -542,10 +593,13 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.replace(/\r/g, "\n"); .replace(/\r/g, "\n");
(base as any).privateKey = Buffer.from(cleanKey, "utf8"); (base as Record<string, unknown>).privateKey = Buffer.from(
cleanKey,
"utf8",
);
if (host.keyPassword) { if (host.keyPassword) {
(base as any).passphrase = host.keyPassword; (base as Record<string, unknown>).passphrase = host.keyPassword;
} }
} catch (keyError) { } catch (keyError) {
statsLogger.error( statsLogger.error(
@@ -643,175 +697,387 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
percent: number | null; percent: number | null;
usedHuman: string | null; usedHuman: string | null;
totalHuman: string | null; totalHuman: string | null;
availableHuman: string | null;
};
network: {
interfaces: Array<{
name: string;
ip: string;
state: string;
rxBytes: string | null;
txBytes: string | null;
}>;
};
uptime: {
seconds: number | null;
formatted: string | null;
};
processes: {
total: number | null;
running: number | null;
top: Array<{
pid: string;
user: string;
cpu: string;
mem: string;
command: string;
}>;
};
system: {
hostname: string | null;
kernel: string | null;
os: string | null;
}; };
}> { }> {
const cached = metricsCache.get(host.id); const cached = metricsCache.get(host.id);
if (cached) { if (cached) {
return cached; return cached as ReturnType<typeof collectMetrics> extends Promise<infer T>
? T
: never;
} }
return requestQueue.queueRequest(host.id, async () => { return requestQueue.queueRequest(host.id, async () => {
return withSshConnection(host, async (client) => { try {
let cpuPercent: number | null = null; return await withSshConnection(host, async (client) => {
let cores: number | null = null; let cpuPercent: number | null = null;
let loadTriplet: [number, number, number] | null = null; let cores: number | null = null;
let loadTriplet: [number, number, number] | null = null;
try { try {
const [stat1, loadAvgOut, coresOut] = await Promise.all([ const [stat1, loadAvgOut, coresOut] = await Promise.all([
execCommand(client, "cat /proc/stat"), execCommand(client, "cat /proc/stat"),
execCommand(client, "cat /proc/loadavg"), execCommand(client, "cat /proc/loadavg"),
execCommand( execCommand(
client, client,
"nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo", "nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo",
), ),
]); ]);
await new Promise((r) => setTimeout(r, 500)); await new Promise((r) => setTimeout(r, 500));
const stat2 = await execCommand(client, "cat /proc/stat"); const stat2 = await execCommand(client, "cat /proc/stat");
const cpuLine1 = ( const cpuLine1 = (
stat1.stdout.split("\n").find((l) => l.startsWith("cpu ")) || "" stat1.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
).trim(); ).trim();
const cpuLine2 = ( const cpuLine2 = (
stat2.stdout.split("\n").find((l) => l.startsWith("cpu ")) || "" stat2.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
).trim(); ).trim();
const a = parseCpuLine(cpuLine1); const a = parseCpuLine(cpuLine1);
const b = parseCpuLine(cpuLine2); const b = parseCpuLine(cpuLine2);
if (a && b) { if (a && b) {
const totalDiff = b.total - a.total; const totalDiff = b.total - a.total;
const idleDiff = b.idle - a.idle; const idleDiff = b.idle - a.idle;
const used = totalDiff - idleDiff; const used = totalDiff - idleDiff;
if (totalDiff > 0) if (totalDiff > 0)
cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100)); cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100));
}
const laParts = loadAvgOut.stdout.trim().split(/\s+/);
if (laParts.length >= 3) {
loadTriplet = [
Number(laParts[0]),
Number(laParts[1]),
Number(laParts[2]),
].map((v) => (Number.isFinite(v) ? Number(v) : 0)) as [
number,
number,
number,
];
}
const coresNum = Number((coresOut.stdout || "").trim());
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
} catch (e) {
statsLogger.warn(
`Failed to collect CPU metrics for host ${host.id}`,
e,
);
cpuPercent = null;
cores = null;
loadTriplet = null;
}
let memPercent: number | null = null;
let usedGiB: number | null = null;
let totalGiB: number | null = null;
try {
const memInfo = await execCommand(client, "cat /proc/meminfo");
const lines = memInfo.stdout.split("\n");
const getVal = (key: string) => {
const line = lines.find((l) => l.startsWith(key));
if (!line) return null;
const m = line.match(/\d+/);
return m ? Number(m[0]) : null;
};
const totalKb = getVal("MemTotal:");
const availKb = getVal("MemAvailable:");
if (totalKb && availKb && totalKb > 0) {
const usedKb = totalKb - availKb;
memPercent = Math.max(0, Math.min(100, (usedKb / totalKb) * 100));
usedGiB = kibToGiB(usedKb);
totalGiB = kibToGiB(totalKb);
}
} catch (e) {
statsLogger.warn(
`Failed to collect memory metrics for host ${host.id}`,
e,
);
memPercent = null;
usedGiB = null;
totalGiB = null;
}
let diskPercent: number | null = null;
let usedHuman: string | null = null;
let totalHuman: string | null = null;
let availableHuman: string | null = null;
try {
const [diskOutHuman, diskOutBytes] = await Promise.all([
execCommand(client, "df -h -P / | tail -n +2"),
execCommand(client, "df -B1 -P / | tail -n +2"),
]);
const humanLine =
diskOutHuman.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)[0] || "";
const bytesLine =
diskOutBytes.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)[0] || "";
const humanParts = humanLine.split(/\s+/);
const bytesParts = bytesLine.split(/\s+/);
if (humanParts.length >= 6 && bytesParts.length >= 6) {
totalHuman = humanParts[1] || null;
usedHuman = humanParts[2] || null;
availableHuman = humanParts[3] || null;
const totalBytes = Number(bytesParts[1]);
const usedBytes = Number(bytesParts[2]);
if (
Number.isFinite(totalBytes) &&
Number.isFinite(usedBytes) &&
totalBytes > 0
) {
diskPercent = Math.max(
0,
Math.min(100, (usedBytes / totalBytes) * 100),
);
} }
const laParts = loadAvgOut.stdout.trim().split(/\s+/);
if (laParts.length >= 3) {
loadTriplet = [
Number(laParts[0]),
Number(laParts[1]),
Number(laParts[2]),
].map((v) => (Number.isFinite(v) ? Number(v) : 0)) as [
number,
number,
number,
];
}
const coresNum = Number((coresOut.stdout || "").trim());
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
} catch (e) {
statsLogger.warn(
`Failed to collect CPU metrics for host ${host.id}`,
e,
);
cpuPercent = null;
cores = null;
loadTriplet = null;
} }
} catch (e) {
statsLogger.warn( let memPercent: number | null = null;
`Failed to collect disk metrics for host ${host.id}`, let usedGiB: number | null = null;
e, let totalGiB: number | null = null;
); try {
diskPercent = null; const memInfo = await execCommand(client, "cat /proc/meminfo");
usedHuman = null; const lines = memInfo.stdout.split("\n");
totalHuman = null; const getVal = (key: string) => {
availableHuman = null; const line = lines.find((l) => l.startsWith(key));
if (!line) return null;
const m = line.match(/\d+/);
return m ? Number(m[0]) : null;
};
const totalKb = getVal("MemTotal:");
const availKb = getVal("MemAvailable:");
if (totalKb && availKb && totalKb > 0) {
const usedKb = totalKb - availKb;
memPercent = Math.max(0, Math.min(100, (usedKb / totalKb) * 100));
usedGiB = kibToGiB(usedKb);
totalGiB = kibToGiB(totalKb);
}
} catch (e) {
statsLogger.warn(
`Failed to collect memory metrics for host ${host.id}`,
e,
);
memPercent = null;
usedGiB = null;
totalGiB = null;
}
let diskPercent: number | null = null;
let usedHuman: string | null = null;
let totalHuman: string | null = null;
let availableHuman: string | null = null;
try {
const [diskOutHuman, diskOutBytes] = await Promise.all([
execCommand(client, "df -h -P / | tail -n +2"),
execCommand(client, "df -B1 -P / | tail -n +2"),
]);
const humanLine =
diskOutHuman.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)[0] || "";
const bytesLine =
diskOutBytes.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)[0] || "";
const humanParts = humanLine.split(/\s+/);
const bytesParts = bytesLine.split(/\s+/);
if (humanParts.length >= 6 && bytesParts.length >= 6) {
totalHuman = humanParts[1] || null;
usedHuman = humanParts[2] || null;
availableHuman = humanParts[3] || null;
const totalBytes = Number(bytesParts[1]);
const usedBytes = Number(bytesParts[2]);
if (
Number.isFinite(totalBytes) &&
Number.isFinite(usedBytes) &&
totalBytes > 0
) {
diskPercent = Math.max(
0,
Math.min(100, (usedBytes / totalBytes) * 100),
);
}
}
} catch (e) {
statsLogger.warn(
`Failed to collect disk metrics for host ${host.id}`,
e,
);
diskPercent = null;
usedHuman = null;
totalHuman = null;
availableHuman = null;
}
// Collect network interfaces
const interfaces: Array<{
name: string;
ip: string;
state: string;
rxBytes: string | null;
txBytes: string | null;
}> = [];
try {
const ifconfigOut = await execCommand(
client,
"ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'",
);
const netStatOut = await execCommand(
client,
"ip -o link show | awk '{print $2,$9}' | sed 's/:$//'",
);
const addrs = ifconfigOut.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
const states = netStatOut.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
const ifMap = new Map<string, { ip: string; state: string }>();
for (const line of addrs) {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
const name = parts[0];
const ip = parts[1].split("/")[0];
if (!ifMap.has(name)) ifMap.set(name, { ip, state: "UNKNOWN" });
}
}
for (const line of states) {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
const name = parts[0];
const state = parts[1];
const existing = ifMap.get(name);
if (existing) {
existing.state = state;
}
}
}
for (const [name, data] of ifMap.entries()) {
interfaces.push({
name,
ip: data.ip,
state: data.state,
rxBytes: null,
txBytes: null,
});
}
} catch (e) {
statsLogger.warn(
`Failed to collect network metrics for host ${host.id}`,
e,
);
}
// Collect uptime
let uptimeSeconds: number | null = null;
let uptimeFormatted: string | null = null;
try {
const uptimeOut = await execCommand(client, "cat /proc/uptime");
const uptimeParts = uptimeOut.stdout.trim().split(/\s+/);
if (uptimeParts.length >= 1) {
uptimeSeconds = Number(uptimeParts[0]);
if (Number.isFinite(uptimeSeconds)) {
const days = Math.floor(uptimeSeconds / 86400);
const hours = Math.floor((uptimeSeconds % 86400) / 3600);
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
}
}
} catch (e) {
statsLogger.warn(`Failed to collect uptime for host ${host.id}`, e);
}
// Collect process information
let totalProcesses: number | null = null;
let runningProcesses: number | null = null;
const topProcesses: Array<{
pid: string;
user: string;
cpu: string;
mem: string;
command: string;
}> = [];
try {
const psOut = await execCommand(
client,
"ps aux --sort=-%cpu | head -n 11",
);
const psLines = psOut.stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
if (psLines.length > 1) {
for (let i = 1; i < Math.min(psLines.length, 11); i++) {
const parts = psLines[i].split(/\s+/);
if (parts.length >= 11) {
topProcesses.push({
pid: parts[1],
user: parts[0],
cpu: parts[2],
mem: parts[3],
command: parts.slice(10).join(" ").substring(0, 50),
});
}
}
}
const procCount = await execCommand(client, "ps aux | wc -l");
const runningCount = await execCommand(
client,
"ps aux | grep -c ' R '",
);
totalProcesses = Number(procCount.stdout.trim()) - 1;
runningProcesses = Number(runningCount.stdout.trim());
} catch (e) {
statsLogger.warn(
`Failed to collect process info for host ${host.id}`,
e,
);
}
// Collect system information
let hostname: string | null = null;
let kernel: string | null = null;
let os: string | null = null;
try {
const hostnameOut = await execCommand(client, "hostname");
const kernelOut = await execCommand(client, "uname -r");
const osOut = await execCommand(
client,
"cat /etc/os-release | grep '^PRETTY_NAME=' | cut -d'\"' -f2",
);
hostname = hostnameOut.stdout.trim() || null;
kernel = kernelOut.stdout.trim() || null;
os = osOut.stdout.trim() || null;
} catch (e) {
statsLogger.warn(
`Failed to collect system info for host ${host.id}`,
e,
);
}
const result = {
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
memory: {
percent: toFixedNum(memPercent, 0),
usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null,
totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null,
},
disk: {
percent: toFixedNum(diskPercent, 0),
usedHuman,
totalHuman,
availableHuman,
},
network: {
interfaces,
},
uptime: {
seconds: uptimeSeconds,
formatted: uptimeFormatted,
},
processes: {
total: totalProcesses,
running: runningProcesses,
top: topProcesses,
},
system: {
hostname,
kernel,
os,
},
};
metricsCache.set(host.id, result);
return result;
});
} catch (error) {
if (
error instanceof Error &&
error.message.includes("TOTP authentication required")
) {
throw error;
} }
throw error;
const result = { }
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
memory: {
percent: toFixedNum(memPercent, 0),
usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null,
totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null,
},
disk: {
percent: toFixedNum(diskPercent, 0),
usedHuman,
totalHuman,
availableHuman,
},
};
metricsCache.set(host.id, result);
return result;
});
}); });
} }
@@ -829,7 +1095,9 @@ function tcpPing(
settled = true; settled = true;
try { try {
socket.destroy(); socket.destroy();
} catch {} } catch {
// Ignore errors when destroying socket
}
resolve(result); resolve(result);
}; };
@@ -859,8 +1127,6 @@ async function pollStatusesOnce(userId?: string): Promise<void> {
return; return;
} }
const now = new Date().toISOString();
const checks = hosts.map(async (h) => { const checks = hosts.map(async (h) => {
const isOnline = await tcpPing(h.ip, h.port, 5000); const isOnline = await tcpPing(h.ip, h.port, 5000);
const now = new Date().toISOString(); const now = new Date().toISOString();
@@ -886,7 +1152,7 @@ async function pollStatusesOnce(userId?: string): Promise<void> {
} }
app.get("/status", async (req, res) => { app.get("/status", async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -907,7 +1173,7 @@ app.get("/status", async (req, res) => {
app.get("/status/:id", validateHostId, async (req, res) => { app.get("/status/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id); const id = Number(req.params.id);
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -938,7 +1204,7 @@ app.get("/status/:id", validateHostId, async (req, res) => {
}); });
app.post("/refresh", async (req, res) => { app.post("/refresh", async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -953,7 +1219,7 @@ app.post("/refresh", async (req, res) => {
app.get("/metrics/:id", validateHostId, async (req, res) => { app.get("/metrics/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id); const id = Number(req.params.id);
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -974,7 +1240,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
error: "Host is offline", error: "Host is offline",
cpu: { percent: null, cores: null, load: null }, cpu: { percent: null, cores: null, load: null },
memory: { percent: null, usedGiB: null, totalGiB: null }, memory: { percent: null, usedGiB: null, totalGiB: null },
disk: { percent: null, usedHuman: null, totalHuman: null }, disk: {
percent: null,
usedHuman: null,
totalHuman: null,
availableHuman: null,
},
network: { interfaces: [] },
uptime: { seconds: null, formatted: null },
processes: { total: null, running: null, top: [] },
system: { hostname: null, kernel: null, os: null },
lastChecked: new Date().toISOString(), lastChecked: new Date().toISOString(),
}); });
} }
@@ -982,6 +1257,29 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
const metrics = await collectMetrics(host); const metrics = await collectMetrics(host);
res.json({ ...metrics, lastChecked: new Date().toISOString() }); res.json({ ...metrics, lastChecked: new Date().toISOString() });
} catch (err) { } catch (err) {
if (
err instanceof Error &&
err.message.includes("TOTP authentication required")
) {
return res.status(403).json({
error: "TOTP_REQUIRED",
message: "Server Stats unavailable for TOTP-enabled servers",
cpu: { percent: null, cores: null, load: null },
memory: { percent: null, usedGiB: null, totalGiB: null },
disk: {
percent: null,
usedHuman: null,
totalHuman: null,
availableHuman: null,
},
network: { interfaces: [] },
uptime: { seconds: null, formatted: null },
processes: { total: null, running: null, top: [] },
system: { hostname: null, kernel: null, os: null },
lastChecked: new Date().toISOString(),
});
}
statsLogger.error("Failed to collect metrics", err); statsLogger.error("Failed to collect metrics", err);
if (err instanceof Error && err.message.includes("timeout")) { if (err instanceof Error && err.message.includes("timeout")) {
@@ -989,7 +1287,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
error: "Metrics collection timeout", error: "Metrics collection timeout",
cpu: { percent: null, cores: null, load: null }, cpu: { percent: null, cores: null, load: null },
memory: { percent: null, usedGiB: null, totalGiB: null }, memory: { percent: null, usedGiB: null, totalGiB: null },
disk: { percent: null, usedHuman: null, totalHuman: null }, disk: {
percent: null,
usedHuman: null,
totalHuman: null,
availableHuman: null,
},
network: { interfaces: [] },
uptime: { seconds: null, formatted: null },
processes: { total: null, running: null, top: [] },
system: { hostname: null, kernel: null, os: null },
lastChecked: new Date().toISOString(), lastChecked: new Date().toISOString(),
}); });
} }
@@ -998,7 +1305,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
error: "Failed to collect metrics", error: "Failed to collect metrics",
cpu: { percent: null, cores: null, load: null }, cpu: { percent: null, cores: null, load: null },
memory: { percent: null, usedGiB: null, totalGiB: null }, memory: { percent: null, usedGiB: null, totalGiB: null },
disk: { percent: null, usedHuman: null, totalHuman: null }, disk: {
percent: null,
usedHuman: null,
totalHuman: null,
availableHuman: null,
},
network: { interfaces: [] },
uptime: { seconds: null, formatted: null },
processes: { total: null, running: null, top: [] },
system: { hostname: null, kernel: null, os: null },
lastChecked: new Date().toISOString(), lastChecked: new Date().toISOString(),
}); });
} }

View File

@@ -1,5 +1,10 @@
import { WebSocketServer, WebSocket, type RawData } from "ws"; import { WebSocketServer, WebSocket, type RawData } from "ws";
import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; import {
Client,
type ClientChannel,
type PseudoTtyOptions,
type ConnectConfig,
} from "ssh2";
import { parse as parseUrl } from "url"; import { parse as parseUrl } from "url";
import { getDb } from "../database/db/index.js"; import { getDb } from "../database/db/index.js";
import { sshCredentials } from "../database/db/schema.js"; import { sshCredentials } from "../database/db/schema.js";
@@ -9,6 +14,42 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import { UserCrypto } from "../utils/user-crypto.js"; import { UserCrypto } from "../utils/user-crypto.js";
interface ConnectToHostData {
cols: number;
rows: number;
hostConfig: {
id: number;
ip: string;
port: number;
username: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
authType?: string;
credentialId?: number;
userId?: string;
};
initialPath?: string;
executeCommand?: string;
}
interface ResizeData {
cols: number;
rows: number;
}
interface TOTPResponseData {
code?: string;
}
interface WebSocketMessage {
type: string;
data?: ConnectToHostData | ResizeData | TOTPResponseData | string | unknown;
code?: string;
[key: string]: unknown;
}
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const userCrypto = UserCrypto.getInstance(); const userCrypto = UserCrypto.getInstance();
@@ -79,7 +120,6 @@ const wss = new WebSocketServer({
wss.on("connection", async (ws: WebSocket, req) => { wss.on("connection", async (ws: WebSocket, req) => {
let userId: string | undefined; let userId: string | undefined;
let userPayload: any;
try { try {
const url = parseUrl(req.url!, true); const url = parseUrl(req.url!, true);
@@ -113,7 +153,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
} }
userId = payload.userId; userId = payload.userId;
userPayload = payload;
} catch (error) { } catch (error) {
sshLogger.error( sshLogger.error(
"WebSocket JWT verification failed during connection", "WebSocket JWT verification failed during connection",
@@ -154,6 +193,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sshConn: Client | null = null; let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null; let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null; let pingInterval: NodeJS.Timeout | null = null;
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
ws.on("close", () => { ws.on("close", () => {
const userWs = userConnections.get(userId); const userWs = userConnections.get(userId);
@@ -186,9 +226,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
return; return;
} }
let parsed: any; let parsed: WebSocketMessage;
try { try {
parsed = JSON.parse(msg.toString()); parsed = JSON.parse(msg.toString()) as WebSocketMessage;
} catch (e) { } catch (e) {
sshLogger.error("Invalid JSON received", e, { sshLogger.error("Invalid JSON received", e, {
operation: "websocket_message_invalid_json", operation: "websocket_message_invalid_json",
@@ -202,16 +242,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
const { type, data } = parsed; const { type, data } = parsed;
switch (type) { switch (type) {
case "connectToHost": case "connectToHost": {
if (data.hostConfig) { const connectData = data as ConnectToHostData;
data.hostConfig.userId = userId; if (connectData.hostConfig) {
connectData.hostConfig.userId = userId;
} }
handleConnectToHost(data).catch((error) => { handleConnectToHost(connectData).catch((error) => {
sshLogger.error("Failed to connect to host", error, { sshLogger.error("Failed to connect to host", error, {
operation: "ssh_connect", operation: "ssh_connect",
userId, userId,
hostId: data.hostConfig?.id, hostId: connectData.hostConfig?.id,
ip: data.hostConfig?.ip, ip: connectData.hostConfig?.ip,
}); });
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
@@ -223,40 +264,77 @@ wss.on("connection", async (ws: WebSocket, req) => {
); );
}); });
break; break;
}
case "resize": case "resize": {
handleResize(data); const resizeData = data as ResizeData;
handleResize(resizeData);
break; break;
}
case "disconnect": case "disconnect":
cleanupSSH(); cleanupSSH();
break; break;
case "input": case "input": {
const inputData = data as string;
if (sshStream) { if (sshStream) {
if (data === "\t") { if (inputData === "\t") {
sshStream.write(data); sshStream.write(inputData);
} else if (data.startsWith("\x1b")) { } else if (
sshStream.write(data); typeof inputData === "string" &&
inputData.startsWith("\x1b")
) {
sshStream.write(inputData);
} else { } else {
try { try {
sshStream.write(Buffer.from(data, "utf8")); sshStream.write(Buffer.from(inputData, "utf8"));
} catch (error) { } catch (error) {
sshLogger.error("Error writing input to SSH stream", error, { sshLogger.error("Error writing input to SSH stream", error, {
operation: "ssh_input_encoding", operation: "ssh_input_encoding",
userId, userId,
dataLength: data.length, dataLength: inputData.length,
}); });
sshStream.write(Buffer.from(data, "latin1")); sshStream.write(Buffer.from(inputData, "latin1"));
} }
} }
} }
break; break;
}
case "ping": case "ping":
ws.send(JSON.stringify({ type: "pong" })); ws.send(JSON.stringify({ type: "pong" }));
break; break;
case "totp_response": {
const totpData = data as TOTPResponseData;
if (keyboardInteractiveFinish && totpData?.code) {
const totpCode = totpData.code;
sshLogger.info("TOTP code received from user", {
operation: "totp_response",
userId,
codeLength: totpCode.length,
});
keyboardInteractiveFinish([totpCode]);
keyboardInteractiveFinish = null;
} else {
sshLogger.warn("TOTP response received but no callback available", {
operation: "totp_response_error",
userId,
hasCallback: !!keyboardInteractiveFinish,
hasCode: !!totpData?.code,
});
ws.send(
JSON.stringify({
type: "error",
message: "TOTP authentication state lost. Please reconnect.",
}),
);
}
break;
}
default: default:
sshLogger.warn("Unknown message type received", { sshLogger.warn("Unknown message type received", {
operation: "websocket_message_unknown_type", operation: "websocket_message_unknown_type",
@@ -266,26 +344,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
} }
}); });
async function handleConnectToHost(data: { async function handleConnectToHost(data: ConnectToHostData) {
cols: number; const { hostConfig, initialPath, executeCommand } = data;
rows: number;
hostConfig: {
id: number;
ip: string;
port: number;
username: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
authType?: string;
credentialId?: number;
userId?: string;
};
initialPath?: string;
executeCommand?: string;
}) {
const { cols, rows, hostConfig, initialPath, executeCommand } = data;
const { const {
id, id,
ip, ip,
@@ -375,12 +435,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedCredentials = { resolvedCredentials = {
password: credential.password, password: credential.password as string | undefined,
key: key: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authType: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authType: (credential.auth_type || credential.authType) as
| string
| undefined,
}; };
} else { } else {
sshLogger.warn(`No credentials found for host ${id}`, { sshLogger.warn(`No credentials found for host ${id}`, {
@@ -557,16 +624,66 @@ wss.on("connection", async (ws: WebSocket, req) => {
cleanupSSH(connectionTimeout); cleanupSSH(connectionTimeout);
}); });
sshConn.on(
"keyboard-interactive",
(
name: string,
instructions: string,
instructionsLang: string,
prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void,
) => {
sshLogger.info("Keyboard-interactive authentication requested", {
operation: "ssh_keyboard_interactive",
hostId: id,
promptsCount: prompts.length,
instructions: instructions || "none",
});
const totpPrompt = prompts.find((p) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
p.prompt,
),
);
if (totpPrompt) {
keyboardInteractiveFinish = finish;
ws.send(
JSON.stringify({
type: "totp_required",
prompt: totpPrompt.prompt,
}),
);
} else {
if (resolvedCredentials.password) {
const responses = prompts.map(
() => resolvedCredentials.password || "",
);
finish(responses);
} else {
sshLogger.warn(
"Keyboard-interactive requires password but none available",
{
operation: "ssh_keyboard_interactive_no_password",
hostId: id,
},
);
finish(prompts.map(() => ""));
}
}
},
);
const connectConfig: any = { const connectConfig: any = {
host: ip, host: ip,
port, port,
username, username,
tryKeyboard: true,
keepaliveInterval: 30000, keepaliveInterval: 30000,
keepaliveCountMax: 3, keepaliveCountMax: 3,
readyTimeout: 60000, readyTimeout: 60000,
tcpKeepAlive: true, tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000, tcpKeepAliveInitialDelay: 30000,
env: { env: {
TERM: "xterm-256color", TERM: "xterm-256color",
LANG: "en_US.UTF-8", LANG: "en_US.UTF-8",
@@ -579,7 +696,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
LC_COLLATE: "en_US.UTF-8", LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor", COLORTERM: "truecolor",
}, },
algorithms: { algorithms: {
kex: [ kex: [
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha256",
@@ -602,6 +718,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
"aes256-cbc", "aes256-cbc",
"3des-cbc", "3des-cbc",
], ],
serverHostKey: [
"ssh-rsa",
"rsa-sha2-256",
"rsa-sha2-512",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"ssh-ed25519",
],
hmac: [ hmac: [
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512-etm@openssh.com",
@@ -640,13 +765,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (resolvedCredentials.keyPassword) { if (resolvedCredentials.keyPassword) {
connectConfig.passphrase = resolvedCredentials.keyPassword; connectConfig.passphrase = resolvedCredentials.keyPassword;
} }
if (
resolvedCredentials.keyType &&
resolvedCredentials.keyType !== "auto"
) {
connectConfig.privateKeyType = resolvedCredentials.keyType;
}
} catch (keyError) { } catch (keyError) {
sshLogger.error("SSH key format error: " + keyError.message); sshLogger.error("SSH key format error: " + keyError.message);
ws.send( ws.send(
@@ -680,7 +798,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.connect(connectConfig); sshConn.connect(connectConfig);
} }
function handleResize(data: { cols: number; rows: number }) { function handleResize(data: ResizeData) {
if (sshStream && sshStream.setWindow) { if (sshStream && sshStream.setWindow) {
sshStream.setWindow(data.rows, data.cols, data.rows, data.cols); sshStream.setWindow(data.rows, data.cols, data.rows, data.cols);
ws.send( ws.send(
@@ -702,8 +820,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshStream) { if (sshStream) {
try { try {
sshStream.end(); sshStream.end();
} catch (e: any) { } catch (e: unknown) {
sshLogger.error("Error closing stream: " + e.message); sshLogger.error(
"Error closing stream: " +
(e instanceof Error ? e.message : "Unknown error"),
);
} }
sshStream = null; sshStream = null;
} }
@@ -711,8 +832,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshConn) { if (sshConn) {
try { try {
sshConn.end(); sshConn.end();
} catch (e: any) { } catch (e: unknown) {
sshLogger.error("Error closing connection: " + e.message); sshLogger.error(
"Error closing connection: " +
(e instanceof Error ? e.message : "Unknown error"),
);
} }
sshConn = null; sshConn = null;
} }
@@ -723,8 +847,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshConn && sshStream) { if (sshConn && sshStream) {
try { try {
sshStream.write("\x00"); sshStream.write("\x00");
} catch (e: any) { } catch (e: unknown) {
sshLogger.error("SSH keepalive failed: " + e.message); sshLogger.error(
"SSH keepalive failed: " +
(e instanceof Error ? e.message : "Unknown error"),
);
cleanupSSH(); cleanupSSH();
} }
} }

View File

@@ -217,7 +217,9 @@ function cleanupTunnelResources(
if (verification?.timeout) clearTimeout(verification.timeout); if (verification?.timeout) clearTimeout(verification.timeout);
try { try {
verification?.conn.end(); verification?.conn.end();
} catch (e) {} } catch {
// Ignore errors
}
tunnelVerifications.delete(tunnelName); tunnelVerifications.delete(tunnelName);
} }
@@ -282,7 +284,9 @@ function handleDisconnect(
const verification = tunnelVerifications.get(tunnelName); const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout); if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end(); verification?.conn.end();
} catch (e) {} } catch {
// Ignore errors
}
tunnelVerifications.delete(tunnelName); tunnelVerifications.delete(tunnelName);
} }
@@ -511,16 +515,19 @@ async function connectSSHTunnel(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} else {
} }
} else {
} }
} catch (error) { } catch (error) {
tunnelLogger.warn("Failed to resolve source credentials from database", { tunnelLogger.warn("Failed to resolve source credentials from database", {
@@ -591,12 +598,17 @@ async function connectSSHTunnel(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedEndpointCredentials = { resolvedEndpointCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} else { } else {
tunnelLogger.warn("No endpoint credentials found in database", { tunnelLogger.warn("No endpoint credentials found in database", {
@@ -605,7 +617,6 @@ async function connectSSHTunnel(
credentialId: tunnelConfig.endpointCredentialId, credentialId: tunnelConfig.endpointCredentialId,
}); });
} }
} else {
} }
} catch (error) { } catch (error) {
tunnelLogger.warn( tunnelLogger.warn(
@@ -631,7 +642,9 @@ async function connectSSHTunnel(
try { try {
conn.end(); conn.end();
} catch (e) {} } catch {
// Ignore errors
}
activeTunnels.delete(tunnelName); activeTunnels.delete(tunnelName);
@@ -771,7 +784,9 @@ async function connectSSHTunnel(
const verification = tunnelVerifications.get(tunnelName); const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout); if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end(); verification?.conn.end();
} catch (e) {} } catch {
// Ignore errors
}
tunnelVerifications.delete(tunnelName); tunnelVerifications.delete(tunnelName);
} }
@@ -822,13 +837,13 @@ async function connectSSHTunnel(
} }
}); });
stream.stdout?.on("data", (data: Buffer) => { stream.stdout?.on("data", () => {
const output = data.toString().trim(); // Silently consume stdout data
if (output) {
}
}); });
stream.on("error", (err: Error) => {}); stream.on("error", () => {
// Silently consume stream errors
});
stream.stderr.on("data", (data) => { stream.stderr.on("data", (data) => {
const errorMsg = data.toString().trim(); const errorMsg = data.toString().trim();
@@ -888,7 +903,7 @@ async function connectSSHTunnel(
}); });
}); });
const connOptions: any = { const connOptions: Record<string, unknown> = {
host: tunnelConfig.sourceIP, host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort, port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername, username: tunnelConfig.sourceUsername,
@@ -1026,15 +1041,19 @@ async function killRemoteTunnelByMarker(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} }
} else {
} }
} catch (error) { } catch (error) {
tunnelLogger.warn("Failed to resolve source credentials for cleanup", { tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
@@ -1046,7 +1065,7 @@ async function killRemoteTunnelByMarker(
} }
const conn = new Client(); const conn = new Client();
const connOptions: any = { const connOptions: Record<string, unknown> = {
host: tunnelConfig.sourceIP, host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort, port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername, username: tunnelConfig.sourceUsername,
@@ -1122,7 +1141,7 @@ async function killRemoteTunnelByMarker(
conn.on("ready", () => { conn.on("ready", () => {
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*-R.*${tunnelConfig.endpointPort})' | grep -v grep`; const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*-R.*${tunnelConfig.endpointPort})' | grep -v grep`;
conn.exec(checkCmd, (err, stream) => { conn.exec(checkCmd, (_err, stream) => {
let foundProcesses = false; let foundProcesses = false;
stream.on("data", (data) => { stream.on("data", (data) => {
@@ -1150,7 +1169,7 @@ async function killRemoteTunnelByMarker(
function executeNextKillCommand() { function executeNextKillCommand() {
if (commandIndex >= killCmds.length) { if (commandIndex >= killCmds.length) {
conn.exec(checkCmd, (err, verifyStream) => { conn.exec(checkCmd, (_err, verifyStream) => {
let stillRunning = false; let stillRunning = false;
verifyStream.on("data", (data) => { verifyStream.on("data", (data) => {
@@ -1183,18 +1202,15 @@ async function killRemoteTunnelByMarker(
tunnelLogger.warn( tunnelLogger.warn(
`Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`, `Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`,
); );
} else {
} }
stream.on("close", (code) => { stream.on("close", () => {
commandIndex++; commandIndex++;
executeNextKillCommand(); executeNextKillCommand();
}); });
stream.on("data", (data) => { stream.on("data", () => {
const output = data.toString().trim(); // Silently consume stream data
if (output) {
}
}); });
stream.stderr.on("data", (data) => { stream.stderr.on("data", (data) => {
@@ -1423,14 +1439,6 @@ async function initializeAutoStartTunnels(): Promise<void> {
isPinned: host.pin, isPinned: host.pin,
}; };
const hasSourcePassword = host.autostartPassword;
const hasSourceKey = host.autostartKey;
const hasEndpointPassword =
tunnelConnection.endpointPassword ||
endpointHost.autostartPassword;
const hasEndpointKey =
tunnelConnection.endpointKey || endpointHost.autostartKey;
autoStartTunnels.push(tunnelConfig); autoStartTunnels.push(tunnelConfig);
} else { } else {
tunnelLogger.error( tunnelLogger.error(
@@ -1453,10 +1461,10 @@ async function initializeAutoStartTunnels(): Promise<void> {
}); });
}, 1000); }, 1000);
} }
} catch (error: any) { } catch (error) {
tunnelLogger.error( tunnelLogger.error(
"Failed to initialize auto-start tunnels:", "Failed to initialize auto-start tunnels:",
error.message, error instanceof Error ? error.message : "Unknown error",
); );
} }
} }

View File

@@ -21,7 +21,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
if (persistentConfig.parsed) { if (persistentConfig.parsed) {
Object.assign(process.env, persistentConfig.parsed); Object.assign(process.env, persistentConfig.parsed);
} }
} catch {} } catch {
// Ignore errors if .env file doesn't exist
}
let version = "unknown"; let version = "unknown";
@@ -73,7 +75,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
version = foundVersion; version = foundVersion;
break; break;
} }
} catch (error) { } catch {
continue; continue;
} }
} }
@@ -126,7 +128,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
process.exit(1); process.exit(1);
}); });
process.on("unhandledRejection", (reason, promise) => { process.on("unhandledRejection", (reason) => {
systemLogger.error("Unhandled promise rejection", reason, { systemLogger.error("Unhandled promise rejection", reason, {
operation: "error_handling", operation: "error_handling",
}); });

View File

@@ -23,6 +23,18 @@ interface JWTPayload {
exp?: number; exp?: number;
} }
interface AuthenticatedRequest extends Request {
userId?: string;
pendingTOTP?: boolean;
dataKey?: Buffer;
}
interface RequestWithHeaders extends Request {
headers: Request["headers"] & {
"x-forwarded-proto"?: string;
};
}
class AuthManager { class AuthManager {
private static instance: AuthManager; private static instance: AuthManager;
private systemCrypto: SystemCrypto; private systemCrypto: SystemCrypto;
@@ -108,7 +120,6 @@ class AuthManager {
if (migrationResult.migrated) { if (migrationResult.migrated) {
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} else {
} }
} catch (error) { } catch (error) {
databaseLogger.error("Lazy encryption migration failed", error, { databaseLogger.error("Lazy encryption migration failed", error, {
@@ -164,7 +175,10 @@ class AuthManager {
}); });
} }
getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) { getSecureCookieOptions(
req: RequestWithHeaders,
maxAge: number = 24 * 60 * 60 * 1000,
) {
return { return {
httpOnly: false, httpOnly: false,
secure: req.secure || req.headers["x-forwarded-proto"] === "https", secure: req.secure || req.headers["x-forwarded-proto"] === "https",
@@ -176,10 +190,11 @@ class AuthManager {
createAuthMiddleware() { createAuthMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
let token = req.cookies?.jwt; const authReq = req as AuthenticatedRequest;
let token = authReq.cookies?.jwt;
if (!token) { if (!token) {
const authHeader = req.headers["authorization"]; const authHeader = authReq.headers["authorization"];
if (authHeader?.startsWith("Bearer ")) { if (authHeader?.startsWith("Bearer ")) {
token = authHeader.split(" ")[1]; token = authHeader.split(" ")[1];
} }
@@ -195,15 +210,16 @@ class AuthManager {
return res.status(401).json({ error: "Invalid token" }); return res.status(401).json({ error: "Invalid token" });
} }
(req as any).userId = payload.userId; authReq.userId = payload.userId;
(req as any).pendingTOTP = payload.pendingTOTP; authReq.pendingTOTP = payload.pendingTOTP;
next(); next();
}; };
} }
createDataAccessMiddleware() { createDataAccessMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const userId = (req as any).userId; const authReq = req as AuthenticatedRequest;
const userId = authReq.userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
} }
@@ -216,7 +232,7 @@ class AuthManager {
}); });
} }
(req as any).dataKey = dataKey; authReq.dataKey = dataKey;
next(); next();
}; };
} }
@@ -257,8 +273,9 @@ class AuthManager {
return res.status(403).json({ error: "Admin access required" }); return res.status(403).json({ error: "Admin access required" });
} }
(req as any).userId = payload.userId; const authReq = req as AuthenticatedRequest;
(req as any).pendingTOTP = payload.pendingTOTP; authReq.userId = payload.userId;
authReq.pendingTOTP = payload.pendingTOTP;
next(); next();
} catch (error) { } catch (error) {
databaseLogger.error("Failed to verify admin privileges", error, { databaseLogger.error("Failed to verify admin privileges", error, {

View File

@@ -1,7 +1,6 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import path from "path"; import path from "path";
import crypto from "crypto";
import { systemLogger } from "./logger.js"; import { systemLogger } from "./logger.js";
export class AutoSSLSetup { export class AutoSSLSetup {
@@ -102,7 +101,7 @@ export class AutoSSLSetup {
try { try {
try { try {
execSync("openssl version", { stdio: "pipe" }); execSync("openssl version", { stdio: "pipe" });
} catch (error) { } catch {
throw new Error( throw new Error(
"OpenSSL is not installed or not available in PATH. Please install OpenSSL to enable SSL certificate generation.", "OpenSSL is not installed or not available in PATH. Please install OpenSSL to enable SSL certificate generation.",
); );
@@ -234,7 +233,9 @@ IP.3 = 0.0.0.0
let envContent = ""; let envContent = "";
try { try {
envContent = await fs.readFile(this.ENV_FILE, "utf8"); envContent = await fs.readFile(this.ENV_FILE, "utf8");
} catch {} } catch {
// File doesn't exist yet, will create with SSL config
}
let updatedContent = envContent; let updatedContent = envContent;
let hasChanges = false; let hasChanges = false;

View File

@@ -3,6 +3,19 @@ import { LazyFieldEncryption } from "./lazy-field-encryption.js";
import { UserCrypto } from "./user-crypto.js"; import { UserCrypto } from "./user-crypto.js";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
interface DatabaseInstance {
prepare: (sql: string) => {
all: (param?: unknown) => DatabaseRecord[];
get: (param?: unknown) => DatabaseRecord;
run: (...params: unknown[]) => unknown;
};
}
interface DatabaseRecord {
id: number | string;
[key: string]: unknown;
}
class DataCrypto { class DataCrypto {
private static userCrypto: UserCrypto; private static userCrypto: UserCrypto;
@@ -10,13 +23,13 @@ class DataCrypto {
this.userCrypto = UserCrypto.getInstance(); this.userCrypto = UserCrypto.getInstance();
} }
static encryptRecord( static encryptRecord<T extends Record<string, unknown>>(
tableName: string, tableName: string,
record: any, record: T,
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any { ): T {
const encryptedRecord = { ...record }; const encryptedRecord: Record<string, unknown> = { ...record };
const recordId = record.id || "temp-" + Date.now(); const recordId = record.id || "temp-" + Date.now();
for (const [fieldName, value] of Object.entries(record)) { for (const [fieldName, value] of Object.entries(record)) {
@@ -24,24 +37,24 @@ class DataCrypto {
encryptedRecord[fieldName] = FieldCrypto.encryptField( encryptedRecord[fieldName] = FieldCrypto.encryptField(
value as string, value as string,
userDataKey, userDataKey,
recordId, recordId as string,
fieldName, fieldName,
); );
} }
} }
return encryptedRecord; return encryptedRecord as T;
} }
static decryptRecord( static decryptRecord<T extends Record<string, unknown>>(
tableName: string, tableName: string,
record: any, record: T,
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any { ): T {
if (!record) return record; if (!record) return record;
const decryptedRecord = { ...record }; const decryptedRecord: Record<string, unknown> = { ...record };
const recordId = record.id; const recordId = record.id;
for (const [fieldName, value] of Object.entries(record)) { for (const [fieldName, value] of Object.entries(record)) {
@@ -49,21 +62,21 @@ class DataCrypto {
decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue( decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue(
value as string, value as string,
userDataKey, userDataKey,
recordId, recordId as string,
fieldName, fieldName,
); );
} }
} }
return decryptedRecord; return decryptedRecord as T;
} }
static decryptRecords( static decryptRecords<T extends Record<string, unknown>>(
tableName: string, tableName: string,
records: any[], records: T[],
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any[] { ): T[] {
if (!Array.isArray(records)) return records; if (!Array.isArray(records)) return records;
return records.map((record) => return records.map((record) =>
this.decryptRecord(tableName, record, userId, userDataKey), this.decryptRecord(tableName, record, userId, userDataKey),
@@ -73,7 +86,7 @@ class DataCrypto {
static async migrateUserSensitiveFields( static async migrateUserSensitiveFields(
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
db: any, db: DatabaseInstance,
): Promise<{ ): Promise<{
migrated: boolean; migrated: boolean;
migratedTables: string[]; migratedTables: string[];
@@ -84,7 +97,7 @@ class DataCrypto {
let migratedFieldsCount = 0; let migratedFieldsCount = 0;
try { try {
const { needsMigration, plaintextFields } = const { needsMigration } =
await LazyFieldEncryption.checkUserNeedsMigration( await LazyFieldEncryption.checkUserNeedsMigration(
userId, userId,
userDataKey, userDataKey,
@@ -97,7 +110,7 @@ class DataCrypto {
const sshDataRecords = db const sshDataRecords = db
.prepare("SELECT * FROM ssh_data WHERE user_id = ?") .prepare("SELECT * FROM ssh_data WHERE user_id = ?")
.all(userId); .all(userId) as DatabaseRecord[];
for (const record of sshDataRecords) { for (const record of sshDataRecords) {
const sensitiveFields = const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data"); LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data");
@@ -132,7 +145,7 @@ class DataCrypto {
const sshCredentialsRecords = db const sshCredentialsRecords = db
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?") .prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
.all(userId); .all(userId) as DatabaseRecord[];
for (const record of sshCredentialsRecords) { for (const record of sshCredentialsRecords) {
const sensitiveFields = const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials"); LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials");
@@ -169,7 +182,7 @@ class DataCrypto {
const userRecord = db const userRecord = db
.prepare("SELECT * FROM users WHERE id = ?") .prepare("SELECT * FROM users WHERE id = ?")
.get(userId); .get(userId) as DatabaseRecord | undefined;
if (userRecord) { if (userRecord) {
const sensitiveFields = const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("users"); LazyFieldEncryption.getSensitiveFieldsForTable("users");
@@ -220,7 +233,7 @@ class DataCrypto {
static async reencryptUserDataAfterPasswordReset( static async reencryptUserDataAfterPasswordReset(
userId: string, userId: string,
newUserDataKey: Buffer, newUserDataKey: Buffer,
db: any, db: DatabaseInstance,
): Promise<{ ): Promise<{
success: boolean; success: boolean;
reencryptedTables: string[]; reencryptedTables: string[];
@@ -262,17 +275,21 @@ class DataCrypto {
try { try {
const records = db const records = db
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`) .prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
.all(userId); .all(userId) as DatabaseRecord[];
for (const record of records) { for (const record of records) {
const recordId = record.id.toString(); const recordId = record.id.toString();
const updatedRecord: DatabaseRecord = { ...record };
let needsUpdate = false; let needsUpdate = false;
const updatedRecord = { ...record };
for (const fieldName of fields) { for (const fieldName of fields) {
const fieldValue = record[fieldName]; const fieldValue = record[fieldName];
if (fieldValue && fieldValue.trim() !== "") { if (
fieldValue &&
typeof fieldValue === "string" &&
fieldValue.trim() !== ""
) {
try { try {
const reencryptedValue = FieldCrypto.encryptField( const reencryptedValue = FieldCrypto.encryptField(
fieldValue, fieldValue,
@@ -384,29 +401,29 @@ class DataCrypto {
return userDataKey; return userDataKey;
} }
static encryptRecordForUser( static encryptRecordForUser<T extends Record<string, unknown>>(
tableName: string, tableName: string,
record: any, record: T,
userId: string, userId: string,
): any { ): T {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
return this.encryptRecord(tableName, record, userId, userDataKey); return this.encryptRecord(tableName, record, userId, userDataKey);
} }
static decryptRecordForUser( static decryptRecordForUser<T extends Record<string, unknown>>(
tableName: string, tableName: string,
record: any, record: T,
userId: string, userId: string,
): any { ): T {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
return this.decryptRecord(tableName, record, userId, userDataKey); return this.decryptRecord(tableName, record, userId, userDataKey);
} }
static decryptRecordsForUser( static decryptRecordsForUser<T extends Record<string, unknown>>(
tableName: string, tableName: string,
records: any[], records: T[],
userId: string, userId: string,
): any[] { ): T[] {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
return this.decryptRecords(tableName, records, userId, userDataKey); return this.decryptRecords(tableName, records, userId, userDataKey);
} }
@@ -435,7 +452,7 @@ class DataCrypto {
); );
return decrypted === testData; return decrypted === testData;
} catch (error) { } catch {
return false; return false;
} }
} }

View File

@@ -30,7 +30,11 @@ class DatabaseFileEncryption {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any; const cipher = crypto.createCipheriv(
this.ALGORITHM,
key,
iv,
) as crypto.CipherGCM;
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]); const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
const tag = cipher.getAuthTag(); const tag = cipher.getAuthTag();
@@ -78,7 +82,11 @@ class DatabaseFileEncryption {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any; const cipher = crypto.createCipheriv(
this.ALGORITHM,
key,
iv,
) as crypto.CipherGCM;
const encrypted = Buffer.concat([ const encrypted = Buffer.concat([
cipher.update(sourceData), cipher.update(sourceData),
cipher.final(), cipher.final(),
@@ -163,7 +171,7 @@ class DatabaseFileEncryption {
metadata.algorithm, metadata.algorithm,
key, key,
Buffer.from(metadata.iv, "hex"), Buffer.from(metadata.iv, "hex"),
) as any; ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(metadata.tag, "hex")); decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
const decryptedBuffer = Buffer.concat([ const decryptedBuffer = Buffer.concat([
@@ -233,7 +241,7 @@ class DatabaseFileEncryption {
metadata.algorithm, metadata.algorithm,
key, key,
Buffer.from(metadata.iv, "hex"), Buffer.from(metadata.iv, "hex"),
) as any; ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(metadata.tag, "hex")); decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
const decrypted = Buffer.concat([ const decrypted = Buffer.concat([
@@ -301,7 +309,6 @@ class DatabaseFileEncryption {
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent); const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
const fileStats = fs.statSync(encryptedPath); const fileStats = fs.statSync(encryptedPath);
const currentFingerprint = "termix-v1-file";
return { return {
version: metadata.version, version: metadata.version,

View File

@@ -55,7 +55,6 @@ export class DatabaseMigration {
if (hasEncryptedDb && hasUnencryptedDb) { if (hasEncryptedDb && hasUnencryptedDb) {
const unencryptedSize = fs.statSync(this.unencryptedDbPath).size; const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;
const encryptedSize = fs.statSync(this.encryptedDbPath).size;
if (unencryptedSize === 0) { if (unencryptedSize === 0) {
needsMigration = false; needsMigration = false;
@@ -168,9 +167,6 @@ export class DatabaseMigration {
return false; return false;
} }
let totalOriginalRows = 0;
let totalMemoryRows = 0;
for (const table of originalTables) { for (const table of originalTables) {
const originalCount = originalDb const originalCount = originalDb
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`) .prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
@@ -179,9 +175,6 @@ export class DatabaseMigration {
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`) .prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
.get() as { count: number }; .get() as { count: number };
totalOriginalRows += originalCount.count;
totalMemoryRows += memoryCount.count;
if (originalCount.count !== memoryCount.count) { if (originalCount.count !== memoryCount.count) {
databaseLogger.error( databaseLogger.error(
"Row count mismatch for table during migration verification", "Row count mismatch for table during migration verification",
@@ -241,7 +234,9 @@ export class DatabaseMigration {
memoryDb.exec("PRAGMA foreign_keys = OFF"); memoryDb.exec("PRAGMA foreign_keys = OFF");
for (const table of tables) { for (const table of tables) {
const rows = originalDb.prepare(`SELECT * FROM ${table.name}`).all(); const rows = originalDb
.prepare(`SELECT * FROM ${table.name}`)
.all() as Record<string, unknown>[];
if (rows.length > 0) { if (rows.length > 0) {
const columns = Object.keys(rows[0]); const columns = Object.keys(rows[0]);
@@ -251,7 +246,7 @@ export class DatabaseMigration {
); );
const insertTransaction = memoryDb.transaction( const insertTransaction = memoryDb.transaction(
(dataRows: any[]) => { (dataRows: Record<string, unknown>[]) => {
for (const row of dataRows) { for (const row of dataRows) {
const values = columns.map((col) => row[col]); const values = columns.map((col) => row[col]);
insertStmt.run(values); insertStmt.run(values);

View File

@@ -21,8 +21,9 @@ class FieldCrypto {
"totp_secret", "totp_secret",
"totp_backup_codes", "totp_backup_codes",
"oidc_identifier", "oidc_identifier",
"oidcIdentifier",
]), ]),
ssh_data: new Set(["password", "key", "key_password"]), ssh_data: new Set(["password", "key", "key_password", "keyPassword"]),
ssh_credentials: new Set([ ssh_credentials: new Set([
"password", "password",
"private_key", "private_key",
@@ -47,7 +48,11 @@ class FieldCrypto {
); );
const iv = crypto.randomBytes(this.IV_LENGTH); const iv = crypto.randomBytes(this.IV_LENGTH);
const cipher = crypto.createCipheriv(this.ALGORITHM, fieldKey, iv) as any; const cipher = crypto.createCipheriv(
this.ALGORITHM,
fieldKey,
iv,
) as crypto.CipherGCM;
let encrypted = cipher.update(plaintext, "utf8", "hex"); let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex"); encrypted += cipher.final("hex");
@@ -89,7 +94,7 @@ class FieldCrypto {
this.ALGORITHM, this.ALGORITHM,
fieldKey, fieldKey,
Buffer.from(encrypted.iv, "hex"), Buffer.from(encrypted.iv, "hex"),
) as any; ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(encrypted.tag, "hex")); decipher.setAuthTag(Buffer.from(encrypted.tag, "hex"));
let decrypted = decipher.update(encrypted.data, "hex", "utf8"); let decrypted = decipher.update(encrypted.data, "hex", "utf8");

View File

@@ -1,6 +1,14 @@
import { FieldCrypto } from "./field-crypto.js"; import { FieldCrypto } from "./field-crypto.js";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
interface DatabaseInstance {
prepare: (sql: string) => {
all: (param?: unknown) => unknown[];
get: (param?: unknown) => unknown;
run: (...params: unknown[]) => unknown;
};
}
export class LazyFieldEncryption { export class LazyFieldEncryption {
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = { private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
key_password: "keyPassword", key_password: "keyPassword",
@@ -39,7 +47,7 @@ export class LazyFieldEncryption {
return false; return false;
} }
return true; return true;
} catch (jsonError) { } catch {
return true; return true;
} }
} }
@@ -74,7 +82,9 @@ export class LazyFieldEncryption {
legacyFieldName, legacyFieldName,
); );
return decrypted; return decrypted;
} catch (legacyError) {} } catch {
// Ignore legacy format errors
}
} }
const sensitiveFields = [ const sensitiveFields = [
@@ -145,7 +155,7 @@ export class LazyFieldEncryption {
wasPlaintext: false, wasPlaintext: false,
wasLegacyEncryption: false, wasLegacyEncryption: false,
}; };
} catch (error) { } catch {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName]; const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) { if (legacyFieldName) {
try { try {
@@ -166,7 +176,9 @@ export class LazyFieldEncryption {
wasPlaintext: false, wasPlaintext: false,
wasLegacyEncryption: true, wasLegacyEncryption: true,
}; };
} catch (legacyError) {} } catch {
// Ignore legacy format errors
}
} }
return { return {
encrypted: fieldValue, encrypted: fieldValue,
@@ -178,12 +190,12 @@ export class LazyFieldEncryption {
} }
static migrateRecordSensitiveFields( static migrateRecordSensitiveFields(
record: any, record: Record<string, unknown>,
sensitiveFields: string[], sensitiveFields: string[],
userKEK: Buffer, userKEK: Buffer,
recordId: string, recordId: string,
): { ): {
updatedRecord: any; updatedRecord: Record<string, unknown>;
migratedFields: string[]; migratedFields: string[];
needsUpdate: boolean; needsUpdate: boolean;
} { } {
@@ -198,7 +210,7 @@ export class LazyFieldEncryption {
try { try {
const { encrypted, wasPlaintext, wasLegacyEncryption } = const { encrypted, wasPlaintext, wasLegacyEncryption } =
this.migrateFieldToEncrypted( this.migrateFieldToEncrypted(
fieldValue, fieldValue as string,
userKEK, userKEK,
recordId, recordId,
fieldName, fieldName,
@@ -253,7 +265,7 @@ export class LazyFieldEncryption {
try { try {
FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName); FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName);
return false; return false;
} catch (error) { } catch {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName]; const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) { if (legacyFieldName) {
try { try {
@@ -264,7 +276,7 @@ export class LazyFieldEncryption {
legacyFieldName, legacyFieldName,
); );
return true; return true;
} catch (legacyError) { } catch {
return false; return false;
} }
} }
@@ -275,7 +287,7 @@ export class LazyFieldEncryption {
static async checkUserNeedsMigration( static async checkUserNeedsMigration(
userId: string, userId: string,
userKEK: Buffer, userKEK: Buffer,
db: any, db: DatabaseInstance,
): Promise<{ ): Promise<{
needsMigration: boolean; needsMigration: boolean;
plaintextFields: Array<{ plaintextFields: Array<{
@@ -294,7 +306,9 @@ export class LazyFieldEncryption {
try { try {
const sshHosts = db const sshHosts = db
.prepare("SELECT * FROM ssh_data WHERE user_id = ?") .prepare("SELECT * FROM ssh_data WHERE user_id = ?")
.all(userId); .all(userId) as Array<
Record<string, unknown> & { id: string | number }
>;
for (const host of sshHosts) { for (const host of sshHosts) {
const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data"); const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data");
const hostPlaintextFields: string[] = []; const hostPlaintextFields: string[] = [];
@@ -303,7 +317,7 @@ export class LazyFieldEncryption {
if ( if (
host[field] && host[field] &&
this.fieldNeedsMigration( this.fieldNeedsMigration(
host[field], host[field] as string,
userKEK, userKEK,
host.id.toString(), host.id.toString(),
field, field,
@@ -325,7 +339,9 @@ export class LazyFieldEncryption {
const sshCredentials = db const sshCredentials = db
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?") .prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
.all(userId); .all(userId) as Array<
Record<string, unknown> & { id: string | number }
>;
for (const credential of sshCredentials) { for (const credential of sshCredentials) {
const sensitiveFields = const sensitiveFields =
this.getSensitiveFieldsForTable("ssh_credentials"); this.getSensitiveFieldsForTable("ssh_credentials");
@@ -335,7 +351,7 @@ export class LazyFieldEncryption {
if ( if (
credential[field] && credential[field] &&
this.fieldNeedsMigration( this.fieldNeedsMigration(
credential[field], credential[field] as string,
userKEK, userKEK,
credential.id.toString(), credential.id.toString(),
field, field,

View File

@@ -11,7 +11,7 @@ export interface LogContext {
sessionId?: string; sessionId?: string;
requestId?: string; requestId?: string;
duration?: number; duration?: number;
[key: string]: any; [key: string]: unknown;
} }
const SENSITIVE_FIELDS = [ const SENSITIVE_FIELDS = [

View File

@@ -5,7 +5,8 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
type TableName = "users" | "ssh_data" | "ssh_credentials"; type TableName = "users" | "ssh_data" | "ssh_credentials";
class SimpleDBOps { class SimpleDBOps {
static async insert<T extends Record<string, any>>( static async insert<T extends Record<string, unknown>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
data: T, data: T,
@@ -44,8 +45,8 @@ class SimpleDBOps {
return decryptedResult as T; return decryptedResult as T;
} }
static async select<T extends Record<string, any>>( static async select<T extends Record<string, unknown>>(
query: any, query: unknown,
tableName: TableName, tableName: TableName,
userId: string, userId: string,
): Promise<T[]> { ): Promise<T[]> {
@@ -56,9 +57,9 @@ class SimpleDBOps {
const results = await query; const results = await query;
const decryptedResults = DataCrypto.decryptRecords( const decryptedResults = DataCrypto.decryptRecords<T>(
tableName, tableName,
results, results as T[],
userId, userId,
userDataKey, userDataKey,
); );
@@ -66,8 +67,8 @@ class SimpleDBOps {
return decryptedResults; return decryptedResults;
} }
static async selectOne<T extends Record<string, any>>( static async selectOne<T extends Record<string, unknown>>(
query: any, query: unknown,
tableName: TableName, tableName: TableName,
userId: string, userId: string,
): Promise<T | undefined> { ): Promise<T | undefined> {
@@ -79,9 +80,9 @@ class SimpleDBOps {
const result = await query; const result = await query;
if (!result) return undefined; if (!result) return undefined;
const decryptedResult = DataCrypto.decryptRecord( const decryptedResult = DataCrypto.decryptRecord<T>(
tableName, tableName,
result, result as T,
userId, userId,
userDataKey, userDataKey,
); );
@@ -89,10 +90,11 @@ class SimpleDBOps {
return decryptedResult; return decryptedResult;
} }
static async update<T extends Record<string, any>>( static async update<T extends Record<string, unknown>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
where: any, where: unknown,
data: Partial<T>, data: Partial<T>,
userId: string, userId: string,
): Promise<T[]> { ): Promise<T[]> {
@@ -108,7 +110,8 @@ class SimpleDBOps {
const result = await getDb() const result = await getDb()
.update(table) .update(table)
.set(encryptedData) .set(encryptedData)
.where(where) // eslint-disable-next-line @typescript-eslint/no-explicit-any
.where(where as any)
.returning(); .returning();
DatabaseSaveTrigger.triggerSave(`update_${tableName}`); DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
@@ -124,12 +127,16 @@ class SimpleDBOps {
} }
static async delete( static async delete(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
where: any, where: unknown,
userId: string, ): Promise<unknown[]> {
): Promise<any[]> { const result = await getDb()
const result = await getDb().delete(table).where(where).returning(); .delete(table)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.where(where as any)
.returning();
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`); DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
@@ -144,13 +151,10 @@ class SimpleDBOps {
return DataCrypto.getUserDataKey(userId) !== null; return DataCrypto.getUserDataKey(userId) !== null;
} }
static async selectEncrypted( static async selectEncrypted(query: unknown): Promise<unknown[]> {
query: any,
tableName: TableName,
): Promise<any[]> {
const results = await query; const results = await query;
return results; return results as unknown[];
} }
} }

View File

@@ -49,7 +49,7 @@ function detectKeyTypeFromContent(keyContent: string): string {
} }
return "ssh-rsa"; return "ssh-rsa";
} catch (error) { } catch {
return "ssh-rsa"; return "ssh-rsa";
} }
} }
@@ -84,7 +84,9 @@ function detectKeyTypeFromContent(keyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) { } else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519"; return "ssh-ed25519";
} }
} catch (error) {} } catch {
// Cannot decode key, fallback to length-based detection
}
if (content.length < 800) { if (content.length < 800) {
return "ssh-ed25519"; return "ssh-ed25519";
@@ -140,7 +142,9 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) { } else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519"; return "ssh-ed25519";
} }
} catch (error) {} } catch {
// Cannot decode key, fallback to length-based detection
}
if (content.length < 400) { if (content.length < 400) {
return "ssh-ed25519"; return "ssh-ed25519";
@@ -236,13 +240,15 @@ export function parseSSHKey(
} else { } else {
publicKey = ""; publicKey = "";
} }
} catch (error) { } catch {
publicKey = ""; publicKey = "";
} }
useSSH2 = true; useSSH2 = true;
} }
} catch (error) {} } catch {
// SSH2 parsing failed, will use fallback method
}
} }
if (!useSSH2) { if (!useSSH2) {
@@ -268,7 +274,9 @@ export function parseSSHKey(
success: true, success: true,
}; };
} }
} catch (fallbackError) {} } catch {
// Fallback parsing also failed
}
return { return {
privateKey: privateKeyData, privateKey: privateKeyData,
@@ -310,7 +318,7 @@ export function detectKeyType(privateKeyData: string): string {
return "unknown"; return "unknown";
} }
return parsedKey.type || "unknown"; return parsedKey.type || "unknown";
} catch (error) { } catch {
return "unknown"; return "unknown";
} }
} }

View File

@@ -37,7 +37,9 @@ class SystemCrypto {
process.env.JWT_SECRET = jwtMatch[1]; process.env.JWT_SECRET = jwtMatch[1];
return; return;
} }
} catch {} } catch {
// Ignore file read errors, will generate new secret
}
await this.generateAndGuideUser(); await this.generateAndGuideUser();
} catch (error) { } catch (error) {
@@ -74,7 +76,9 @@ class SystemCrypto {
process.env.DATABASE_KEY = dbKeyMatch[1]; process.env.DATABASE_KEY = dbKeyMatch[1];
return; return;
} }
} catch {} } catch {
// Ignore file read errors, will generate new key
}
await this.generateAndGuideDatabaseKey(); await this.generateAndGuideDatabaseKey();
} catch (error) { } catch (error) {
@@ -111,7 +115,9 @@ class SystemCrypto {
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1]; process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
return; return;
} }
} catch {} } catch {
// Ignore file read errors, will generate new token
}
await this.generateAndGuideInternalAuthToken(); await this.generateAndGuideInternalAuthToken();
} catch (error) { } catch (error) {

View File

@@ -163,9 +163,10 @@ class UserCrypto {
async authenticateOIDCUser(userId: string): Promise<boolean> { async authenticateOIDCUser(userId: string): Promise<boolean> {
try { try {
const kekSalt = await this.getKEKSalt(userId);
const encryptedDEK = await this.getEncryptedDEK(userId); const encryptedDEK = await this.getEncryptedDEK(userId);
if (!encryptedDEK) { if (!kekSalt || !encryptedDEK) {
await this.setupOIDCUserEncryption(userId); await this.setupOIDCUserEncryption(userId);
return true; return true;
} }
@@ -195,7 +196,7 @@ class UserCrypto {
DEK.fill(0); DEK.fill(0);
return true; return true;
} catch (error) { } catch {
await this.setupOIDCUserEncryption(userId); await this.setupOIDCUserEncryption(userId);
return true; return true;
} }
@@ -276,21 +277,6 @@ class UserCrypto {
oldKEK.fill(0); oldKEK.fill(0);
newKEK.fill(0); newKEK.fill(0);
const dekCopy = Buffer.from(DEK);
const now = Date.now();
const oldSession = this.userSessions.get(userId);
if (oldSession) {
oldSession.dataKey.fill(0);
}
this.userSessions.set(userId, {
dataKey: dekCopy,
lastActivity: now,
expiresAt: now + UserCrypto.SESSION_DURATION,
});
DEK.fill(0); DEK.fill(0);
return true; return true;
@@ -363,7 +349,7 @@ class UserCrypto {
DEK.fill(0); DEK.fill(0);
return true; return true;
} catch (error) { } catch {
return false; return false;
} }
} }
@@ -482,7 +468,7 @@ class UserCrypto {
} }
return JSON.parse(result[0].value); return JSON.parse(result[0].value);
} catch (error) { } catch {
return null; return null;
} }
} }
@@ -522,7 +508,7 @@ class UserCrypto {
} }
return JSON.parse(result[0].value); return JSON.parse(result[0].value);
} catch (error) { } catch {
return null; return null;
} }
} }

View File

@@ -18,14 +18,14 @@ interface UserExportData {
userId: string; userId: string;
username: string; username: string;
userData: { userData: {
sshHosts: any[]; sshHosts: unknown[];
sshCredentials: any[]; sshCredentials: unknown[];
fileManagerData: { fileManagerData: {
recent: any[]; recent: unknown[];
pinned: any[]; pinned: unknown[];
shortcuts: any[]; shortcuts: unknown[];
}; };
dismissedAlerts: any[]; dismissedAlerts: unknown[];
}; };
metadata: { metadata: {
totalRecords: number; totalRecords: number;
@@ -83,7 +83,7 @@ class UserDataExport {
) )
: sshHosts; : sshHosts;
let sshCredentialsData: any[] = []; let sshCredentialsData: unknown[] = [];
if (includeCredentials) { if (includeCredentials) {
const credentials = await getDb() const credentials = await getDb()
.select() .select()
@@ -185,7 +185,10 @@ class UserDataExport {
return JSON.stringify(exportData, null, pretty ? 2 : 0); return JSON.stringify(exportData, null, pretty ? 2 : 0);
} }
static validateExportData(data: any): { valid: boolean; errors: string[] } { static validateExportData(data: unknown): {
valid: boolean;
errors: string[];
} {
const errors: string[] = []; const errors: string[] = [];
if (!data || typeof data !== "object") { if (!data || typeof data !== "object") {
@@ -193,23 +196,26 @@ class UserDataExport {
return { valid: false, errors }; return { valid: false, errors };
} }
if (!data.version) { const dataObj = data as Record<string, unknown>;
if (!dataObj.version) {
errors.push("Missing version field"); errors.push("Missing version field");
} }
if (!data.userId) { if (!dataObj.userId) {
errors.push("Missing userId field"); errors.push("Missing userId field");
} }
if (!data.userData || typeof data.userData !== "object") { if (!dataObj.userData || typeof dataObj.userData !== "object") {
errors.push("Missing or invalid userData field"); errors.push("Missing or invalid userData field");
} }
if (!data.metadata || typeof data.metadata !== "object") { if (!dataObj.metadata || typeof dataObj.metadata !== "object") {
errors.push("Missing or invalid metadata field"); errors.push("Missing or invalid metadata field");
} }
if (data.userData) { if (dataObj.userData) {
const userData = dataObj.userData as Record<string, unknown>;
const requiredFields = [ const requiredFields = [
"sshHosts", "sshHosts",
"sshCredentials", "sshCredentials",
@@ -218,23 +224,24 @@ class UserDataExport {
]; ];
for (const field of requiredFields) { for (const field of requiredFields) {
if ( if (
!Array.isArray(data.userData[field]) && !Array.isArray(userData[field]) &&
!( !(field === "fileManagerData" && typeof userData[field] === "object")
field === "fileManagerData" &&
typeof data.userData[field] === "object"
)
) { ) {
errors.push(`Missing or invalid userData.${field} field`); errors.push(`Missing or invalid userData.${field} field`);
} }
} }
if ( if (
data.userData.fileManagerData && userData.fileManagerData &&
typeof data.userData.fileManagerData === "object" typeof userData.fileManagerData === "object"
) { ) {
const fileManagerData = userData.fileManagerData as Record<
string,
unknown
>;
const fmFields = ["recent", "pinned", "shortcuts"]; const fmFields = ["recent", "pinned", "shortcuts"];
for (const field of fmFields) { for (const field of fmFields) {
if (!Array.isArray(data.userData.fileManagerData[field])) { if (!Array.isArray(fileManagerData[field])) {
errors.push( errors.push(
`Missing or invalid userData.fileManagerData.${field} field`, `Missing or invalid userData.fileManagerData.${field} field`,
); );

View File

@@ -12,7 +12,6 @@ import { eq, and } from "drizzle-orm";
import { DataCrypto } from "./data-crypto.js"; import { DataCrypto } from "./data-crypto.js";
import { UserDataExport, type UserExportData } from "./user-data-export.js"; import { UserDataExport, type UserExportData } from "./user-data-export.js";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
import { nanoid } from "nanoid";
interface ImportOptions { interface ImportOptions {
replaceExisting?: boolean; replaceExisting?: boolean;
@@ -90,7 +89,7 @@ class UserDataImport {
) { ) {
const importStats = await this.importSshHosts( const importStats = await this.importSshHosts(
targetUserId, targetUserId,
exportData.userData.sshHosts, exportData.userData.sshHosts as Record<string, unknown>[],
{ replaceExisting, dryRun, userDataKey }, { replaceExisting, dryRun, userDataKey },
); );
result.summary.sshHostsImported = importStats.imported; result.summary.sshHostsImported = importStats.imported;
@@ -105,7 +104,7 @@ class UserDataImport {
) { ) {
const importStats = await this.importSshCredentials( const importStats = await this.importSshCredentials(
targetUserId, targetUserId,
exportData.userData.sshCredentials, exportData.userData.sshCredentials as Record<string, unknown>[],
{ replaceExisting, dryRun, userDataKey }, { replaceExisting, dryRun, userDataKey },
); );
result.summary.sshCredentialsImported = importStats.imported; result.summary.sshCredentialsImported = importStats.imported;
@@ -130,7 +129,7 @@ class UserDataImport {
) { ) {
const importStats = await this.importDismissedAlerts( const importStats = await this.importDismissedAlerts(
targetUserId, targetUserId,
exportData.userData.dismissedAlerts, exportData.userData.dismissedAlerts as Record<string, unknown>[],
{ replaceExisting, dryRun }, { replaceExisting, dryRun },
); );
result.summary.dismissedAlertsImported = importStats.imported; result.summary.dismissedAlertsImported = importStats.imported;
@@ -160,7 +159,7 @@ class UserDataImport {
private static async importSshHosts( private static async importSshHosts(
targetUserId: string, targetUserId: string,
sshHosts: any[], sshHosts: Record<string, unknown>[],
options: { options: {
replaceExisting: boolean; replaceExisting: boolean;
dryRun: boolean; dryRun: boolean;
@@ -199,7 +198,9 @@ class UserDataImport {
delete processedHostData.id; delete processedHostData.id;
await getDb().insert(sshData).values(processedHostData); await getDb()
.insert(sshData)
.values(processedHostData as unknown as typeof sshData.$inferInsert);
imported++; imported++;
} catch (error) { } catch (error) {
errors.push( errors.push(
@@ -214,7 +215,7 @@ class UserDataImport {
private static async importSshCredentials( private static async importSshCredentials(
targetUserId: string, targetUserId: string,
credentials: any[], credentials: Record<string, unknown>[],
options: { options: {
replaceExisting: boolean; replaceExisting: boolean;
dryRun: boolean; dryRun: boolean;
@@ -255,7 +256,11 @@ class UserDataImport {
delete processedCredentialData.id; delete processedCredentialData.id;
await getDb().insert(sshCredentials).values(processedCredentialData); await getDb()
.insert(sshCredentials)
.values(
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
);
imported++; imported++;
} catch (error) { } catch (error) {
errors.push( errors.push(
@@ -270,7 +275,7 @@ class UserDataImport {
private static async importFileManagerData( private static async importFileManagerData(
targetUserId: string, targetUserId: string,
fileManagerData: any, fileManagerData: Record<string, unknown>,
options: { replaceExisting: boolean; dryRun: boolean }, options: { replaceExisting: boolean; dryRun: boolean },
) { ) {
let imported = 0; let imported = 0;
@@ -357,7 +362,7 @@ class UserDataImport {
private static async importDismissedAlerts( private static async importDismissedAlerts(
targetUserId: string, targetUserId: string,
alerts: any[], alerts: Record<string, unknown>[],
options: { replaceExisting: boolean; dryRun: boolean }, options: { replaceExisting: boolean; dryRun: boolean },
) { ) {
let imported = 0; let imported = 0;
@@ -377,7 +382,7 @@ class UserDataImport {
.where( .where(
and( and(
eq(dismissedAlerts.userId, targetUserId), eq(dismissedAlerts.userId, targetUserId),
eq(dismissedAlerts.alertId, alert.alertId), eq(dismissedAlerts.alertId, alert.alertId as string),
), ),
); );
@@ -396,10 +401,12 @@ class UserDataImport {
if (existing.length > 0 && options.replaceExisting) { if (existing.length > 0 && options.replaceExisting) {
await getDb() await getDb()
.update(dismissedAlerts) .update(dismissedAlerts)
.set(newAlert) .set(newAlert as typeof dismissedAlerts.$inferInsert)
.where(eq(dismissedAlerts.id, existing[0].id)); .where(eq(dismissedAlerts.id, existing[0].id));
} else { } else {
await getDb().insert(dismissedAlerts).values(newAlert); await getDb()
.insert(dismissedAlerts)
.values(newAlert as typeof dismissedAlerts.$inferInsert);
} }
imported++; imported++;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system"; type Theme = "dark" | "light" | "system";

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Chart Container
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
/>
);
});
ChartContainer.displayName = "ChartContainer";
export { ChartContainer, RechartsPrimitive };

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"; import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label"; import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";

View File

@@ -5,8 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface PasswordInputProps type PasswordInputProps = React.InputHTMLAttributes<HTMLInputElement>;
extends React.InputHTMLAttributes<HTMLInputElement> {}
export const PasswordInput = React.forwardRef< export const PasswordInput = React.forwardRef<
HTMLInputElement, HTMLInputElement,

View File

@@ -17,10 +17,7 @@ export const Status = ({ className, status, ...props }: StatusProps) => (
export type StatusIndicatorProps = HTMLAttributes<HTMLSpanElement>; export type StatusIndicatorProps = HTMLAttributes<HTMLSpanElement>;
export const StatusIndicator = ({ export const StatusIndicator = ({ ...props }: StatusIndicatorProps) => (
className,
...props
}: StatusIndicatorProps) => (
<span className="relative flex h-2 w-2" {...props}> <span className="relative flex h-2 w-2" {...props}>
<span <span
className={cn( className={cn(

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
@@ -8,13 +9,6 @@ import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
Tooltip, Tooltip,
@@ -26,7 +20,6 @@ import {
const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b"; const SIDEBAR_KEYBOARD_SHORTCUT = "b";
@@ -161,7 +154,7 @@ function Sidebar({
variant?: "sidebar" | "floating" | "inset"; variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none"; collapsible?: "offcanvas" | "icon" | "none";
}) { }) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); const { state } = useSidebar();
if (collapsible === "none") { if (collapsible === "none") {
return ( return (

View File

@@ -8,7 +8,10 @@ const Toaster = ({ ...props }: ToasterProps) => {
const originalToast = toast; const originalToast = toast;
const rateLimitedToast = (message: string, options?: any) => { const rateLimitedToast = (
message: string,
options?: Record<string, unknown>,
) => {
const now = Date.now(); const now = Date.now();
const lastToast = lastToastRef.current; const lastToast = lastToastRef.current;
@@ -25,13 +28,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
}; };
Object.assign(toast, { Object.assign(toast, {
success: (message: string, options?: any) => success: (message: string, options?: Record<string, unknown>) =>
rateLimitedToast(message, { ...options, type: "success" }), rateLimitedToast(message, { ...options, type: "success" }),
error: (message: string, options?: any) => error: (message: string, options?: Record<string, unknown>) =>
rateLimitedToast(message, { ...options, type: "error" }), rateLimitedToast(message, { ...options, type: "error" }),
warning: (message: string, options?: any) => warning: (message: string, options?: Record<string, unknown>) =>
rateLimitedToast(message, { ...options, type: "warning" }), rateLimitedToast(message, { ...options, type: "warning" }),
info: (message: string, options?: any) => info: (message: string, options?: Record<string, unknown>) =>
rateLimitedToast(message, { ...options, type: "info" }), rateLimitedToast(message, { ...options, type: "info" }),
message: rateLimitedToast, message: rateLimitedToast,
}); });

View File

@@ -2,8 +2,7 @@ import * as React from "react";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
export interface TextareaProps export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View File

@@ -1,25 +1,25 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { VersionAlert } from "@/components/ui/version-alert.tsx"; import { VersionAlert } from "@/components/ui/version-alert.tsx";
import { RefreshCw, X } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { checkElectronUpdate, isElectron } from "@/ui/main-axios.ts"; import { checkElectronUpdate, isElectron } from "@/ui/main-axios.ts";
interface VersionCheckModalProps { interface VersionCheckModalProps {
onDismiss: () => void;
onContinue: () => void; onContinue: () => void;
isAuthenticated?: boolean; isAuthenticated?: boolean;
} }
export function VersionCheckModal({ export function VersionCheckModal({
onDismiss,
onContinue, onContinue,
isAuthenticated = false, isAuthenticated = false,
}: VersionCheckModalProps) { }: VersionCheckModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [versionInfo, setVersionInfo] = useState<any>(null); const [versionInfo, setVersionInfo] = useState<Record<
string,
unknown
> | null>(null);
const [versionChecking, setVersionChecking] = useState(false); const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed, setVersionDismissed] = useState(false); const [versionDismissed] = useState(false);
useEffect(() => { useEffect(() => {
if (isElectron()) { if (isElectron()) {
@@ -47,10 +47,6 @@ export function VersionCheckModal({
} }
}; };
const handleVersionDismiss = () => {
setVersionDismissed(true);
};
const handleDownloadUpdate = () => { const handleDownloadUpdate = () => {
if (versionInfo?.latest_release?.html_url) { if (versionInfo?.latest_release?.html_url) {
window.open(versionInfo.latest_release.html_url, "_blank"); window.open(versionInfo.latest_release.html_url, "_blank");

View File

@@ -17,7 +17,7 @@ export interface LogContext {
errorCode?: string; errorCode?: string;
errorMessage?: string; errorMessage?: string;
[key: string]: any; [key: string]: unknown;
} }
class FrontendLogger { class FrontendLogger {
@@ -218,7 +218,6 @@ class FrontendLogger {
context?: LogContext, context?: LogContext,
): void { ): void {
const cleanUrl = this.sanitizeUrl(url); const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
const statusIcon = this.getStatusIcon(status); const statusIcon = this.getStatusIcon(status);
const performanceIcon = this.getPerformanceIcon(responseTime); const performanceIcon = this.getPerformanceIcon(responseTime);
@@ -244,7 +243,6 @@ class FrontendLogger {
context?: LogContext, context?: LogContext,
): void { ): void {
const cleanUrl = this.sanitizeUrl(url); const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
const statusIcon = this.getStatusIcon(status); const statusIcon = this.getStatusIcon(status);
this.error(`${statusIcon} ${status} ${errorMessage}`, undefined, { this.error(`${statusIcon} ${status} ${errorMessage}`, undefined, {
@@ -265,7 +263,6 @@ class FrontendLogger {
context?: LogContext, context?: LogContext,
): void { ): void {
const cleanUrl = this.sanitizeUrl(url); const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
this.error(`🌐 Network Error: ${errorMessage}`, undefined, { this.error(`🌐 Network Error: ${errorMessage}`, undefined, {
...context, ...context,
@@ -279,7 +276,6 @@ class FrontendLogger {
authError(method: string, url: string, context?: LogContext): void { authError(method: string, url: string, context?: LogContext): void {
const cleanUrl = this.sanitizeUrl(url); const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
this.security(`🔐 Authentication Required`, { this.security(`🔐 Authentication Required`, {
...context, ...context,
@@ -298,7 +294,6 @@ class FrontendLogger {
context?: LogContext, context?: LogContext,
): void { ): void {
const cleanUrl = this.sanitizeUrl(url); const cleanUrl = this.sanitizeUrl(url);
const shortUrl = this.getShortUrl(cleanUrl);
this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, { this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, {
...context, ...context,

View File

@@ -191,6 +191,40 @@
"enableRightClickCopyPaste": "Enable rightclick copy/paste", "enableRightClickCopyPaste": "Enable rightclick copy/paste",
"shareIdeas": "Have ideas for what should come next for ssh tools? Share them on" "shareIdeas": "Have ideas for what should come next for ssh tools? Share them on"
}, },
"snippets": {
"title": "Snippets",
"new": "New Snippet",
"create": "Create Snippet",
"edit": "Edit Snippet",
"run": "Run",
"empty": "No snippets yet",
"emptyHint": "Create a snippet to save commonly used commands",
"name": "Name",
"description": "Description",
"content": "Command",
"namePlaceholder": "e.g., Restart Nginx",
"descriptionPlaceholder": "Optional description",
"contentPlaceholder": "e.g., sudo systemctl restart nginx",
"nameRequired": "Name is required",
"contentRequired": "Command is required",
"createDescription": "Create a new command snippet for quick execution",
"editDescription": "Edit this command snippet",
"deleteConfirmTitle": "Delete Snippet",
"deleteConfirmDescription": "Are you sure you want to delete \"{{name}}\"?",
"createSuccess": "Snippet created successfully",
"updateSuccess": "Snippet updated successfully",
"deleteSuccess": "Snippet deleted successfully",
"createFailed": "Failed to create snippet",
"updateFailed": "Failed to update snippet",
"deleteFailed": "Failed to delete snippet",
"failedToFetch": "Failed to fetch snippets",
"executeSuccess": "Executing: {{name}}",
"copySuccess": "Copied \"{{name}}\" to clipboard",
"runTooltip": "Execute this snippet in the terminal",
"copyTooltip": "Copy snippet to clipboard",
"editTooltip": "Edit this snippet",
"deleteTooltip": "Delete this snippet"
},
"homepage": { "homepage": {
"loggedInTitle": "Logged in!", "loggedInTitle": "Logged in!",
"loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.", "loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.",
@@ -357,6 +391,7 @@
"admin": "Admin", "admin": "Admin",
"userProfile": "User Profile", "userProfile": "User Profile",
"tools": "Tools", "tools": "Tools",
"snippets": "Snippets",
"newTab": "New Tab", "newTab": "New Tab",
"splitScreen": "Split Screen", "splitScreen": "Split Screen",
"closeTab": "Close Tab", "closeTab": "Close Tab",
@@ -410,10 +445,12 @@
"general": "General", "general": "General",
"userRegistration": "User Registration", "userRegistration": "User Registration",
"allowNewAccountRegistration": "Allow new account registration", "allowNewAccountRegistration": "Allow new account registration",
"allowPasswordLogin": "Allow username/password login",
"missingRequiredFields": "Missing required fields: {{fields}}", "missingRequiredFields": "Missing required fields: {{fields}}",
"oidcConfigurationUpdated": "OIDC configuration updated successfully!", "oidcConfigurationUpdated": "OIDC configuration updated successfully!",
"failedToFetchOidcConfig": "Failed to fetch OIDC configuration", "failedToFetchOidcConfig": "Failed to fetch OIDC configuration",
"failedToFetchRegistrationStatus": "Failed to fetch registration status", "failedToFetchRegistrationStatus": "Failed to fetch registration status",
"failedToFetchPasswordLoginStatus": "Failed to fetch password login status",
"failedToFetchUsers": "Failed to fetch users", "failedToFetchUsers": "Failed to fetch users",
"oidcConfigurationDisabled": "OIDC configuration disabled successfully!", "oidcConfigurationDisabled": "OIDC configuration disabled successfully!",
"failedToUpdateOidcConfig": "Failed to update OIDC configuration", "failedToUpdateOidcConfig": "Failed to update OIDC configuration",
@@ -669,14 +706,34 @@
"terminal": "Terminal", "terminal": "Terminal",
"tunnel": "Tunnel", "tunnel": "Tunnel",
"fileManager": "File Manager", "fileManager": "File Manager",
"serverStats": "Server Stats",
"hostViewer": "Host Viewer", "hostViewer": "Host Viewer",
"enableServerStats": "Enable Server Stats",
"enableServerStatsDesc": "Enable/disable server statistics collection for this host",
"displayItems": "Display Items",
"displayItemsDesc": "Choose which metrics to display on the server stats page",
"enableCpu": "CPU Usage",
"enableMemory": "Memory Usage",
"enableDisk": "Disk Usage",
"enableNetwork": "Network Statistics (Coming Soon)",
"enableProcesses": "Process Count (Coming Soon)",
"enableUptime": "Uptime (Coming Soon)",
"enableHostname": "Hostname (Coming Soon)",
"enableOs": "Operating System (Coming Soon)",
"customCommands": "Custom Commands (Coming Soon)",
"customCommandsDesc": "Define custom shutdown and reboot commands for this server",
"shutdownCommand": "Shutdown Command",
"rebootCommand": "Reboot Command",
"confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The host will be moved to \"No Folder\".", "confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The host will be moved to \"No Folder\".",
"removedFromFolder": "Host \"{{name}}\" removed from folder successfully", "removedFromFolder": "Host \"{{name}}\" removed from folder successfully",
"failedToRemoveFromFolder": "Failed to remove host from folder", "failedToRemoveFromFolder": "Failed to remove host from folder",
"folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully", "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
"failedToRenameFolder": "Failed to rename folder", "failedToRenameFolder": "Failed to rename folder",
"movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully", "movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully",
"failedToMoveToFolder": "Failed to move host to folder" "failedToMoveToFolder": "Failed to move host to folder",
"statistics": "Statistics",
"enabledWidgets": "Enabled Widgets",
"enabledWidgetsDesc": "Select which statistics widgets to display for this host"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -710,7 +767,11 @@
"connectionTimeout": "Connection timeout", "connectionTimeout": "Connection timeout",
"terminalTitle": "Terminal - {{host}}", "terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}", "terminalWithPath": "Terminal - {{host}}:{{path}}",
"runTitle": "Running {{command}} - {{host}}" "runTitle": "Running {{command}} - {{host}}",
"totpRequired": "Two-Factor Authentication Required",
"totpCodeLabel": "Verification Code",
"totpPlaceholder": "000000",
"totpVerify": "Verify"
}, },
"fileManager": { "fileManager": {
"title": "File Manager", "title": "File Manager",
@@ -994,7 +1055,9 @@
"fileComparison": "File Comparison: {{file1}} vs {{file2}}", "fileComparison": "File Comparison: {{file1}} vs {{file2}}",
"fileTooLarge": "File too large: {{error}}", "fileTooLarge": "File too large: {{error}}",
"sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})", "sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})",
"loadFileFailed": "Failed to load file: {{error}}" "loadFileFailed": "Failed to load file: {{error}}",
"connectedSuccessfully": "Connected successfully",
"totpVerificationFailed": "TOTP verification failed"
}, },
"tunnels": { "tunnels": {
"title": "SSH Tunnels", "title": "SSH Tunnels",
@@ -1083,6 +1146,7 @@
"loadAverageNA": "Avg: N/A", "loadAverageNA": "Avg: N/A",
"cpuUsage": "CPU Usage", "cpuUsage": "CPU Usage",
"memoryUsage": "Memory Usage", "memoryUsage": "Memory Usage",
"diskUsage": "Disk Usage",
"rootStorageSpace": "Root Storage Space", "rootStorageSpace": "Root Storage Space",
"of": "of", "of": "of",
"feedbackMessage": "Have ideas for what should come next for server management? Share them on", "feedbackMessage": "Have ideas for what should come next for server management? Share them on",
@@ -1094,9 +1158,29 @@
"refreshing": "Refreshing...", "refreshing": "Refreshing...",
"serverOffline": "Server Offline", "serverOffline": "Server Offline",
"cannotFetchMetrics": "Cannot fetch metrics from offline server", "cannotFetchMetrics": "Cannot fetch metrics from offline server",
"totpRequired": "TOTP Authentication Required",
"totpUnavailable": "Server Stats unavailable for TOTP-enabled servers",
"load": "Load", "load": "Load",
"free": "Free", "free": "Free",
"available": "Available" "available": "Available",
"editLayout": "Edit Layout",
"cancelEdit": "Cancel",
"addWidget": "Add Widget",
"saveLayout": "Save Layout",
"unsavedChanges": "Unsaved changes",
"layoutSaved": "Layout saved successfully",
"failedToSaveLayout": "Failed to save layout",
"systemInfo": "System Information",
"hostname": "Hostname",
"operatingSystem": "Operating System",
"kernel": "Kernel",
"totalUptime": "Total Uptime",
"seconds": "seconds",
"networkInterfaces": "Network Interfaces",
"noInterfacesFound": "No network interfaces found",
"totalProcesses": "Total Processes",
"running": "Running",
"noProcessesFound": "No processes found"
}, },
"auth": { "auth": {
"loginTitle": "Login to Termix", "loginTitle": "Login to Termix",

View File

@@ -189,6 +189,40 @@
"enableRightClickCopyPaste": "启用右键复制/粘贴", "enableRightClickCopyPaste": "启用右键复制/粘贴",
"shareIdeas": "对 SSH 工具有什么想法?在此分享" "shareIdeas": "对 SSH 工具有什么想法?在此分享"
}, },
"snippets": {
"title": "代码片段",
"new": "新建片段",
"create": "创建代码片段",
"edit": "编辑代码片段",
"run": "运行",
"empty": "暂无代码片段",
"emptyHint": "创建代码片段以保存常用命令",
"name": "名称",
"description": "描述",
"content": "命令",
"namePlaceholder": "例如: 重启 Nginx",
"descriptionPlaceholder": "可选描述",
"contentPlaceholder": "例如: sudo systemctl restart nginx",
"nameRequired": "名称不能为空",
"contentRequired": "命令不能为空",
"createDescription": "创建新的命令片段以便快速执行",
"editDescription": "编辑此命令片段",
"deleteConfirmTitle": "删除代码片段",
"deleteConfirmDescription": "确定要删除 \"{{name}}\" 吗?",
"createSuccess": "代码片段创建成功",
"updateSuccess": "代码片段更新成功",
"deleteSuccess": "代码片段删除成功",
"createFailed": "创建代码片段失败",
"updateFailed": "更新代码片段失败",
"deleteFailed": "删除代码片段失败",
"failedToFetch": "获取代码片段失败",
"executeSuccess": "正在执行: {{name}}",
"copySuccess": "已复制 \"{{name}}\" 到剪贴板",
"runTooltip": "在终端中执行此片段",
"copyTooltip": "复制片段到剪贴板",
"editTooltip": "编辑此片段",
"deleteTooltip": "删除此片段"
},
"homepage": { "homepage": {
"loggedInTitle": "登录成功!", "loggedInTitle": "登录成功!",
"loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。", "loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。",
@@ -343,6 +377,7 @@
"admin": "管理员", "admin": "管理员",
"userProfile": "用户资料", "userProfile": "用户资料",
"tools": "工具", "tools": "工具",
"snippets": "代码片段",
"newTab": "新标签页", "newTab": "新标签页",
"splitScreen": "分屏", "splitScreen": "分屏",
"closeTab": "关闭标签页", "closeTab": "关闭标签页",
@@ -396,10 +431,12 @@
"general": "常规", "general": "常规",
"userRegistration": "用户注册", "userRegistration": "用户注册",
"allowNewAccountRegistration": "允许新账户注册", "allowNewAccountRegistration": "允许新账户注册",
"allowPasswordLogin": "允许用户名/密码登录",
"missingRequiredFields": "缺少必填字段:{{fields}}", "missingRequiredFields": "缺少必填字段:{{fields}}",
"oidcConfigurationUpdated": "OIDC 配置更新成功!", "oidcConfigurationUpdated": "OIDC 配置更新成功!",
"failedToFetchOidcConfig": "获取 OIDC 配置失败", "failedToFetchOidcConfig": "获取 OIDC 配置失败",
"failedToFetchRegistrationStatus": "获取注册状态失败", "failedToFetchRegistrationStatus": "获取注册状态失败",
"failedToFetchPasswordLoginStatus": "获取密码登录状态失败",
"failedToFetchUsers": "获取用户列表失败", "failedToFetchUsers": "获取用户列表失败",
"oidcConfigurationDisabled": "OIDC 配置禁用成功!", "oidcConfigurationDisabled": "OIDC 配置禁用成功!",
"failedToUpdateOidcConfig": "更新 OIDC 配置失败", "failedToUpdateOidcConfig": "更新 OIDC 配置失败",
@@ -691,19 +728,44 @@
"terminal": "终端", "terminal": "终端",
"tunnel": "隧道", "tunnel": "隧道",
"fileManager": "文件管理器", "fileManager": "文件管理器",
"serverStats": "服务器统计",
"hostViewer": "主机查看器",
"enableServerStats": "启用服务器统计",
"enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集",
"displayItems": "显示项目",
"displayItemsDesc": "选择在服务器统计页面上显示哪些指标",
"enableCpu": "CPU使用率",
"enableMemory": "内存使用率",
"enableDisk": "磁盘使用率",
"enableNetwork": "网络统计(即将推出)",
"enableProcesses": "进程数(即将推出)",
"enableUptime": "运行时间(即将推出)",
"enableHostname": "主机名(即将推出)",
"enableOs": "操作系统(即将推出)",
"customCommands": "自定义命令(即将推出)",
"customCommandsDesc": "为此服务器定义自定义关机和重启命令",
"shutdownCommand": "关机命令",
"rebootCommand": "重启命令",
"confirmRemoveFromFolder": "确定要将\"{{name}}\"从文件夹\"{{folder}}\"中移除吗?主机将被移动到\"无文件夹\"。", "confirmRemoveFromFolder": "确定要将\"{{name}}\"从文件夹\"{{folder}}\"中移除吗?主机将被移动到\"无文件夹\"。",
"removedFromFolder": "主机\"{{name}}\"已成功从文件夹中移除", "removedFromFolder": "主机\"{{name}}\"已成功从文件夹中移除",
"failedToRemoveFromFolder": "从文件夹中移除主机失败", "failedToRemoveFromFolder": "从文件夹中移除主机失败",
"folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"", "folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"",
"failedToRenameFolder": "重命名文件夹失败", "failedToRenameFolder": "重命名文件夹失败",
"movedToFolder": "主机\"{{name}}\"已成功移动到\"{{folder}}\"", "movedToFolder": "主机\"{{name}}\"已成功移动到\"{{folder}}\"",
"failedToMoveToFolder": "移动主机到文件夹失败" "failedToMoveToFolder": "移动主机到文件夹失败",
"statistics": "统计",
"enabledWidgets": "已启用组件",
"enabledWidgetsDesc": "选择要为此主机显示的统计组件"
}, },
"terminal": { "terminal": {
"title": "终端", "title": "终端",
"terminalTitle": "终端 - {{host}}", "terminalTitle": "终端 - {{host}}",
"terminalWithPath": "终端 - {{host}}:{{path}}", "terminalWithPath": "终端 - {{host}}:{{path}}",
"runTitle": "运行 {{command}} - {{host}}", "runTitle": "运行 {{command}} - {{host}}",
"totpRequired": "需要双因素认证",
"totpCodeLabel": "验证码",
"totpPlaceholder": "000000",
"totpVerify": "验证",
"connect": "连接主机", "connect": "连接主机",
"disconnect": "断开连接", "disconnect": "断开连接",
"clear": "清屏", "clear": "清屏",
@@ -985,7 +1047,9 @@
"fileComparison": "文件对比:{{file1}} 与 {{file2}}", "fileComparison": "文件对比:{{file1}} 与 {{file2}}",
"fileTooLarge": "文件过大:{{error}}", "fileTooLarge": "文件过大:{{error}}",
"sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接", "sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接",
"loadFileFailed": "加载文件失败:{{error}}" "loadFileFailed": "加载文件失败:{{error}}",
"connectedSuccessfully": "连接成功",
"totpVerificationFailed": "TOTP 验证失败"
}, },
"tunnels": { "tunnels": {
"title": "SSH 隧道", "title": "SSH 隧道",
@@ -1063,6 +1127,7 @@
"loadAverageNA": "平均: N/A", "loadAverageNA": "平均: N/A",
"cpuUsage": "CPU 使用率", "cpuUsage": "CPU 使用率",
"memoryUsage": "内存使用率", "memoryUsage": "内存使用率",
"diskUsage": "磁盘使用率",
"rootStorageSpace": "根目录存储空间", "rootStorageSpace": "根目录存储空间",
"of": "的", "of": "的",
"feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧", "feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
@@ -1073,7 +1138,29 @@
"refreshing": "正在刷新...", "refreshing": "正在刷新...",
"serverOffline": "服务器离线", "serverOffline": "服务器离线",
"cannotFetchMetrics": "无法从离线服务器获取指标", "cannotFetchMetrics": "无法从离线服务器获取指标",
"load": "负载" "totpRequired": "需要 TOTP 认证",
"totpUnavailable": "启用了 TOTP 的服务器无法使用服务器统计功能",
"load": "负载",
"free": "空闲",
"available": "可用",
"editLayout": "编辑布局",
"cancelEdit": "取消",
"addWidget": "添加小组件",
"saveLayout": "保存布局",
"unsavedChanges": "有未保存的更改",
"layoutSaved": "布局保存成功",
"failedToSaveLayout": "保存布局失败",
"systemInfo": "系统信息",
"hostname": "主机名",
"operatingSystem": "操作系统",
"kernel": "内核",
"totalUptime": "总运行时间",
"seconds": "秒",
"networkInterfaces": "网络接口",
"noInterfacesFound": "未找到网络接口",
"totalProcesses": "总进程数",
"running": "运行中",
"noProcessesFound": "未找到进程"
}, },
"auth": { "auth": {
"loginTitle": "登录 Termix", "loginTitle": "登录 Termix",

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import { StrictMode, useEffect, useState, useRef } from "react"; import { StrictMode, useEffect, useState, useRef } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
@@ -9,7 +10,6 @@ import { isElectron } from "./ui/main-axios.ts";
function useWindowWidth() { function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth); const [width, setWidth] = useState(window.innerWidth);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const lastSwitchTime = useRef(0); const lastSwitchTime = useRef(0);
const isCurrentlyMobile = useRef(window.innerWidth < 768); const isCurrentlyMobile = useRef(window.innerWidth < 768);
const hasSwitchedOnce = useRef(false); const hasSwitchedOnce = useRef(false);
@@ -36,7 +36,6 @@ function useWindowWidth() {
isCurrentlyMobile.current = newIsMobile; isCurrentlyMobile.current = newIsMobile;
hasSwitchedOnce.current = true; hasSwitchedOnce.current = true;
setWidth(newWidth); setWidth(newWidth);
setIsMobile(newIsMobile);
} else { } else {
setWidth(newWidth); setWidth(newWidth);
} }

View File

@@ -1,22 +1,49 @@
interface ServerConfig {
serverUrl?: string;
[key: string]: unknown;
}
interface ConnectionTestResult {
success: boolean;
error?: string;
[key: string]: unknown;
}
interface DialogOptions {
title?: string;
defaultPath?: string;
buttonLabel?: string;
filters?: Array<{ name: string; extensions: string[] }>;
properties?: string[];
[key: string]: unknown;
}
interface DialogResult {
canceled: boolean;
filePath?: string;
filePaths?: string[];
[key: string]: unknown;
}
export interface ElectronAPI { export interface ElectronAPI {
getAppVersion: () => Promise<string>; getAppVersion: () => Promise<string>;
getPlatform: () => Promise<string>; getPlatform: () => Promise<string>;
getServerConfig: () => Promise<any>; getServerConfig: () => Promise<ServerConfig>;
saveServerConfig: (config: any) => Promise<any>; saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
testServerConnection: (serverUrl: string) => Promise<any>; testServerConnection: (serverUrl: string) => Promise<ConnectionTestResult>;
showSaveDialog: (options: any) => Promise<any>; showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
showOpenDialog: (options: any) => Promise<any>; showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
onUpdateAvailable: (callback: Function) => void; onUpdateAvailable: (callback: () => void) => void;
onUpdateDownloaded: (callback: Function) => void; onUpdateDownloaded: (callback: () => void) => void;
removeAllListeners: (channel: string) => void; removeAllListeners: (channel: string) => void;
isElectron: boolean; isElectron: boolean;
isDev: boolean; isDev: boolean;
invoke: (channel: string, ...args: any[]) => Promise<any>; invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
createTempFile: (fileData: { createTempFile: (fileData: {
fileName: string; fileName: string;

View File

@@ -1,9 +1,9 @@
// ============================================================================ // ============================================================================
// CENTRAL TYPE DEFINITIONS // CENTRAL TYPE DEFINITIONS
// ============================================================================ // ============================================================================
// This file contains all shared interfaces and types used across the application
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import type { Request } from "express";
// ============================================================================ // ============================================================================
// SSH HOST TYPES // SSH HOST TYPES
@@ -35,6 +35,7 @@ export interface SSHHost {
enableFileManager: boolean; enableFileManager: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: TunnelConnection[]; tunnelConnections: TunnelConnection[];
statsConfig?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -57,7 +58,8 @@ export interface SSHHostData {
enableTunnel?: boolean; enableTunnel?: boolean;
enableFileManager?: boolean; enableFileManager?: boolean;
defaultPath?: string; defaultPath?: string;
tunnelConnections?: any[]; tunnelConnections?: TunnelConnection[];
statsConfig?: string;
} }
// ============================================================================ // ============================================================================
@@ -261,8 +263,8 @@ export interface TabContextTab {
| "file_manager" | "file_manager"
| "user_profile"; | "user_profile";
title: string; title: string;
hostConfig?: any; hostConfig?: SSHHost;
terminalRef?: React.RefObject<any>; terminalRef?: any;
} }
// ============================================================================ // ============================================================================
@@ -303,7 +305,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519";
// API RESPONSE TYPES // API RESPONSE TYPES
// ============================================================================ // ============================================================================
export interface ApiResponse<T = any> { export interface ApiResponse<T = unknown> {
data?: T; data?: T;
error?: string; error?: string;
message?: string; message?: string;
@@ -366,13 +368,13 @@ export interface SSHTunnelViewerProps {
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any> ) => Promise<void>
>; >;
onTunnelAction?: ( onTunnelAction?: (
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any>; ) => Promise<void>;
} }
export interface FileManagerProps { export interface FileManagerProps {
@@ -400,7 +402,7 @@ export interface SSHTunnelObjectProps {
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any>; ) => Promise<void>;
compact?: boolean; compact?: boolean;
bare?: boolean; bare?: boolean;
} }
@@ -413,6 +415,26 @@ export interface FolderStats {
}>; }>;
} }
// ============================================================================
// SNIPPETS TYPES
// ============================================================================
export interface Snippet {
id: number;
userId: string;
name: string;
content: string;
description?: string;
createdAt: string;
updatedAt: string;
}
export interface SnippetData {
name: string;
content: string;
description?: string;
}
// ============================================================================ // ============================================================================
// BACKEND TYPES // BACKEND TYPES
// ============================================================================ // ============================================================================
@@ -439,3 +461,95 @@ export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>; export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>; export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
// ============================================================================
// EXPRESS REQUEST TYPES
// ============================================================================
export interface AuthenticatedRequest extends Request {
userId: string;
user?: {
id: string;
username: string;
isAdmin: boolean;
};
}
// ============================================================================
// GITHUB API TYPES
// ============================================================================
export interface GitHubAsset {
id: number;
name: string;
size: number;
download_count: number;
browser_download_url: string;
}
export interface GitHubRelease {
id: number;
tag_name: string;
name: string;
body: string;
published_at: string;
html_url: string;
assets: GitHubAsset[];
prerelease: boolean;
draft: boolean;
}
export interface GitHubAPIResponse<T> {
data: T;
cached: boolean;
cache_age?: number;
timestamp?: number;
}
// ============================================================================
// CACHE TYPES
// ============================================================================
export interface CacheEntry<T = unknown> {
data: T;
timestamp: number;
expiresAt: number;
}
// ============================================================================
// DATABASE EXPORT/IMPORT TYPES
// ============================================================================
export interface ExportSummary {
sshHostsImported: number;
sshCredentialsImported: number;
fileManagerItemsImported: number;
dismissedAlertsImported: number;
credentialUsageImported: number;
settingsImported: number;
skippedItems: number;
errors: string[];
}
export interface ImportResult {
success: boolean;
summary: ExportSummary;
}
export interface ExportRequestBody {
password: string;
}
export interface ImportRequestBody {
password: string;
}
export interface ExportPreviewBody {
scope?: string;
includeCredentials?: boolean;
}
export interface RestoreRequestBody {
backupPath: string;
targetPath?: string;
}

View File

@@ -0,0 +1,16 @@
export type WidgetType =
| "cpu"
| "memory"
| "disk"
| "network"
| "uptime"
| "processes"
| "system";
export interface StatsConfig {
enabledWidgets: WidgetType[];
}
export const DEFAULT_STATS_CONFIG: StatsConfig = {
enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
};

View File

@@ -26,7 +26,6 @@ import {
Trash2, Trash2,
Users, Users,
Database, Database,
Key,
Lock, Lock,
Download, Download,
Upload, Upload,
@@ -37,14 +36,15 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts";
import { import {
getOIDCConfig, getOIDCConfig,
getRegistrationAllowed, getRegistrationAllowed,
getPasswordLoginAllowed,
getUserList, getUserList,
updateRegistrationAllowed, updateRegistrationAllowed,
updatePasswordLoginAllowed,
updateOIDCConfig, updateOIDCConfig,
disableOIDCConfig, disableOIDCConfig,
makeUserAdmin, makeUserAdmin,
removeAdminStatus, removeAdminStatus,
deleteUser, deleteUser,
getCookie,
isElectron, isElectron,
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
@@ -62,6 +62,9 @@ export function AdminSettings({
const [allowRegistration, setAllowRegistration] = React.useState(true); const [allowRegistration, setAllowRegistration] = React.useState(true);
const [regLoading, setRegLoading] = React.useState(false); const [regLoading, setRegLoading] = React.useState(false);
const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true);
const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false);
const [oidcConfig, setOidcConfig] = React.useState({ const [oidcConfig, setOidcConfig] = React.useState({
client_id: "", client_id: "",
client_secret: "", client_secret: "",
@@ -91,8 +94,6 @@ export function AdminSettings({
null, null,
); );
const [securityInitialized, setSecurityInitialized] = React.useState(true);
const [exportLoading, setExportLoading] = React.useState(false); const [exportLoading, setExportLoading] = React.useState(false);
const [importLoading, setImportLoading] = React.useState(false); const [importLoading, setImportLoading] = React.useState(false);
const [importFile, setImportFile] = React.useState<File | null>(null); const [importFile, setImportFile] = React.useState<File | null>(null);
@@ -102,7 +103,8 @@ export function AdminSettings({
React.useEffect(() => { React.useEffect(() => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -122,7 +124,8 @@ export function AdminSettings({
React.useEffect(() => { React.useEffect(() => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -141,9 +144,32 @@ export function AdminSettings({
}); });
}, []); }, []);
React.useEffect(() => {
if (isElectron()) {
const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) {
return;
}
}
getPasswordLoginAllowed()
.then((res) => {
if (typeof res?.allowed === "boolean") {
setAllowPasswordLogin(res.allowed);
}
})
.catch((err) => {
if (err.code !== "NO_SERVER_CONFIGURED") {
toast.error(t("admin.failedToFetchPasswordLoginStatus"));
}
});
}, []);
const fetchUsers = async () => { const fetchUsers = async () => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -172,6 +198,16 @@ export function AdminSettings({
} }
}; };
const handleTogglePasswordLogin = async (checked: boolean) => {
setPasswordLoginLoading(true);
try {
await updatePasswordLoginAllowed(checked);
setAllowPasswordLogin(checked);
} finally {
setPasswordLoginLoading(false);
}
};
const handleOIDCConfigSubmit = async (e: React.FormEvent) => { const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setOidcLoading(true); setOidcLoading(true);
@@ -198,9 +234,10 @@ export function AdminSettings({
try { try {
await updateOIDCConfig(oidcConfig); await updateOIDCConfig(oidcConfig);
toast.success(t("admin.oidcConfigurationUpdated")); toast.success(t("admin.oidcConfigurationUpdated"));
} catch (err: any) { } catch (err: unknown) {
setOidcError( setOidcError(
err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"), (err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToUpdateOidcConfig"),
); );
} finally { } finally {
setOidcLoading(false); setOidcLoading(false);
@@ -221,9 +258,10 @@ export function AdminSettings({
toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername })); toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
setNewAdminUsername(""); setNewAdminUsername("");
fetchUsers(); fetchUsers();
} catch (err: any) { } catch (err: unknown) {
setMakeAdminError( setMakeAdminError(
err?.response?.data?.error || t("admin.failedToMakeUserAdmin"), (err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToMakeUserAdmin"),
); );
} finally { } finally {
setMakeAdminLoading(false); setMakeAdminLoading(false);
@@ -236,7 +274,7 @@ export function AdminSettings({
await removeAdminStatus(username); await removeAdminStatus(username);
toast.success(t("admin.adminStatusRemoved", { username })); toast.success(t("admin.adminStatusRemoved", { username }));
fetchUsers(); fetchUsers();
} catch (err: any) { } catch {
toast.error(t("admin.failedToRemoveAdminStatus")); toast.error(t("admin.failedToRemoveAdminStatus"));
} }
}); });
@@ -250,7 +288,7 @@ export function AdminSettings({
await deleteUser(username); await deleteUser(username);
toast.success(t("admin.userDeletedSuccessfully", { username })); toast.success(t("admin.userDeletedSuccessfully", { username }));
fetchUsers(); fetchUsers();
} catch (err: any) { } catch {
toast.error(t("admin.failedToDeleteUser")); toast.error(t("admin.failedToDeleteUser"));
} }
}, },
@@ -280,7 +318,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1"); window.location.hostname === "127.0.0.1");
const apiUrl = isElectron() const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/export` ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
: isDev : isDev
? `http://localhost:30001/database/export` ? `http://localhost:30001/database/export`
: `${window.location.protocol}//${window.location.host}/database/export`; : `${window.location.protocol}//${window.location.host}/database/export`;
@@ -321,7 +359,7 @@ export function AdminSettings({
toast.error(error.error || t("admin.databaseExportFailed")); toast.error(error.error || t("admin.databaseExportFailed"));
} }
} }
} catch (err) { } catch {
toast.error(t("admin.databaseExportFailed")); toast.error(t("admin.databaseExportFailed"));
} finally { } finally {
setExportLoading(false); setExportLoading(false);
@@ -350,7 +388,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1"); window.location.hostname === "127.0.0.1");
const apiUrl = isElectron() const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/import` ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
: isDev : isDev
? `http://localhost:30001/database/import` ? `http://localhost:30001/database/import`
: `${window.location.protocol}//${window.location.host}/database/import`; : `${window.location.protocol}//${window.location.host}/database/import`;
@@ -413,7 +451,7 @@ export function AdminSettings({
toast.error(error.error || t("admin.databaseImportFailed")); toast.error(error.error || t("admin.databaseImportFailed"));
} }
} }
} catch (err) { } catch {
toast.error(t("admin.databaseImportFailed")); toast.error(t("admin.databaseImportFailed"));
} finally { } finally {
setImportLoading(false); setImportLoading(false);
@@ -483,6 +521,14 @@ export function AdminSettings({
/> />
{t("admin.allowNewAccountRegistration")} {t("admin.allowNewAccountRegistration")}
</label> </label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordLogin}
onCheckedChange={handleTogglePasswordLogin}
disabled={passwordLoginLoading}
/>
{t("admin.allowPasswordLogin")}
</label>
</div> </div>
</TabsContent> </TabsContent>
@@ -669,9 +715,13 @@ export function AdminSettings({
try { try {
await disableOIDCConfig(); await disableOIDCConfig();
toast.success(t("admin.oidcConfigurationDisabled")); toast.success(t("admin.oidcConfigurationDisabled"));
} catch (err: any) { } catch (err: unknown) {
setOidcError( setOidcError(
err?.response?.data?.error || (
err as {
response?: { data?: { error?: string } };
}
)?.response?.data?.error ||
t("admin.failedToDisableOidcConfig"), t("admin.failedToDisableOidcConfig"),
); );
} finally { } finally {

View File

@@ -42,9 +42,9 @@ export function CredentialEditor({
onFormSubmit, onFormSubmit,
}: CredentialEditorProps) { }: CredentialEditorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [credentials, setCredentials] = useState<Credential[]>([]); const [, setCredentials] = useState<Credential[]>([]);
const [folders, setFolders] = useState<string[]>([]); const [folders, setFolders] = useState<string[]>([]);
const [loading, setLoading] = useState(true); const [, setLoading] = useState(true);
const [fullCredentialDetails, setFullCredentialDetails] = const [fullCredentialDetails, setFullCredentialDetails] =
useState<Credential | null>(null); useState<Credential | null>(null);
@@ -79,7 +79,8 @@ export function CredentialEditor({
].sort() as string[]; ].sort() as string[];
setFolders(uniqueFolders); setFolders(uniqueFolders);
} catch (error) { } catch {
// Failed to load credentials
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -94,7 +95,7 @@ export function CredentialEditor({
try { try {
const fullDetails = await getCredentialDetails(editingCredential.id); const fullDetails = await getCredentialDetails(editingCredential.id);
setFullCredentialDetails(fullDetails); setFullCredentialDetails(fullDetails);
} catch (error) { } catch {
toast.error(t("credentials.failedToFetchCredentialDetails")); toast.error(t("credentials.failedToFetchCredentialDetails"));
} }
} else { } else {
@@ -154,7 +155,9 @@ export function CredentialEditor({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema) as any, resolver: zodResolver(formSchema) as unknown as Parameters<
typeof useForm<FormData>
>[0]["resolver"],
defaultValues: { defaultValues: {
name: "", name: "",
description: "", description: "",
@@ -197,7 +200,7 @@ export function CredentialEditor({
formData.publicKey = fullCredentialDetails.publicKey || ""; formData.publicKey = fullCredentialDetails.publicKey || "";
formData.keyPassword = fullCredentialDetails.keyPassword || ""; formData.keyPassword = fullCredentialDetails.keyPassword || "";
formData.keyType = formData.keyType =
(fullCredentialDetails.keyType as any) || ("auto" as const); (fullCredentialDetails.keyType as string) || ("auto" as const);
} }
form.reset(formData); form.reset(formData);
@@ -636,10 +639,6 @@ export function CredentialEditor({
form.setValue("key", null); form.setValue("key", null);
form.setValue("keyPassword", ""); form.setValue("keyPassword", "");
form.setValue("keyType", "auto"); form.setValue("keyType", "auto");
if (newAuthType === "password") {
} else if (newAuthType === "key") {
}
}} }}
className="flex-1 flex flex-col h-full min-h-0" className="flex-1 flex flex-col h-full min-h-0"
> >

View File

@@ -35,7 +35,7 @@ export function CredentialSelector({
? data ? data
: data.credentials || data.data || []; : data.credentials || data.data || [];
setCredentials(credentialsArray); setCredentials(credentialsArray);
} catch (error) { } catch {
const { toast } = await import("sonner"); const { toast } = await import("sonner");
toast.error(t("credentials.failedToFetchCredentials")); toast.error(t("credentials.failedToFetchCredentials"));
setCredentials([]); setCredentials([]);

View File

@@ -70,7 +70,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({
try { try {
const response = await getCredentialDetails(credential.id); const response = await getCredentialDetails(credential.id);
setCredentialDetails(response); setCredentialDetails(response);
} catch (error) { } catch {
toast.error(t("credentials.failedToFetchCredentialDetails")); toast.error(t("credentials.failedToFetchCredentialDetails"));
} }
}; };
@@ -79,7 +79,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({
try { try {
const response = await getCredentialHosts(credential.id); const response = await getCredentialHosts(credential.id);
setHostsUsing(response); setHostsUsing(response);
} catch (error) { } catch {
toast.error(t("credentials.failedToFetchHostsUsing")); toast.error(t("credentials.failedToFetchHostsUsing"));
} finally { } finally {
setLoading(false); setLoading(false);
@@ -97,7 +97,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
toast.success(t("copiedToClipboard", { field: fieldName })); toast.success(t("copiedToClipboard", { field: fieldName }));
} catch (error) { } catch {
toast.error(t("credentials.failedToCopy")); toast.error(t("credentials.failedToCopy"));
} }
}; };

View File

@@ -9,21 +9,7 @@ import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { import { Sheet, SheetContent } from "@/components/ui/sheet";
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -37,7 +23,6 @@ import {
Edit, Edit,
Trash2, Trash2,
Shield, Shield,
Pin,
Tag, Tag,
Info, Info,
FolderMinus, FolderMinus,
@@ -75,9 +60,7 @@ export function CredentialsManager({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [showViewer, setShowViewer] = useState(false); const [showViewer, setShowViewer] = useState(false);
const [viewingCredential, setViewingCredential] = useState<Credential | null>( const [viewingCredential] = useState<Credential | null>(null);
null,
);
const [draggedCredential, setDraggedCredential] = useState<Credential | null>( const [draggedCredential, setDraggedCredential] = useState<Credential | null>(
null, null,
); );
@@ -88,7 +71,15 @@ export function CredentialsManager({
const [showDeployDialog, setShowDeployDialog] = useState(false); const [showDeployDialog, setShowDeployDialog] = useState(false);
const [deployingCredential, setDeployingCredential] = const [deployingCredential, setDeployingCredential] =
useState<Credential | null>(null); useState<Credential | null>(null);
const [availableHosts, setAvailableHosts] = useState<any[]>([]); const [availableHosts, setAvailableHosts] = useState<
Array<{
id: number;
name: string;
ip: string;
port: number;
username: string;
}>
>([]);
const [selectedHostId, setSelectedHostId] = useState<string>(""); const [selectedHostId, setSelectedHostId] = useState<string>("");
const [deployLoading, setDeployLoading] = useState(false); const [deployLoading, setDeployLoading] = useState(false);
const [hostSearchQuery, setHostSearchQuery] = useState(""); const [hostSearchQuery, setHostSearchQuery] = useState("");
@@ -153,7 +144,7 @@ export function CredentialsManager({
const data = await getCredentials(); const data = await getCredentials();
setCredentials(data); setCredentials(data);
setError(null); setError(null);
} catch (err) { } catch {
setError(t("credentials.failedToFetchCredentials")); setError(t("credentials.failedToFetchCredentials"));
} finally { } finally {
setLoading(false); setLoading(false);
@@ -224,10 +215,13 @@ export function CredentialsManager({
); );
await fetchCredentials(); await fetchCredentials();
window.dispatchEvent(new CustomEvent("credentials:changed")); window.dispatchEvent(new CustomEvent("credentials:changed"));
} catch (err: any) { } catch (err: unknown) {
if (err.response?.data?.details) { const error = err as {
response?: { data?: { error?: string; details?: string } };
};
if (error.response?.data?.details) {
toast.error( toast.error(
`${err.response.data.error}\n${err.response.data.details}`, `${error.response.data.error}\n${error.response.data.details}`,
); );
} else { } else {
toast.error(t("credentials.failedToDeleteCredential")); toast.error(t("credentials.failedToDeleteCredential"));
@@ -256,7 +250,7 @@ export function CredentialsManager({
); );
await fetchCredentials(); await fetchCredentials();
window.dispatchEvent(new CustomEvent("credentials:changed")); window.dispatchEvent(new CustomEvent("credentials:changed"));
} catch (err) { } catch {
toast.error(t("credentials.failedToRemoveFromFolder")); toast.error(t("credentials.failedToRemoveFromFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);
@@ -285,7 +279,7 @@ export function CredentialsManager({
window.dispatchEvent(new CustomEvent("credentials:changed")); window.dispatchEvent(new CustomEvent("credentials:changed"));
setEditingFolder(null); setEditingFolder(null);
setEditingFolderName(""); setEditingFolderName("");
} catch (err) { } catch {
toast.error(t("credentials.failedToRenameFolder")); toast.error(t("credentials.failedToRenameFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);
@@ -325,7 +319,7 @@ export function CredentialsManager({
setDragOverFolder(folderName); setDragOverFolder(folderName);
}; };
const handleDragLeave = (e: React.DragEvent) => { const handleDragLeave = () => {
dragCounter.current--; dragCounter.current--;
if (dragCounter.current === 0) { if (dragCounter.current === 0) {
setDragOverFolder(null); setDragOverFolder(null);
@@ -359,7 +353,7 @@ export function CredentialsManager({
); );
await fetchCredentials(); await fetchCredentials();
window.dispatchEvent(new CustomEvent("credentials:changed")); window.dispatchEvent(new CustomEvent("credentials:changed"));
} catch (err) { } catch {
toast.error(t("credentials.failedToMoveToFolder")); toast.error(t("credentials.failedToMoveToFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);

View File

@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TOTPDialog } from "@/ui/components/TOTPDialog";
import { import {
Upload, Upload,
FolderPlus, FolderPlus,
@@ -22,8 +23,6 @@ import {
Search, Search,
Grid3X3, Grid3X3,
List, List,
Eye,
Settings,
} from "lucide-react"; } from "lucide-react";
import { TerminalWindow } from "./components/TerminalWindow"; import { TerminalWindow } from "./components/TerminalWindow";
import type { SSHHost, FileItem } from "../../../types/index.js"; import type { SSHHost, FileItem } from "../../../types/index.js";
@@ -38,6 +37,7 @@ import {
renameSSHItem, renameSSHItem,
moveSSHItem, moveSSHItem,
connectSSH, connectSSH,
verifySSHTOTP,
getSSHStatus, getSSHStatus,
keepSSHAlive, keepSSHAlive,
identifySSHSymlink, identifySSHSymlink,
@@ -85,9 +85,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { confirmWithToast } = useConfirmation(); const { confirmWithToast } = useConfirmation();
const [currentHost, setCurrentHost] = useState<SSHHost | null>( const [currentHost] = useState<SSHHost | null>(initialHost || null);
initialHost || null,
);
const [currentPath, setCurrentPath] = useState( const [currentPath, setCurrentPath] = useState(
initialHost?.defaultPath || "/", initialHost?.defaultPath || "/",
); );
@@ -98,6 +96,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [lastRefreshTime, setLastRefreshTime] = useState<number>(0); const [lastRefreshTime, setLastRefreshTime] = useState<number>(0);
const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [totpRequired, setTotpRequired] = useState(false);
const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
const [totpPrompt, setTotpPrompt] = useState<string>("");
const [pinnedFiles, setPinnedFiles] = useState<Set<string>>(new Set()); const [pinnedFiles, setPinnedFiles] = useState<Set<string>>(new Set());
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0); const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
const [isClosing, setIsClosing] = useState<boolean>(false); const [isClosing, setIsClosing] = useState<boolean>(false);
@@ -140,10 +141,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null); const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
const [editingFile, setEditingFile] = useState<FileItem | null>(null); const [editingFile, setEditingFile] = useState<FileItem | null>(null);
const { selectedFiles, selectFile, selectAll, clearSelection, setSelection } = const { selectedFiles, clearSelection, setSelection } = useFileSelection();
useFileSelection();
const { isDragging, dragHandlers } = useDragAndDrop({ const { dragHandlers } = useDragAndDrop({
onFilesDropped: handleFilesDropped, onFilesDropped: handleFilesDropped,
onError: (error) => toast.error(error), onError: (error) => toast.error(error),
maxFileSize: 5120, maxFileSize: 5120,
@@ -288,6 +288,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
userId: currentHost.userId, userId: currentHost.userId,
}); });
if (result?.requires_totp) {
setTotpRequired(true);
setTotpSessionId(sessionId);
setTotpPrompt(result.prompt || "Verification code:");
setIsLoading(false);
return;
}
setSshSessionId(sessionId); setSshSessionId(sessionId);
try { try {
@@ -298,10 +306,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files); setFiles(files);
clearSelection(); clearSelection();
initialLoadDoneRef.current = true; initialLoadDoneRef.current = true;
} catch (dirError: any) { } catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError); console.error("Failed to load initial directory:", dirError);
} }
} catch (error: any) { } catch (error: unknown) {
console.error("SSH connection failed:", error); console.error("SSH connection failed:", error);
handleCloseWithError( handleCloseWithError(
t("fileManager.failedToConnect") + ": " + (error.message || error), t("fileManager.failedToConnect") + ": " + (error.message || error),
@@ -340,7 +348,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files); setFiles(files);
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: unknown) {
if (currentLoadingPathRef.current === path) { if (currentLoadingPathRef.current === path) {
console.error("Failed to load directory:", error); console.error("Failed to load directory:", error);
@@ -522,7 +530,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileUploadedSuccessfully", { name: file.name }), t("fileManager.fileUploadedSuccessfully", { name: file.name }),
); );
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
toast.dismiss(progressToast); toast.dismiss(progressToast);
if ( if (
@@ -571,7 +579,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileDownloadedSuccessfully", { name: file.name }), t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
); );
} }
} catch (error: any) { } catch (error: unknown) {
if ( if (
error.message?.includes("connection") || error.message?.includes("connection") ||
error.message?.includes("established") error.message?.includes("established")
@@ -652,7 +660,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
); );
handleRefreshDirectory(); handleRefreshDirectory();
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: unknown) {
if ( if (
error.message?.includes("connection") || error.message?.includes("connection") ||
error.message?.includes("established") error.message?.includes("established")
@@ -706,28 +714,24 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
try { try {
let currentSessionId = sshSessionId; const currentSessionId = sshSessionId;
try { const status = await getSSHStatus(currentSessionId);
const status = await getSSHStatus(currentSessionId); if (!status.connected) {
if (!status.connected) { const result = await connectSSH(currentSessionId, {
const result = await connectSSH(currentSessionId, { hostId: currentHost.id,
hostId: currentHost.id, host: currentHost.ip,
host: currentHost.ip, port: currentHost.port,
port: currentHost.port, username: currentHost.username,
username: currentHost.username, authType: currentHost.authType,
authType: currentHost.authType, password: currentHost.password,
password: currentHost.password, key: currentHost.key,
key: currentHost.key, keyPassword: currentHost.keyPassword,
keyPassword: currentHost.keyPassword, credentialId: currentHost.credentialId,
credentialId: currentHost.credentialId, });
});
if (!result.success) { if (!result.success) {
throw new Error(t("fileManager.failedToReconnectSSH")); throw new Error(t("fileManager.failedToReconnectSSH"));
}
} }
} catch (sessionErr) {
throw sessionErr;
} }
const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path); const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path);
@@ -766,7 +770,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
component: createWindowComponent, component: createWindowComponent,
}); });
} }
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
error?.response?.data?.error || error?.response?.data?.error ||
error?.message || error?.message ||
@@ -775,7 +779,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
}; };
async function handleFileOpen(file: FileItem, editMode: boolean = false) { async function handleFileOpen(file: FileItem) {
if (file.type === "directory") { if (file.type === "directory") {
setCurrentPath(file.path); setCurrentPath(file.path);
} else if (file.type === "link") { } else if (file.type === "link") {
@@ -825,14 +829,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
} }
function handleFileEdit(file: FileItem) {
handleFileOpen(file, true);
}
function handleFileView(file: FileItem) {
handleFileOpen(file, false);
}
function handleContextMenu(event: React.MouseEvent, file?: FileItem) { function handleContextMenu(event: React.MouseEvent, file?: FileItem) {
event.preventDefault(); event.preventDefault();
@@ -905,7 +901,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
successCount++; successCount++;
} }
} }
} catch (error: any) { } catch (error: unknown) {
console.error(`Failed to ${operation} file ${file.name}:`, error); console.error(`Failed to ${operation} file ${file.name}:`, error);
toast.error( toast.error(
t("fileManager.operationFailed", { t("fileManager.operationFailed", {
@@ -1006,7 +1002,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (operation === "cut") { if (operation === "cut") {
setClipboard(null); setClipboard(null);
} }
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
`${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`,
); );
@@ -1041,7 +1037,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(), currentHost?.userId?.toString(),
); );
successCount++; successCount++;
} catch (error: any) { } catch (error: unknown) {
console.error( console.error(
`Failed to delete copied file ${copiedFile.targetName}:`, `Failed to delete copied file ${copiedFile.targetName}:`,
error, error,
@@ -1083,7 +1079,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(), currentHost?.userId?.toString(),
); );
successCount++; successCount++;
} catch (error: any) { } catch (error: unknown) {
console.error( console.error(
`Failed to move back file ${movedFile.targetName}:`, `Failed to move back file ${movedFile.targetName}:`,
error, error,
@@ -1123,7 +1119,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
`${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`,
); );
@@ -1195,7 +1191,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setCreateIntent(null); setCreateIntent(null);
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
console.error("Create failed:", error); console.error("Create failed:", error);
toast.error(t("fileManager.failedToCreateItem")); toast.error(t("fileManager.failedToCreateItem"));
} }
@@ -1224,7 +1220,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
); );
setEditingFile(null); setEditingFile(null);
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
console.error("Rename failed:", error); console.error("Rename failed:", error);
toast.error(t("fileManager.failedToRenameItem")); toast.error(t("fileManager.failedToRenameItem"));
} }
@@ -1238,6 +1234,47 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setEditingFile(null); setEditingFile(null);
} }
async function handleTotpSubmit(code: string) {
if (!totpSessionId || !code) return;
try {
setIsLoading(true);
const result = await verifySSHTOTP(totpSessionId, code);
if (result?.status === "success") {
setTotpRequired(false);
setTotpPrompt("");
setSshSessionId(totpSessionId);
setTotpSessionId(null);
try {
const response = await listSSHFiles(totpSessionId, currentPath);
const files = Array.isArray(response)
? response
: response?.files || [];
setFiles(files);
clearSelection();
initialLoadDoneRef.current = true;
toast.success(t("fileManager.connectedSuccessfully"));
} catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError);
}
}
} catch (error: unknown) {
console.error("TOTP verification failed:", error);
toast.error(t("fileManager.totpVerificationFailed"));
} finally {
setIsLoading(false);
}
}
function handleTotpCancel() {
setTotpRequired(false);
setTotpPrompt("");
setTotpSessionId(null);
if (onClose) onClose();
}
function generateUniqueName( function generateUniqueName(
baseName: string, baseName: string,
type: "file" | "directory", type: "file" | "directory",
@@ -1290,7 +1327,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
movedItems.push(file.name); movedItems.push(file.name);
successCount++; successCount++;
} }
} catch (error: any) { } catch (error: unknown) {
console.error(`Failed to move file ${file.name}:`, error); console.error(`Failed to move file ${file.name}:`, error);
toast.error( toast.error(
t("fileManager.moveFileFailed", { name: file.name }) + t("fileManager.moveFileFailed", { name: file.name }) +
@@ -1301,18 +1338,16 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
if (successCount > 0) { if (successCount > 0) {
const movedFiles = draggedFiles const movedFiles = draggedFiles.slice(0, successCount).map((file) => {
.slice(0, successCount) const targetPath = targetFolder.path.endsWith("/")
.map((file, index) => { ? `${targetFolder.path}${file.name}`
const targetPath = targetFolder.path.endsWith("/") : `${targetFolder.path}/${file.name}`;
? `${targetFolder.path}${file.name}` return {
: `${targetFolder.path}/${file.name}`; originalPath: file.path,
return { targetPath: targetPath,
originalPath: file.path, targetName: file.name,
targetPath: targetPath, };
targetName: file.name, });
};
});
const undoAction: UndoAction = { const undoAction: UndoAction = {
type: "cut", type: "cut",
@@ -1338,7 +1373,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory(); handleRefreshDirectory();
clearSelection(); clearSelection();
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Drag move operation failed:", error); console.error("Drag move operation failed:", error);
toast.error(t("fileManager.moveOperationFailed") + ": " + error.message); toast.error(t("fileManager.moveOperationFailed") + ": " + error.message);
} }
@@ -1409,7 +1444,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
await dragToDesktop.dragFilesToDesktop(files); await dragToDesktop.dragFilesToDesktop(files);
} }
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Drag to desktop failed:", error); console.error("Drag to desktop failed:", error);
toast.error( toast.error(
t("fileManager.dragFailed") + t("fileManager.dragFailed") +
@@ -1504,7 +1539,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
try { try {
const pinnedData = await getPinnedFiles(currentHost.id); const pinnedData = await getPinnedFiles(currentHost.id);
const pinnedPaths = new Set(pinnedData.map((item: any) => item.path)); const pinnedPaths = new Set(
pinnedData.map((item: Record<string, unknown>) => item.path),
);
setPinnedFiles(pinnedPaths); setPinnedFiles(pinnedPaths);
} catch (error) { } catch (error) {
console.error("Failed to load pinned files:", error); console.error("Failed to load pinned files:", error);
@@ -1806,6 +1843,13 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
/> />
</div> </div>
</div> </div>
<TOTPDialog
isOpen={totpRequired}
prompt={totpPrompt}
onSubmit={handleTotpSubmit}
onCancel={handleTotpCancel}
/>
</div> </div>
); );
} }

View File

@@ -13,8 +13,6 @@ import {
RefreshCw, RefreshCw,
Clipboard, Clipboard,
Eye, Eye,
Share,
ExternalLink,
Terminal, Terminal,
Play, Play,
Star, Star,
@@ -190,7 +188,6 @@ export function FileManagerContextMenu({
const isSingleFile = files.length === 1; const isSingleFile = files.length === 1;
const isMultipleFiles = files.length > 1; const isMultipleFiles = files.length > 1;
const hasFiles = files.some((f) => f.type === "file"); const hasFiles = files.some((f) => f.type === "file");
const hasDirectories = files.some((f) => f.type === "directory");
const hasExecutableFiles = files.some( const hasExecutableFiles = files.some(
(f) => f.type === "file" && f.executable, (f) => f.type === "file" && f.executable,
); );

View File

@@ -166,7 +166,6 @@ const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
export function FileManagerGrid({ export function FileManagerGrid({
files, files,
selectedFiles, selectedFiles,
onFileSelect,
onFileOpen, onFileOpen,
onSelectionChange, onSelectionChange,
currentPath, currentPath,
@@ -188,7 +187,6 @@ export function FileManagerGrid({
onUndo, onUndo,
onFileDrop, onFileDrop,
onFileDiff, onFileDiff,
onSystemDragStart,
onSystemDragEnd, onSystemDragEnd,
hasClipboard, hasClipboard,
createIntent, createIntent,
@@ -327,7 +325,6 @@ export function FileManagerGrid({
dragState.files[0].type === "file" dragState.files[0].type === "file"
) { ) {
onFileDiff?.(dragState.files[0], targetFile); onFileDiff?.(dragState.files[0], targetFile);
} else {
} }
setDragState({ type: "none", files: [], counter: 0 }); setDragState({ type: "none", files: [], counter: 0 });
@@ -429,16 +426,6 @@ export function FileManagerGrid({
setIsEditingPath(false); setIsEditingPath(false);
}; };
const handlePathInputKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
confirmEditingPath();
} else if (e.key === "Escape") {
e.preventDefault();
cancelEditingPath();
}
};
useEffect(() => { useEffect(() => {
if (!isEditingPath) { if (!isEditingPath) {
setEditPathValue(currentPath); setEditPathValue(currentPath);
@@ -458,8 +445,6 @@ export function FileManagerGrid({
type: "external", type: "external",
counter: prev.counter + 1, counter: prev.counter + 1,
})); }));
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
}
} }
}, },
[dragState.type], [dragState.type],
@@ -620,7 +605,7 @@ export function FileManagerGrid({
); );
useEffect(() => { useEffect(() => {
const handleGlobalMouseUp = (e: MouseEvent) => { const handleGlobalMouseUp = () => {
if (isSelecting) { if (isSelecting) {
setIsSelecting(false); setIsSelecting(false);
setSelectionStart(null); setSelectionStart(null);

View File

@@ -23,6 +23,35 @@ import {
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
import { toast } from "sonner"; import { toast } from "sonner";
interface RecentFileData {
id: number;
name: string;
path: string;
lastOpened?: string;
[key: string]: unknown;
}
interface PinnedFileData {
id: number;
name: string;
path: string;
[key: string]: unknown;
}
interface ShortcutData {
id: number;
name: string;
path: string;
[key: string]: unknown;
}
interface DirectoryItemData {
name: string;
path: string;
type: string;
[key: string]: unknown;
}
export interface SidebarItem { export interface SidebarItem {
id: string; id: string;
name: string; name: string;
@@ -37,7 +66,6 @@ interface FileManagerSidebarProps {
currentHost: SSHHost; currentHost: SSHHost;
currentPath: string; currentPath: string;
onPathChange: (path: string) => void; onPathChange: (path: string) => void;
onLoadDirectory?: (path: string) => void;
onFileOpen?: (file: SidebarItem) => void; onFileOpen?: (file: SidebarItem) => void;
sshSessionId?: string; sshSessionId?: string;
refreshTrigger?: number; refreshTrigger?: number;
@@ -47,7 +75,6 @@ export function FileManagerSidebar({
currentHost, currentHost,
currentPath, currentPath,
onPathChange, onPathChange,
onLoadDirectory,
onFileOpen, onFileOpen,
sshSessionId, sshSessionId,
refreshTrigger, refreshTrigger,
@@ -88,31 +115,37 @@ export function FileManagerSidebar({
try { try {
const recentData = await getRecentFiles(currentHost.id); const recentData = await getRecentFiles(currentHost.id);
const recentItems = recentData.slice(0, 5).map((item: any) => ({ const recentItems = (recentData as RecentFileData[])
id: `recent-${item.id}`, .slice(0, 5)
name: item.name, .map((item: RecentFileData) => ({
path: item.path, id: `recent-${item.id}`,
type: "recent" as const, name: item.name,
lastAccessed: item.lastOpened, path: item.path,
})); type: "recent" as const,
lastAccessed: item.lastOpened,
}));
setRecentItems(recentItems); setRecentItems(recentItems);
const pinnedData = await getPinnedFiles(currentHost.id); const pinnedData = await getPinnedFiles(currentHost.id);
const pinnedItems = pinnedData.map((item: any) => ({ const pinnedItems = (pinnedData as PinnedFileData[]).map(
id: `pinned-${item.id}`, (item: PinnedFileData) => ({
name: item.name, id: `pinned-${item.id}`,
path: item.path, name: item.name,
type: "pinned" as const, path: item.path,
})); type: "pinned" as const,
}),
);
setPinnedItems(pinnedItems); setPinnedItems(pinnedItems);
const shortcutData = await getFolderShortcuts(currentHost.id); const shortcutData = await getFolderShortcuts(currentHost.id);
const shortcutItems = shortcutData.map((item: any) => ({ const shortcutItems = (shortcutData as ShortcutData[]).map(
id: `shortcut-${item.id}`, (item: ShortcutData) => ({
name: item.name, id: `shortcut-${item.id}`,
path: item.path, name: item.name,
type: "shortcut" as const, path: item.path,
})); type: "shortcut" as const,
}),
);
setShortcuts(shortcutItems); setShortcuts(shortcutItems);
} catch (error) { } catch (error) {
console.error("Failed to load quick access data:", error); console.error("Failed to load quick access data:", error);
@@ -230,12 +263,12 @@ export function FileManagerSidebar({
try { try {
const response = await listSSHFiles(sshSessionId, "/"); const response = await listSSHFiles(sshSessionId, "/");
const rootFiles = response.files || []; const rootFiles = (response.files || []) as DirectoryItemData[];
const rootFolders = rootFiles.filter( const rootFolders = rootFiles.filter(
(item: any) => item.type === "directory", (item: DirectoryItemData) => item.type === "directory",
); );
const rootTreeItems = rootFolders.map((folder: any) => ({ const rootTreeItems = rootFolders.map((folder: DirectoryItemData) => ({
id: `folder-${folder.name}`, id: `folder-${folder.name}`,
name: folder.name, name: folder.name,
path: folder.path, path: folder.path,
@@ -298,12 +331,12 @@ export function FileManagerSidebar({
try { try {
const subResponse = await listSSHFiles(sshSessionId, folderPath); const subResponse = await listSSHFiles(sshSessionId, folderPath);
const subFiles = subResponse.files || []; const subFiles = (subResponse.files || []) as DirectoryItemData[];
const subFolders = subFiles.filter( const subFolders = subFiles.filter(
(item: any) => item.type === "directory", (item: DirectoryItemData) => item.type === "directory",
); );
const subTreeItems = subFolders.map((folder: any) => ({ const subTreeItems = subFolders.map((folder: DirectoryItemData) => ({
id: `folder-${folder.path.replace(/\//g, "-")}`, id: `folder-${folder.path.replace(/\//g, "-")}`,
name: folder.name, name: folder.name,
path: folder.path, path: folder.path,

View File

@@ -61,23 +61,19 @@ export function DiffViewer({
userId: sshHost.userId, userId: sshHost.userId,
}); });
} }
} catch (error) { } catch {
try { await connectSSH(sshSessionId, {
await connectSSH(sshSessionId, { hostId: sshHost.id,
hostId: sshHost.id, ip: sshHost.ip,
ip: sshHost.ip, port: sshHost.port,
port: sshHost.port, username: sshHost.username,
username: sshHost.username, password: sshHost.password,
password: sshHost.password, sshKey: sshHost.key,
sshKey: sshHost.key, keyPassword: sshHost.keyPassword,
keyPassword: sshHost.keyPassword, authType: sshHost.authType,
authType: sshHost.authType, credentialId: sshHost.credentialId,
credentialId: sshHost.credentialId, userId: sshHost.userId,
userId: sshHost.userId, });
});
} catch (reconnectError) {
throw reconnectError;
}
} }
}; };
@@ -100,15 +96,19 @@ export function DiffViewer({
setContent1(response1.content || ""); setContent1(response1.content || "");
setContent2(response2.content || ""); setContent2(response2.content || "");
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to load files for diff:", error); console.error("Failed to load files for diff:", error);
const errorData = error?.response?.data; const err = error as {
message?: string;
response?: { data?: { tooLarge?: boolean; error?: string } };
};
const errorData = err?.response?.data;
if (errorData?.tooLarge) { if (errorData?.tooLarge) {
setError(t("fileManager.fileTooLarge", { error: errorData.error })); setError(t("fileManager.fileTooLarge", { error: errorData.error }));
} else if ( } else if (
error.message?.includes("connection") || err.message?.includes("connection") ||
error.message?.includes("established") err.message?.includes("established")
) { ) {
setError( setError(
t("fileManager.sshConnectionFailed", { t("fileManager.sshConnectionFailed", {
@@ -121,9 +121,7 @@ export function DiffViewer({
setError( setError(
t("fileManager.loadFileFailed", { t("fileManager.loadFileFailed", {
error: error:
error.message || err.message || errorData?.error || t("fileManager.unknownError"),
errorData?.error ||
t("fileManager.unknownError"),
}), }),
); );
} }
@@ -161,12 +159,13 @@ export function DiffViewer({
t("fileManager.downloadFileSuccess", { name: file.name }), t("fileManager.downloadFileSuccess", { name: file.name }),
); );
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to download file:", error); console.error("Failed to download file:", error);
const err = error as { message?: string };
toast.error( toast.error(
t("fileManager.downloadFileFailed") + t("fileManager.downloadFileFailed") +
": " + ": " +
(error.message || t("fileManager.unknownError")), (err.message || t("fileManager.unknownError")),
); );
} }
}; };

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from "react"; import React, { useState, useRef, useCallback, useEffect } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Minus, Square, X, Maximize2, Minimize2 } from "lucide-react"; import { Minus, X, Maximize2, Minimize2 } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface DraggableWindowProps { interface DraggableWindowProps {

View File

@@ -60,7 +60,6 @@ import {
import { autocompletion, completionKeymap } from "@codemirror/autocomplete"; import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import { PhotoProvider, PhotoView } from "react-photo-view"; import { PhotoProvider, PhotoView } from "react-photo-view";
import "react-photo-view/dist/react-photo-view.css"; import "react-photo-view/dist/react-photo-view.css";
import ReactPlayer from "react-player";
import AudioPlayer from "react-h5-audio-player"; import AudioPlayer from "react-h5-audio-player";
import "react-h5-audio-player/lib/styles.css"; import "react-h5-audio-player/lib/styles.css";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@@ -290,7 +289,7 @@ function getLanguageExtension(filename: string) {
return language ? loadLanguage(language) : null; return language ? loadLanguage(language) : null;
} }
function formatFileSize(bytes?: number, t?: any): string { function formatFileSize(bytes?: number, t?: (key: string) => string): string {
if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size"; if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size";
const sizes = ["B", "KB", "MB", "GB"]; const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024)); const i = Math.floor(Math.log(bytes) / Math.log(1024));
@@ -311,9 +310,7 @@ export function FileViewer({
}: FileViewerProps) { }: FileViewerProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [editedContent, setEditedContent] = useState(content); const [editedContent, setEditedContent] = useState(content);
const [originalContent, setOriginalContent] = useState( const [, setOriginalContent] = useState(savedContent || content);
savedContent || content,
);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [showLargeFileWarning, setShowLargeFileWarning] = useState(false); const [showLargeFileWarning, setShowLargeFileWarning] = useState(false);
const [forceShowAsText, setForceShowAsText] = useState(false); const [forceShowAsText, setForceShowAsText] = useState(false);
@@ -326,7 +323,9 @@ export function FileViewer({
const [pdfScale, setPdfScale] = useState(1.2); const [pdfScale, setPdfScale] = useState(1.2);
const [pdfError, setPdfError] = useState(false); const [pdfError, setPdfError] = useState(false);
const [markdownEditMode, setMarkdownEditMode] = useState(false); const [markdownEditMode, setMarkdownEditMode] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<{
view?: { dispatch: (transaction: unknown) => void };
} | null>(null);
const fileTypeInfo = getFileType(file.name); const fileTypeInfo = getFileType(file.name);
@@ -975,13 +974,7 @@ export function FileViewer({
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
code({ code({ inline, className, children, ...props }) {
node,
inline,
className,
children,
...props
}) {
const match = /language-(\w+)/.exec( const match = /language-(\w+)/.exec(
className || "", className || "",
); );
@@ -1097,7 +1090,7 @@ export function FileViewer({
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
code({ node, inline, className, children, ...props }) { code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || ""); const match = /language-(\w+)/.exec(className || "");
return !inline && match ? ( return !inline && match ? (
<SyntaxHighlighter <SyntaxHighlighter
@@ -1380,8 +1373,7 @@ export function FileViewer({
<div className="rounded-lg overflow-hidden"> <div className="rounded-lg overflow-hidden">
<AudioPlayer <AudioPlayer
src={audioUrl} src={audioUrl}
onLoadedMetadata={(e) => { onLoadedMetadata={() => {
const audio = e.currentTarget;
if (onMediaDimensionsChange) { if (onMediaDimensionsChange) {
onMediaDimensionsChange({ onMediaDimensionsChange({
width: 600, width: 600,

View File

@@ -56,7 +56,7 @@ export function FileWindow({
initialY = 100, initialY = 100,
onFileNotFound, onFileNotFound,
}: FileWindowProps) { }: FileWindowProps) {
const { closeWindow, maximizeWindow, focusWindow, updateWindow, windows } = const { closeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager(); useWindowManager();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -157,28 +157,40 @@ export function FileWindow({
const extension = file.name.split(".").pop()?.toLowerCase(); const extension = file.name.split(".").pop()?.toLowerCase();
setIsEditable(!mediaExtensions.includes(extension || "")); setIsEditable(!mediaExtensions.includes(extension || ""));
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to load file:", error); console.error("Failed to load file:", error);
const errorData = error?.response?.data; const err = error as {
message?: string;
isFileNotFound?: boolean;
response?: {
status?: number;
data?: {
tooLarge?: boolean;
error?: string;
fileNotFound?: boolean;
};
};
};
const errorData = err?.response?.data;
if (errorData?.tooLarge) { if (errorData?.tooLarge) {
toast.error(`File too large: ${errorData.error}`, { toast.error(`File too large: ${errorData.error}`, {
duration: 10000, duration: 10000,
}); });
} else if ( } else if (
error.message?.includes("connection") || err.message?.includes("connection") ||
error.message?.includes("established") err.message?.includes("established")
) { ) {
toast.error( toast.error(
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
); );
} else { } else {
const errorMessage = const errorMessage =
errorData?.error || error.message || "Unknown error"; errorData?.error || err.message || "Unknown error";
const isFileNotFound = const isFileNotFound =
(error as any).isFileNotFound || err.isFileNotFound ||
errorData?.fileNotFound || errorData?.fileNotFound ||
error.response?.status === 404 || err.response?.status === 404 ||
errorMessage.includes("File not found") || errorMessage.includes("File not found") ||
errorMessage.includes("No such file or directory") || errorMessage.includes("No such file or directory") ||
errorMessage.includes("cannot access") || errorMessage.includes("cannot access") ||
@@ -229,10 +241,11 @@ export function FileWindow({
const contentSize = new Blob([fileContent]).size; const contentSize = new Blob([fileContent]).size;
file.size = contentSize; file.size = contentSize;
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to load file content:", error); console.error("Failed to load file content:", error);
const err = error as { message?: string };
toast.error( toast.error(
`${t("fileManager.failedToLoadFile")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.failedToLoadFile")}: ${err.message || t("fileManager.unknownError")}`,
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -258,19 +271,20 @@ export function FileWindow({
} }
toast.success(t("fileManager.fileSavedSuccessfully")); toast.success(t("fileManager.fileSavedSuccessfully"));
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to save file:", error); console.error("Failed to save file:", error);
const err = error as { message?: string };
if ( if (
error.message?.includes("connection") || err.message?.includes("connection") ||
error.message?.includes("established") err.message?.includes("established")
) { ) {
toast.error( toast.error(
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
); );
} else { } else {
toast.error( toast.error(
`${t("fileManager.failedToSaveFile")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.failedToSaveFile")}: ${err.message || t("fileManager.unknownError")}`,
); );
} }
} finally { } finally {
@@ -335,19 +349,20 @@ export function FileWindow({
toast.success(t("fileManager.fileDownloadedSuccessfully")); toast.success(t("fileManager.fileDownloadedSuccessfully"));
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Failed to download file:", error); console.error("Failed to download file:", error);
const err = error as { message?: string };
if ( if (
error.message?.includes("connection") || err.message?.includes("connection") ||
error.message?.includes("established") err.message?.includes("established")
) { ) {
toast.error( toast.error(
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
); );
} else { } else {
toast.error( toast.error(
`Failed to download file: ${error.message || "Unknown error"}`, `Failed to download file: ${err.message || "Unknown error"}`,
); );
} }
} }

View File

@@ -36,11 +36,19 @@ export function TerminalWindow({
executeCommand, executeCommand,
}: TerminalWindowProps) { }: TerminalWindowProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } = const { closeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager(); useWindowManager();
const terminalRef = React.useRef<any>(null); const terminalRef = React.useRef<{ fit?: () => void } | null>(null);
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null); const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
React.useEffect(() => {
return () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, []);
const currentWindow = windows.find((w) => w.id === windowId); const currentWindow = windows.find((w) => w.id === windowId);
if (!currentWindow) { if (!currentWindow) {
return null; return null;
@@ -50,10 +58,6 @@ export function TerminalWindow({
closeWindow(windowId); closeWindow(windowId);
}; };
const handleMinimize = () => {
minimizeWindow(windowId);
};
const handleMaximize = () => { const handleMaximize = () => {
maximizeWindow(windowId); maximizeWindow(windowId);
}; };
@@ -74,14 +78,6 @@ export function TerminalWindow({
}, 100); }, 100);
}; };
React.useEffect(() => {
return () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, []);
const terminalTitle = executeCommand const terminalTitle = executeCommand
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand }) ? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
: initialPath : initialPath

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import React, { useState, useCallback, useRef } from "react"; import React, { useState, useCallback, useRef } from "react";
export interface WindowInstance { export interface WindowInstance {

View File

@@ -15,14 +15,17 @@ import { useTranslation } from "react-i18next";
import type { SSHHost, HostManagerProps } from "../../../types/index"; import type { SSHHost, HostManagerProps } from "../../../types/index";
export function HostManager({ export function HostManager({
onSelectView,
isTopbarOpen, isTopbarOpen,
}: HostManagerProps): React.ReactElement { }: HostManagerProps): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("host_viewer"); const [activeTab, setActiveTab] = useState("host_viewer");
const [editingHost, setEditingHost] = useState<SSHHost | null>(null); const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
const [editingCredential, setEditingCredential] = useState<any | null>(null); const [editingCredential, setEditingCredential] = useState<{
id: number;
name?: string;
username: string;
} | null>(null);
const { state: sidebarState } = useSidebar(); const { state: sidebarState } = useSidebar();
const handleEditHost = (host: SSHHost) => { const handleEditHost = (host: SSHHost) => {
@@ -30,12 +33,16 @@ export function HostManager({
setActiveTab("add_host"); setActiveTab("add_host");
}; };
const handleFormSubmit = (updatedHost?: SSHHost) => { const handleFormSubmit = () => {
setEditingHost(null); setEditingHost(null);
setActiveTab("host_viewer"); setActiveTab("host_viewer");
}; };
const handleEditCredential = (credential: any) => { const handleEditCredential = (credential: {
id: number;
name?: string;
username: string;
}) => {
setEditingCredential(credential); setEditingCredential(credential);
setActiveTab("add_credential"); setActiveTab("add_credential");
}; };

View File

@@ -38,6 +38,9 @@ import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSele
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@codemirror/theme-one-dark"; import { oneDark } from "@codemirror/theme-one-dark";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import type { StatsConfig } from "@/types/stats-widgets";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
import { Checkbox } from "@/components/ui/checkbox.tsx";
interface SSHHost { interface SSHHost {
id: number; id: number;
@@ -57,7 +60,15 @@ interface SSHHost {
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: any[]; tunnelConnections: Array<{
sourcePort: number;
endpointPort: number;
endpointHost: string;
maxRetries: number;
retryInterval: number;
autoStart: boolean;
}>;
statsConfig?: StatsConfig;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
credentialId?: number; credentialId?: number;
@@ -73,11 +84,11 @@ export function HostManagerEditor({
onFormSubmit, onFormSubmit,
}: SSHManagerHostEditorProps) { }: SSHManagerHostEditorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [hosts, setHosts] = useState<SSHHost[]>([]);
const [folders, setFolders] = useState<string[]>([]); const [folders, setFolders] = useState<string[]>([]);
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]); const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
const [credentials, setCredentials] = useState<any[]>([]); const [credentials, setCredentials] = useState<
const [loading, setLoading] = useState(true); Array<{ id: number; username: string; authType: string }>
>([]);
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">( const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
"password", "password",
@@ -92,12 +103,10 @@ export function HostManagerEditor({
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true);
const [hostsData, credentialsData] = await Promise.all([ const [hostsData, credentialsData] = await Promise.all([
getSSHHosts(), getSSHHosts(),
getCredentials(), getCredentials(),
]); ]);
setHosts(hostsData);
setCredentials(credentialsData); setCredentials(credentialsData);
const uniqueFolders = [ const uniqueFolders = [
@@ -118,9 +127,8 @@ export function HostManagerEditor({
setFolders(uniqueFolders); setFolders(uniqueFolders);
setSshConfigurations(uniqueConfigurations); setSshConfigurations(uniqueConfigurations);
} catch (error) { } catch {
} finally { // Failed to load hosts data
setLoading(false);
} }
}; };
@@ -130,9 +138,7 @@ export function HostManagerEditor({
useEffect(() => { useEffect(() => {
const handleCredentialChange = async () => { const handleCredentialChange = async () => {
try { try {
setLoading(true);
const hostsData = await getSSHHosts(); const hostsData = await getSSHHosts();
setHosts(hostsData);
const uniqueFolders = [ const uniqueFolders = [
...new Set( ...new Set(
@@ -152,9 +158,8 @@ export function HostManagerEditor({
setFolders(uniqueFolders); setFolders(uniqueFolders);
setSshConfigurations(uniqueConfigurations); setSshConfigurations(uniqueConfigurations);
} catch (error) { } catch {
} finally { // Failed to reload hosts after credential change
setLoading(false);
} }
}; };
@@ -208,6 +213,32 @@ export function HostManagerEditor({
.default([]), .default([]),
enableFileManager: z.boolean().default(true), enableFileManager: z.boolean().default(true),
defaultPath: z.string().optional(), defaultPath: z.string().optional(),
statsConfig: z
.object({
enabledWidgets: z
.array(
z.enum([
"cpu",
"memory",
"disk",
"network",
"uptime",
"processes",
"system",
]),
)
.default(["cpu", "memory", "disk", "network", "uptime", "system"]),
})
.default({
enabledWidgets: [
"cpu",
"memory",
"disk",
"network",
"uptime",
"system",
],
}),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.authType === "password") { if (data.authType === "password") {
@@ -270,7 +301,7 @@ export function HostManagerEditor({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema) as any, resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: "", name: "",
ip: "", ip: "",
@@ -290,6 +321,7 @@ export function HostManagerEditor({
enableFileManager: true, enableFileManager: true,
defaultPath: "/", defaultPath: "/",
tunnelConnections: [], tunnelConnections: [],
statsConfig: DEFAULT_STATS_CONFIG,
}, },
}); });
@@ -346,6 +378,7 @@ export function HostManagerEditor({
enableFileManager: Boolean(cleanedHost.enableFileManager), enableFileManager: Boolean(cleanedHost.enableFileManager),
defaultPath: cleanedHost.defaultPath || "/", defaultPath: cleanedHost.defaultPath || "/",
tunnelConnections: cleanedHost.tunnelConnections || [], tunnelConnections: cleanedHost.tunnelConnections || [],
statsConfig: cleanedHost.statsConfig || DEFAULT_STATS_CONFIG,
}; };
if (defaultAuthType === "password") { if (defaultAuthType === "password") {
@@ -353,7 +386,17 @@ export function HostManagerEditor({
} else if (defaultAuthType === "key") { } else if (defaultAuthType === "key") {
formData.key = editingHost.id ? "existing_key" : editingHost.key; formData.key = editingHost.id ? "existing_key" : editingHost.key;
formData.keyPassword = cleanedHost.keyPassword || ""; formData.keyPassword = cleanedHost.keyPassword || "";
formData.keyType = (cleanedHost.keyType as any) || "auto"; formData.keyType =
(cleanedHost.keyType as
| "auto"
| "ssh-rsa"
| "ssh-ed25519"
| "ecdsa-sha2-nistp256"
| "ecdsa-sha2-nistp384"
| "ecdsa-sha2-nistp521"
| "ssh-dss"
| "ssh-rsa-sha2-256"
| "ssh-rsa-sha2-512") || "auto";
} else if (defaultAuthType === "credential") { } else if (defaultAuthType === "credential") {
formData.credentialId = formData.credentialId =
cleanedHost.credentialId || "existing_credential"; cleanedHost.credentialId || "existing_credential";
@@ -381,6 +424,7 @@ export function HostManagerEditor({
enableFileManager: true, enableFileManager: true,
defaultPath: "/", defaultPath: "/",
tunnelConnections: [], tunnelConnections: [],
statsConfig: DEFAULT_STATS_CONFIG,
}; };
form.reset(defaultFormData); form.reset(defaultFormData);
@@ -405,7 +449,7 @@ export function HostManagerEditor({
data.name = `${data.username}@${data.ip}`; data.name = `${data.username}@${data.ip}`;
} }
const submitData: any = { const submitData: Record<string, unknown> = {
name: data.name, name: data.name,
ip: data.ip, ip: data.ip,
port: data.port, port: data.port,
@@ -419,6 +463,7 @@ export function HostManagerEditor({
enableFileManager: Boolean(data.enableFileManager), enableFileManager: Boolean(data.enableFileManager),
defaultPath: data.defaultPath || "/", defaultPath: data.defaultPath || "/",
tunnelConnections: data.tunnelConnections || [], tunnelConnections: data.tunnelConnections || [],
statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG,
}; };
submitData.credentialId = null; submitData.credentialId = null;
@@ -497,7 +542,7 @@ export function HostManagerEditor({
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
form.reset(); form.reset();
} catch (error) { } catch {
toast.error(t("hosts.failedToSaveHost")); toast.error(t("hosts.failedToSaveHost"));
} finally { } finally {
isSubmittingRef.current = false; isSubmittingRef.current = false;
@@ -667,6 +712,9 @@ export function HostManagerEditor({
<TabsTrigger value="file_manager"> <TabsTrigger value="file_manager">
{t("hosts.fileManager")} {t("hosts.fileManager")}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="statistics">
{t("hosts.statistics")}
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="general" className="pt-2"> <TabsContent value="general" className="pt-2">
<FormLabel className="mb-3 font-bold"> <FormLabel className="mb-3 font-bold">
@@ -1531,6 +1579,65 @@ export function HostManagerEditor({
</div> </div>
)} )}
</TabsContent> </TabsContent>
<TabsContent value="statistics">
<FormField
control={form.control}
name="statsConfig.enabledWidgets"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.enabledWidgets")}</FormLabel>
<FormDescription>
{t("hosts.enabledWidgetsDesc")}
</FormDescription>
<div className="space-y-3 mt-3">
{(
[
"cpu",
"memory",
"disk",
"network",
"uptime",
"processes",
"system",
] as const
).map((widget) => (
<div
key={widget}
className="flex items-center space-x-2"
>
<Checkbox
checked={field.value?.includes(widget)}
onCheckedChange={(checked) => {
const currentWidgets = field.value || [];
if (checked) {
field.onChange([...currentWidgets, widget]);
} else {
field.onChange(
currentWidgets.filter((w) => w !== widget),
);
}
}}
/>
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{widget === "cpu" && t("serverStats.cpuUsage")}
{widget === "memory" &&
t("serverStats.memoryUsage")}
{widget === "disk" && t("serverStats.diskUsage")}
{widget === "network" &&
t("serverStats.networkInterfaces")}
{widget === "uptime" && t("serverStats.uptime")}
{widget === "processes" &&
t("serverStats.processes")}
{widget === "system" &&
t("serverStats.systemInfo")}
</label>
</div>
))}
</div>
</FormItem>
)}
/>
</TabsContent>
</Tabs> </Tabs>
</ScrollArea> </ScrollArea>
<footer className="shrink-0 w-full pb-0"> <footer className="shrink-0 w-full pb-0">

View File

@@ -106,7 +106,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
setHosts(cleanedHosts); setHosts(cleanedHosts);
setError(null); setError(null);
} catch (err) { } catch {
setError(t("hosts.failedToLoadHosts")); setError(t("hosts.failedToLoadHosts"));
} finally { } finally {
setLoading(false); setLoading(false);
@@ -122,7 +122,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
toast.success(t("hosts.hostDeletedSuccessfully", { name: hostName })); toast.success(t("hosts.hostDeletedSuccessfully", { name: hostName }));
await fetchHosts(); await fetchHosts();
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
} catch (err) { } catch {
toast.error(t("hosts.failedToDeleteHost")); toast.error(t("hosts.failedToDeleteHost"));
} }
}, },
@@ -143,7 +143,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
}); });
confirmWithToast(confirmMessage, () => { confirmWithToast(confirmMessage, () => {
performExport(host, actualAuthType); performExport(host);
}); });
return; return;
} else if (actualAuthType === "password" || actualAuthType === "key") { } else if (actualAuthType === "password" || actualAuthType === "key") {
@@ -152,21 +152,21 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
}); });
confirmWithToast(confirmMessage, () => { confirmWithToast(confirmMessage, () => {
performExport(host, actualAuthType); performExport(host);
}); });
return; return;
} }
performExport(host, actualAuthType); performExport(host);
}; };
const performExport = async (host: SSHHost, actualAuthType: string) => { const performExport = async (host: SSHHost) => {
try { try {
const decryptedHost = await exportSSHHostWithCredentials(host.id); const decryptedHost = await exportSSHHostWithCredentials(host.id);
const cleanExportData = Object.fromEntries( const cleanExportData = Object.fromEntries(
Object.entries(decryptedHost).filter( Object.entries(decryptedHost).filter(
([_, value]) => value !== undefined, ([, value]) => value !== undefined,
), ),
); );
@@ -185,7 +185,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
toast.success( toast.success(
`Exported host configuration for ${host.name || host.username}@${host.ip}`, `Exported host configuration for ${host.name || host.username}@${host.ip}`,
); );
} catch (error) { } catch {
toast.error(t("hosts.failedToExportHost")); toast.error(t("hosts.failedToExportHost"));
} }
}; };
@@ -222,7 +222,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
); );
await fetchHosts(); await fetchHosts();
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
} catch (err) { } catch {
toast.error(t("hosts.failedToRemoveFromFolder")); toast.error(t("hosts.failedToRemoveFromFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);
@@ -251,7 +251,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
setEditingFolder(null); setEditingFolder(null);
setEditingFolderName(""); setEditingFolderName("");
} catch (err) { } catch {
toast.error(t("hosts.failedToRenameFolder")); toast.error(t("hosts.failedToRenameFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);
@@ -291,7 +291,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
setDragOverFolder(folderName); setDragOverFolder(folderName);
}; };
const handleDragLeave = (e: React.DragEvent) => { const handleDragLeave = () => {
dragCounter.current--; dragCounter.current--;
if (dragCounter.current === 0) { if (dragCounter.current === 0) {
setDragOverFolder(null); setDragOverFolder(null);
@@ -325,7 +325,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
); );
await fetchHosts(); await fetchHosts();
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
} catch (err) { } catch {
toast.error(t("hosts.failedToMoveToFolder")); toast.error(t("hosts.failedToMoveToFolder"));
} finally { } finally {
setOperationLoading(false); setOperationLoading(false);

View File

@@ -3,8 +3,6 @@ import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status"; import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
import { Separator } from "@/components/ui/separator.tsx"; import { Separator } from "@/components/ui/separator.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { Progress } from "@/components/ui/progress.tsx";
import { Cpu, HardDrive, MemoryStick } from "lucide-react";
import { Tunnel } from "@/ui/Desktop/Apps/Tunnel/Tunnel.tsx"; import { Tunnel } from "@/ui/Desktop/Apps/Tunnel/Tunnel.tsx";
import { import {
getServerStatusById, getServerStatusById,
@@ -14,9 +12,43 @@ import {
import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import {
type WidgetType,
type StatsConfig,
DEFAULT_STATS_CONFIG,
} from "@/types/stats-widgets";
import {
CpuWidget,
MemoryWidget,
DiskWidget,
NetworkWidget,
UptimeWidget,
ProcessesWidget,
SystemWidget,
} from "./widgets";
interface HostConfig {
id: number;
name: string;
ip: string;
username: string;
folder?: string;
enableFileManager?: boolean;
tunnelConnections?: unknown[];
statsConfig?: string | StatsConfig;
[key: string]: unknown;
}
interface TabData {
id: number;
type: string;
title?: string;
hostConfig?: HostConfig;
[key: string]: unknown;
}
interface ServerProps { interface ServerProps {
hostConfig?: any; hostConfig?: HostConfig;
title?: string; title?: string;
isVisible?: boolean; isVisible?: boolean;
isTopbarOpen?: boolean; isTopbarOpen?: boolean;
@@ -32,20 +64,80 @@ export function Server({
}: ServerProps): React.ReactElement { }: ServerProps): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const { state: sidebarState } = useSidebar(); const { state: sidebarState } = useSidebar();
const { addTab, tabs } = useTabs() as any; const { addTab, tabs } = useTabs() as {
addTab: (tab: { type: string; [key: string]: unknown }) => number;
tabs: TabData[];
};
const [serverStatus, setServerStatus] = React.useState<"online" | "offline">( const [serverStatus, setServerStatus] = React.useState<"online" | "offline">(
"offline", "offline",
); );
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null); const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
const [metricsHistory, setMetricsHistory] = React.useState<ServerMetrics[]>(
[],
);
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false); const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false);
const [isRefreshing, setIsRefreshing] = React.useState(false); const [isRefreshing, setIsRefreshing] = React.useState(false);
const [showStatsUI, setShowStatsUI] = React.useState(true); const [showStatsUI, setShowStatsUI] = React.useState(true);
const enabledWidgets = React.useMemo((): WidgetType[] => {
if (!currentHostConfig?.statsConfig) {
return DEFAULT_STATS_CONFIG.enabledWidgets;
}
try {
const parsed =
typeof currentHostConfig.statsConfig === "string"
? JSON.parse(currentHostConfig.statsConfig)
: currentHostConfig.statsConfig;
return parsed?.enabledWidgets || DEFAULT_STATS_CONFIG.enabledWidgets;
} catch (error) {
console.error("Failed to parse statsConfig:", error);
return DEFAULT_STATS_CONFIG.enabledWidgets;
}
}, [currentHostConfig?.statsConfig]);
React.useEffect(() => { React.useEffect(() => {
setCurrentHostConfig(hostConfig); setCurrentHostConfig(hostConfig);
}, [hostConfig]); }, [hostConfig]);
const renderWidget = (widgetType: WidgetType) => {
switch (widgetType) {
case "cpu":
return <CpuWidget metrics={metrics} metricsHistory={metricsHistory} />;
case "memory":
return (
<MemoryWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "disk":
return <DiskWidget metrics={metrics} metricsHistory={metricsHistory} />;
case "network":
return (
<NetworkWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "uptime":
return (
<UptimeWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "processes":
return (
<ProcessesWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "system":
return (
<SystemWidget metrics={metrics} metricsHistory={metricsHistory} />
);
default:
return null;
}
};
React.useEffect(() => { React.useEffect(() => {
const fetchLatestHostConfig = async () => { const fetchLatestHostConfig = async () => {
if (hostConfig?.id) { if (hostConfig?.id) {
@@ -56,7 +148,7 @@ export function Server({
if (updatedHost) { if (updatedHost) {
setCurrentHostConfig(updatedHost); setCurrentHostConfig(updatedHost);
} }
} catch (error) { } catch {
toast.error(t("serverStats.failedToFetchHostConfig")); toast.error(t("serverStats.failedToFetchHostConfig"));
} }
} }
@@ -73,7 +165,7 @@ export function Server({
if (updatedHost) { if (updatedHost) {
setCurrentHostConfig(updatedHost); setCurrentHostConfig(updatedHost);
} }
} catch (error) { } catch {
toast.error(t("serverStats.failedToFetchHostConfig")); toast.error(t("serverStats.failedToFetchHostConfig"));
} }
} }
@@ -94,13 +186,16 @@ export function Server({
if (!cancelled) { if (!cancelled) {
setServerStatus(res?.status === "online" ? "online" : "offline"); setServerStatus(res?.status === "online" ? "online" : "offline");
} }
} catch (error: any) { } catch (error: unknown) {
if (!cancelled) { if (!cancelled) {
if (error?.response?.status === 503) { const err = error as {
response?: { status?: number };
};
if (err?.response?.status === 503) {
setServerStatus("offline"); setServerStatus("offline");
} else if (error?.response?.status === 504) { } else if (err?.response?.status === 504) {
setServerStatus("offline"); setServerStatus("offline");
} else if (error?.response?.status === 404) { } else if (err?.response?.status === 404) {
setServerStatus("offline"); setServerStatus("offline");
} else { } else {
setServerStatus("offline"); setServerStatus("offline");
@@ -117,13 +212,30 @@ export function Server({
const data = await getServerMetricsById(currentHostConfig.id); const data = await getServerMetricsById(currentHostConfig.id);
if (!cancelled) { if (!cancelled) {
setMetrics(data); setMetrics(data);
setMetricsHistory((prev) => {
const newHistory = [...prev, data];
// Keep last 20 data points for chart
return newHistory.slice(-20);
});
setShowStatsUI(true); setShowStatsUI(true);
} }
} catch (error) { } catch (error: unknown) {
if (!cancelled) { if (!cancelled) {
setMetrics(null); setMetrics(null);
setShowStatsUI(false); setShowStatsUI(false);
toast.error(t("serverStats.failedToFetchMetrics")); const err = error as {
code?: string;
response?: { status?: number; data?: { error?: string } };
};
if (
err?.code === "TOTP_REQUIRED" ||
(err?.response?.status === 403 &&
err?.response?.data?.error === "TOTP_REQUIRED")
) {
toast.error(t("serverStats.totpUnavailable"));
} else {
toast.error(t("serverStats.failedToFetchMetrics"));
}
} }
} finally { } finally {
if (!cancelled) { if (!cancelled) {
@@ -154,7 +266,7 @@ export function Server({
const isFileManagerAlreadyOpen = React.useMemo(() => { const isFileManagerAlreadyOpen = React.useMemo(() => {
if (!currentHostConfig) return false; if (!currentHostConfig) return false;
return tabs.some( return tabs.some(
(tab: any) => (tab: TabData) =>
tab.type === "file_manager" && tab.type === "file_manager" &&
tab.hostConfig?.id === currentHostConfig.id, tab.hostConfig?.id === currentHostConfig.id,
); );
@@ -209,18 +321,46 @@ export function Server({
); );
setMetrics(data); setMetrics(data);
setShowStatsUI(true); setShowStatsUI(true);
} catch (error: any) { } catch (error: unknown) {
if (error?.response?.status === 503) { const err = error as {
code?: string;
status?: number;
response?: { status?: number; data?: { error?: string } };
};
if (
err?.code === "TOTP_REQUIRED" ||
(err?.response?.status === 403 &&
err?.response?.data?.error === "TOTP_REQUIRED")
) {
toast.error(t("serverStats.totpUnavailable"));
setMetrics(null);
setShowStatsUI(false);
} else if (
err?.response?.status === 503 ||
err?.status === 503
) {
setServerStatus("offline"); setServerStatus("offline");
} else if (error?.response?.status === 504) { setMetrics(null);
setShowStatsUI(false);
} else if (
err?.response?.status === 504 ||
err?.status === 504
) {
setServerStatus("offline"); setServerStatus("offline");
} else if (error?.response?.status === 404) { setMetrics(null);
setShowStatsUI(false);
} else if (
err?.response?.status === 404 ||
err?.status === 404
) {
setServerStatus("offline"); setServerStatus("offline");
setMetrics(null);
setShowStatsUI(false);
} else { } else {
setServerStatus("offline"); setServerStatus("offline");
setMetrics(null);
setShowStatsUI(false);
} }
setMetrics(null);
setShowStatsUI(false);
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@@ -294,154 +434,12 @@ export function Server({
</div> </div>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* CPU Stats */} {enabledWidgets.map((widgetType) => (
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200"> <div key={widgetType} className="h-[280px]">
<div className="flex items-center gap-2 mb-3"> {renderWidget(widgetType)}
<Cpu className="h-5 w-5 text-blue-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.cpuUsage")}
</h3>
</div> </div>
))}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-300">
{(() => {
const pct = metrics?.cpu?.percent;
const cores = metrics?.cpu?.cores;
const pctText =
typeof pct === "number" ? `${pct}%` : "N/A";
const coresText =
typeof cores === "number"
? t("serverStats.cpuCores", { count: cores })
: t("serverStats.naCpus");
return `${pctText} ${t("serverStats.of")} ${coresText}`;
})()}
</span>
</div>
<div className="relative">
<Progress
value={
typeof metrics?.cpu?.percent === "number"
? metrics!.cpu!.percent!
: 0
}
className="h-2"
/>
</div>
<div className="text-xs text-gray-500">
{metrics?.cpu?.load
? `Load: ${metrics.cpu.load[0].toFixed(2)}, ${metrics.cpu.load[1].toFixed(2)}, ${metrics.cpu.load[2].toFixed(2)}`
: "Load: N/A"}
</div>
</div>
</div>
{/* Memory Stats */}
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
<div className="flex items-center gap-2 mb-3">
<MemoryStick className="h-5 w-5 text-green-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.memoryUsage")}
</h3>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-300">
{(() => {
const pct = metrics?.memory?.percent;
const used = metrics?.memory?.usedGiB;
const total = metrics?.memory?.totalGiB;
const pctText =
typeof pct === "number" ? `${pct}%` : "N/A";
const usedText =
typeof used === "number"
? `${used.toFixed(1)} GiB`
: "N/A";
const totalText =
typeof total === "number"
? `${total.toFixed(1)} GiB`
: "N/A";
return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`;
})()}
</span>
</div>
<div className="relative">
<Progress
value={
typeof metrics?.memory?.percent === "number"
? metrics!.memory!.percent!
: 0
}
className="h-2"
/>
</div>
<div className="text-xs text-gray-500">
{(() => {
const used = metrics?.memory?.usedGiB;
const total = metrics?.memory?.totalGiB;
const free =
typeof used === "number" && typeof total === "number"
? (total - used).toFixed(1)
: "N/A";
return `Free: ${free} GiB`;
})()}
</div>
</div>
</div>
{/* Disk Stats */}
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
<div className="flex items-center gap-2 mb-3">
<HardDrive className="h-5 w-5 text-orange-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.rootStorageSpace")}
</h3>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-300">
{(() => {
const pct = metrics?.disk?.percent;
const used = metrics?.disk?.usedHuman;
const total = metrics?.disk?.totalHuman;
const pctText =
typeof pct === "number" ? `${pct}%` : "N/A";
const usedText = used ?? "N/A";
const totalText = total ?? "N/A";
return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`;
})()}
</span>
</div>
<div className="relative">
<Progress
value={
typeof metrics?.disk?.percent === "number"
? metrics!.disk!.percent!
: 0
}
className="h-2"
/>
</div>
<div className="text-xs text-gray-500">
{(() => {
const available = metrics?.disk?.availableHuman;
return available
? `Available: ${available}`
: "Available: N/A";
})()}
</div>
</div>
</div>
</div> </div>
)} )}
</div> </div>
@@ -465,7 +463,7 @@ export function Server({
<p className="px-4 pt-2 pb-2 text-sm text-gray-500"> <p className="px-4 pt-2 pb-2 text-sm text-gray-500">
{t("serverStats.feedbackMessage")}{" "} {t("serverStats.feedbackMessage")}{" "}
<a <a
href="https://github.com/LukeGus/Termix/issues/new" href="https://github.com/Termix-SSH/Termix/issues/new"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-500 hover:underline" className="text-blue-500 hover:underline"

View File

@@ -0,0 +1,102 @@
import React from "react";
import { Cpu } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
import { RechartsPrimitive } from "@/components/ui/chart.tsx";
const {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} = RechartsPrimitive;
interface CpuWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) {
const { t } = useTranslation();
// Prepare chart data
const chartData = React.useMemo(() => {
return metricsHistory.map((m, index) => ({
index,
cpu: m.cpu?.percent || 0,
}));
}, [metricsHistory]);
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Cpu className="h-5 w-5 text-blue-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.cpuUsage")}
</h3>
</div>
<div className="flex flex-col flex-1 min-h-0 gap-2">
<div className="flex items-baseline gap-3 flex-shrink-0">
<div className="text-2xl font-bold text-blue-400">
{typeof metrics?.cpu?.percent === "number"
? `${metrics.cpu.percent}%`
: "N/A"}
</div>
<div className="text-xs text-gray-400">
{typeof metrics?.cpu?.cores === "number"
? t("serverStats.cpuCores", { count: metrics.cpu.cores })
: t("serverStats.naCpus")}
</div>
</div>
<div className="text-xs text-gray-500 flex-shrink-0">
{metrics?.cpu?.load
? t("serverStats.loadAverage", {
avg1: metrics.cpu.load[0].toFixed(2),
avg5: metrics.cpu.load[1].toFixed(2),
avg15: metrics.cpu.load[2].toFixed(2),
})
: t("serverStats.loadAverageNA")}
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="index"
stroke="#9ca3af"
tick={{ fill: "#9ca3af" }}
hide
/>
<YAxis
domain={[0, 100]}
stroke="#9ca3af"
tick={{ fill: "#9ca3af" }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1f2937",
border: "1px solid #374151",
borderRadius: "6px",
color: "#fff",
}}
formatter={(value: number) => [`${value.toFixed(1)}%`, "CPU"]}
/>
<Line
type="monotone"
dataKey="cpu"
stroke="#60a5fa"
strokeWidth={2}
dot={false}
animationDuration={300}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,100 @@
import React from "react";
import { HardDrive } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
import { RechartsPrimitive } from "@/components/ui/chart.tsx";
const { RadialBarChart, RadialBar, PolarAngleAxis, ResponsiveContainer } =
RechartsPrimitive;
interface DiskWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function DiskWidget({ metrics }: DiskWidgetProps) {
const { t } = useTranslation();
// Prepare radial chart data
const radialData = React.useMemo(() => {
const percent = metrics?.disk?.percent || 0;
return [
{
name: "Disk",
value: percent,
fill: "#fb923c",
},
];
}, [metrics]);
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<HardDrive className="h-5 w-5 text-orange-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.diskUsage")}
</h3>
</div>
<div className="flex flex-col flex-1 min-h-0">
<div className="flex-1 min-h-0 flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="60%"
outerRadius="90%"
data={radialData}
startAngle={90}
endAngle={-270}
>
<PolarAngleAxis
type="number"
domain={[0, 100]}
angleAxisId={0}
tick={false}
/>
<RadialBar
background
dataKey="value"
cornerRadius={10}
fill="#fb923c"
/>
<text
x="50%"
y="50%"
textAnchor="middle"
dominantBaseline="middle"
className="text-2xl font-bold fill-orange-400"
>
{typeof metrics?.disk?.percent === "number"
? `${metrics.disk.percent}%`
: "N/A"}
</text>
</RadialBarChart>
</ResponsiveContainer>
</div>
<div className="flex-shrink-0 space-y-1 text-center pb-2">
<div className="text-xs text-gray-400">
{(() => {
const used = metrics?.disk?.usedHuman;
const total = metrics?.disk?.totalHuman;
if (used && total) {
return `${used} / ${total}`;
}
return "N/A";
})()}
</div>
<div className="text-xs text-gray-500">
{(() => {
const available = metrics?.disk?.availableHuman;
return available
? `${t("serverStats.available")}: ${available}`
: `${t("serverStats.available")}: N/A`;
})()}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,118 @@
import React from "react";
import { MemoryStick } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
import { RechartsPrimitive } from "@/components/ui/chart.tsx";
const {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} = RechartsPrimitive;
interface MemoryWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) {
const { t } = useTranslation();
// Prepare chart data
const chartData = React.useMemo(() => {
return metricsHistory.map((m, index) => ({
index,
memory: m.memory?.percent || 0,
}));
}, [metricsHistory]);
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<MemoryStick className="h-5 w-5 text-green-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.memoryUsage")}
</h3>
</div>
<div className="flex flex-col flex-1 min-h-0 gap-2">
<div className="flex items-baseline gap-3 flex-shrink-0">
<div className="text-2xl font-bold text-green-400">
{typeof metrics?.memory?.percent === "number"
? `${metrics.memory.percent}%`
: "N/A"}
</div>
<div className="text-xs text-gray-400">
{(() => {
const used = metrics?.memory?.usedGiB;
const total = metrics?.memory?.totalGiB;
if (typeof used === "number" && typeof total === "number") {
return `${used.toFixed(1)} / ${total.toFixed(1)} GiB`;
}
return "N/A";
})()}
</div>
</div>
<div className="text-xs text-gray-500 flex-shrink-0">
{(() => {
const used = metrics?.memory?.usedGiB;
const total = metrics?.memory?.totalGiB;
const free =
typeof used === "number" && typeof total === "number"
? (total - used).toFixed(1)
: "N/A";
return `${t("serverStats.free")}: ${free} GiB`;
})()}
</div>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#34d399" stopOpacity={0.8} />
<stop offset="95%" stopColor="#34d399" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="index"
stroke="#9ca3af"
tick={{ fill: "#9ca3af" }}
hide
/>
<YAxis
domain={[0, 100]}
stroke="#9ca3af"
tick={{ fill: "#9ca3af" }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1f2937",
border: "1px solid #374151",
borderRadius: "6px",
color: "#fff",
}}
formatter={(value: number) => [
`${value.toFixed(1)}%`,
"Memory",
]}
/>
<Area
type="monotone"
dataKey="memory"
stroke="#34d399"
strokeWidth={2}
fill="url(#memoryGradient)"
animationDuration={300}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}

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