Feature engineering improvements (#376)
* chore: add engineering improvements - Configure Prettier with unified code style rules - Add husky + lint-staged for automated pre-commit checks - Add commitlint to enforce conventional commit messages - Add PR check workflow for CI automation - Auto-format all files with Prettier - Fix TypeScript any types in field-crypto.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: enhance development environment - Add .editorconfig for unified editor settings - Add .nvmrc to specify Node.js version (20) - Add useful npm scripts: format, format:check, lint, lint:fix, type-check * chore: add IDE and Git configuration - Add VS Code workspace settings for consistent development experience - Add VS Code extension recommendations (ESLint, Prettier, EditorConfig) - Add .gitattributes to enforce LF line endings * refactor: clean up unused variables and empty blocks - database.ts: Remove unused variables (authManager, format, HTTPS_PORT, etc.) - database.ts: Fix empty catch blocks with descriptive comments - database.ts: Add eslint-disable for required middleware parameter - db/index.ts: Remove unused variables and fix empty catch blocks - Temporarily remove ESLint from pre-commit to allow incremental fixes Reduced total errors from 947 to 913 (34 fixes) * refactor: clean up unused variables and empty blocks in routes Routes updated: - credentials.ts: Remove 12 unused variables/imports - alerts.ts: Remove 1 unused variable - users.ts: Remove 9 unused variables/imports Changes: - Remove unused imports (NextFunction, jwt, UserCrypto, detectKeyType) - Fix empty catch blocks with descriptive comments - Prefix reserved parameters with underscore - Clean up unused error variables in catch blocks Reduced errors from 913 to 886 (27 fixes) * refactor: clean up unused variables in routes/ssh.ts - Remove unused imports (NextFunction, jwt) - Remove 6 unused variables (result, updateResult, name x3) - All 8 no-unused-vars errors fixed * refactor: clean up unused variables and empty blocks in file-manager.ts - Remove 22 unused variables (linkCount, hostId, userId, content, escapedTempFile, index, code) - Fix 1 empty catch block - Simplify multiple route handlers by removing unused destructured parameters Reduced errors from 878 to 855 (23 fixes) * refactor: clean up unused variables and empty blocks in utils database-migration.ts: - Remove 3 unused variables (encryptedSize, totalOriginalRows, totalMemoryRows) lazy-field-encryption.ts: - Fix 6 empty catch blocks with descriptive comments - Keep error variables where they are used in logging tunnel.ts: - Fix multiple empty catch blocks - Remove empty else blocks - Partially fixed (10/21 issues resolved) Reduced errors from 855 to 833 (22 fixes) * fix: restore error variable in catch block for logging Fix TypeScript error where error variable was removed from catch block but still used in logging statements. The error variable is needed for proper error logging and re-throwing. * fix: clean up tunnel.ts empty blocks and unused variables 移除了 tunnel.ts 中的空块和未使用的变量: - 移除 2 个空 else 块 - 修复 2 个空 if 块并添加注释 - 修复空错误处理器并添加注释 - 将未使用的 err 参数重命名为 _err 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up empty blocks and unused variables in backend utils 修复了后端工具文件中的空块和未使用的变量: - auth-manager.ts: 移除空 else 块 - system-crypto.ts: 修复空 catch 块并添加注释 - starter.ts: 修复空 catch 块并添加注释 - server-stats.ts: 将未使用的 reject 参数重命名为 _reject - credentials.ts: 将 connectionTimeout 从 let 改为 const 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up empty catch blocks in frontend components 修复了前端组件中的空 catch 块: - Tunnel.tsx: 修复空 catch 块并添加注释 - ServerConfig.tsx: 修复空 catch 块并添加注释 - TerminalKeyboard.tsx: 修复空 catch 块并添加注释 - system-crypto.ts: 修复遗漏的空 catch 块 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up empty catch blocks in backend utilities 修复了后端工具文件中的 10 个空 catch 块: - system-crypto.ts: 修复 1 个空 catch 块 - server-stats.ts: 修复 4 个空 catch 块 - auto-ssl-setup.ts: 修复 1 个空 catch 块 - ssh-key-utils.ts: 修复 4 个空 catch 块 所有空块都添加了描述性注释说明为何忽略错误。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up empty catch blocks in UI hooks and components 修复了 5 个 UI 组件和 hooks 中的空 catch 块: - useDragToSystemDesktop.ts: 修复 2 个空 catch 块 - HomepageAuth.tsx: 修复 1 个空 catch 块 - HostManagerEditor.tsx: 修复 2 个空 catch 块 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up empty blocks in file manager and credential editor 修复了 5 个空块: - FileManagerGrid.tsx: 移除 1 个空 else 块和 1 个空 if 块 - CredentialEditor.tsx: 修复 1 个空 catch 块,移除 2 个空 if/else 块 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: clean up all empty catch blocks in Terminal components 修复了 Terminal 组件中的所有 8 个空 catch 块: - Desktop/Apps/Terminal/Terminal.tsx: 修复 5 个空 catch 块 - Mobile/Apps/Terminal/Terminal.tsx: 修复 3 个空 catch 块 所有空块都添加了描述性注释。这是空块修复的最后一批。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove useless try/catch wrappers 移除了 3 个无用的 try/catch 包装器: - users.ts: 移除只重新抛出错误的外层 try/catch - FileManager.tsx: 移除只重新抛出错误的内层 try/catch - DiffViewer.tsx: 移除只重新抛出错误的内层 try/catch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove unused imports and mark unused parameters 移除了未使用的导入和标记未使用的参数: - auto-ssl-setup.ts: 移除未使用的 crypto 导入 - user-crypto.ts: 移除未使用的 users 导入 - user-data-import.ts: 移除未使用的 nanoid 导入 - simple-db-ops.ts: 标记未使用的 userId 和 tableName 参数 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove unnecessary escape characters in regex patterns 移除了正则表达式中不必要的转义字符: - users.ts: 修复 5 个 \/ 不必要的转义 - TabContext.tsx: 修复 1 个 \/ 不必要的转义 在字符串形式的正则表达式中,/ 不需要转义。 --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit was merged in pull request #376.
This commit is contained in:
21
.commitlintrc.json
Normal file
21
.commitlintrc.json
Normal 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
20
.editorconfig
Normal 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
36
.gitattributes
vendored
Normal 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
|
||||||
34
.github/workflows/pr-check.yml
vendored
Normal file
34
.github/workflows/pr-check.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- 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
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no -- commitlint --edit $1
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
@@ -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
|
||||||
|
|||||||
10
.prettierrc
10
.prettierrc
@@ -1 +1,9 @@
|
|||||||
{}
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"editorconfig.editorconfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
1483
package-lock.json
generated
1483
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -8,6 +8,11 @@
|
|||||||
"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",
|
||||||
@@ -20,7 +25,8 @@
|
|||||||
"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",
|
||||||
"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",
|
||||||
@@ -105,6 +111,8 @@
|
|||||||
"zod": "^4.0.5"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^20.1.0",
|
||||||
|
"@commitlint/config-conventional": "^20.0.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 +131,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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
|
|||||||
localVersion = foundVersion;
|
localVersion = foundVersion;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,7 +374,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 },
|
||||||
@@ -419,8 +418,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",
|
||||||
});
|
});
|
||||||
@@ -442,8 +439,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",
|
||||||
});
|
});
|
||||||
@@ -970,7 +965,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) {
|
||||||
@@ -1061,7 +1056,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,7 +1117,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",
|
||||||
);
|
);
|
||||||
@@ -1193,7 +1188,7 @@ app.post(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (tableError) {
|
} catch {
|
||||||
apiLogger.info(`${table} table not found in import file, skipping`);
|
apiLogger.info(`${table} table not found in import file, skipping`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1231,7 +1226,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",
|
||||||
);
|
);
|
||||||
@@ -1272,7 +1267,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 {
|
||||||
@@ -1290,7 +1285,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,
|
||||||
@@ -1316,7 +1311,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,
|
||||||
@@ -1339,11 +1334,7 @@ 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 any).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",
|
||||||
@@ -1420,7 +1411,8 @@ 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",
|
||||||
@@ -1433,7 +1425,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 {
|
||||||
@@ -1446,13 +1437,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",
|
||||||
@@ -1484,13 +1468,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({
|
||||||
|
|||||||
@@ -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)) {
|
||||||
@@ -288,7 +289,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};`);
|
||||||
@@ -487,21 +488,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ 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";
|
||||||
@@ -970,7 +968,7 @@ router.post(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let privateKeyObj;
|
let privateKeyObj;
|
||||||
let parseAttempts = [];
|
const parseAttempts = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
privateKeyObj = crypto.createPrivateKey({
|
privateKeyObj = crypto.createPrivateKey({
|
||||||
@@ -1093,7 +1091,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,
|
||||||
@@ -1119,13 +1119,13 @@ router.post(
|
|||||||
async function deploySSHKeyToHost(
|
async function deploySSHKeyToHost(
|
||||||
hostConfig: any,
|
hostConfig: any,
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
credentialData: any,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_credentialData: any,
|
||||||
): 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 +1158,9 @@ async function deploySSHKeyToHost(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on("data", (data) => {});
|
stream.on("data", () => {
|
||||||
|
// Ignore output
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1175,7 +1177,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 +1206,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 +1233,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 +1275,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 +1303,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);
|
||||||
@@ -1521,7 +1529,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,
|
||||||
@@ -1571,7 +1579,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",
|
||||||
|
|||||||
@@ -9,8 +9,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";
|
||||||
@@ -816,7 +815,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)));
|
||||||
|
|
||||||
@@ -943,7 +942,7 @@ router.delete(
|
|||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).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");
|
||||||
@@ -1063,7 +1062,7 @@ router.delete(
|
|||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).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");
|
||||||
@@ -1183,7 +1182,7 @@ router.delete(
|
|||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const userId = (req as any).userId;
|
const userId = (req as any).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");
|
||||||
@@ -1573,7 +1572,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateResult = await db
|
await db
|
||||||
.update(sshData)
|
.update(sshData)
|
||||||
.set({
|
.set({
|
||||||
autostartPassword: decryptedConfig.password || null,
|
autostartPassword: decryptedConfig.password || null,
|
||||||
@@ -1630,7 +1629,7 @@ router.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db
|
await db
|
||||||
.update(sshData)
|
.update(sshData)
|
||||||
.set({
|
.set({
|
||||||
autostartPassword: null,
|
autostartPassword: null,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import QRCode from "qrcode";
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
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";
|
||||||
import { UserCrypto } from "../../utils/user-crypto.js";
|
|
||||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js";
|
import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js";
|
||||||
|
|
||||||
@@ -29,94 +28,89 @@ async function verifyOIDCToken(
|
|||||||
issuerUrl: string,
|
issuerUrl: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
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 any;
|
||||||
issuerUrl,
|
if (discovery.jwks_uri) {
|
||||||
normalizedIssuerUrl,
|
jwksUrls.unshift(discovery.jwks_uri);
|
||||||
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 {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let jwks: any = null;
|
|
||||||
let jwksUrl: string | null = null;
|
|
||||||
|
|
||||||
for (const url of jwksUrls) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (response.ok) {
|
|
||||||
const jwksData = (await response.json()) as any;
|
|
||||||
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
|
|
||||||
jwks = jwksData;
|
|
||||||
jwksUrl = url;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
authLogger.error(
|
|
||||||
`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (discoveryError) {
|
||||||
if (!jwks) {
|
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jwks: any = null;
|
||||||
|
|
||||||
|
for (const url of jwksUrls) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const jwksData = (await response.json()) as any;
|
||||||
|
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
|
||||||
|
jwks = jwksData;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
authLogger.error(
|
||||||
|
`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-200 response
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jwks) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -125,15 +119,8 @@ function isNonEmptyString(val: any): 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
|
||||||
@@ -451,7 +438,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,
|
||||||
@@ -504,7 +491,7 @@ router.get("/oidc/authorize", async (req, res) => {
|
|||||||
|
|
||||||
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")) {
|
||||||
@@ -606,15 +593,12 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
const tokenData = (await tokenResponse.json()) as any;
|
const tokenData = (await tokenResponse.json()) as any;
|
||||||
|
|
||||||
let userInfo: any = null;
|
let userInfo: any = 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`;
|
||||||
@@ -651,7 +635,8 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
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.split(".");
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
@@ -911,7 +896,7 @@ 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 {
|
||||||
// Continue if setup fails - authenticateUser will handle it
|
// Continue if setup fails - authenticateUser will handle it
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,7 +1600,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 = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ function cleanupSession(sessionId: string) {
|
|||||||
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];
|
||||||
}
|
}
|
||||||
@@ -598,13 +600,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]}`;
|
||||||
@@ -837,7 +838,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) {
|
||||||
@@ -1024,14 +1025,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) {
|
||||||
@@ -1165,8 +1159,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"`;
|
||||||
@@ -1231,13 +1223,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}'`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1320,14 +1310,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) {
|
||||||
@@ -1428,7 +1411,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) {
|
||||||
@@ -1529,7 +1512,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) {
|
||||||
@@ -1631,7 +1614,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) {
|
||||||
@@ -1739,7 +1722,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) {
|
||||||
@@ -2128,7 +2111,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) {
|
||||||
@@ -2165,7 +2148,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" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class SSHConnectionPool {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
const checkAvailable = () => {
|
const checkAvailable = () => {
|
||||||
const available = connections.find((conn) => !conn.inUse);
|
const available = connections.find((conn) => !conn.inUse);
|
||||||
if (available) {
|
if (available) {
|
||||||
@@ -157,7 +157,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;
|
||||||
@@ -177,7 +179,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();
|
||||||
@@ -215,7 +219,9 @@ class RequestQueue {
|
|||||||
if (request) {
|
if (request) {
|
||||||
try {
|
try {
|
||||||
await request();
|
await request();
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// Ignore errors from queued requests
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,7 +877,9 @@ function tcpPing(
|
|||||||
settled = true;
|
settled = true;
|
||||||
try {
|
try {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
} catch {}
|
} catch {
|
||||||
|
// Ignore errors when destroying socket
|
||||||
|
}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,9 +522,7 @@ async function connectSSHTunnel(
|
|||||||
keyType: credential.key_type || credential.keyType,
|
keyType: credential.key_type || credential.keyType,
|
||||||
authMethod: credential.auth_type || credential.authType,
|
authMethod: credential.auth_type || credential.authType,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tunnelLogger.warn("Failed to resolve source credentials from database", {
|
tunnelLogger.warn("Failed to resolve source credentials from database", {
|
||||||
@@ -605,7 +607,6 @@ async function connectSSHTunnel(
|
|||||||
credentialId: tunnelConfig.endpointCredentialId,
|
credentialId: tunnelConfig.endpointCredentialId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tunnelLogger.warn(
|
tunnelLogger.warn(
|
||||||
@@ -631,7 +632,9 @@ async function connectSSHTunnel(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
conn.end();
|
conn.end();
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
activeTunnels.delete(tunnelName);
|
activeTunnels.delete(tunnelName);
|
||||||
|
|
||||||
@@ -771,7 +774,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,12 +828,12 @@ async function connectSSHTunnel(
|
|||||||
});
|
});
|
||||||
|
|
||||||
stream.stdout?.on("data", (data: Buffer) => {
|
stream.stdout?.on("data", (data: Buffer) => {
|
||||||
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();
|
||||||
@@ -1034,7 +1039,6 @@ async function killRemoteTunnelByMarker(
|
|||||||
authMethod: credential.auth_type || credential.authType,
|
authMethod: credential.auth_type || credential.authType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
|
tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
|
||||||
@@ -1122,7 +1126,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 +1154,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 +1187,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) => {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,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, {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class LazyFieldEncryption {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (jsonError) {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,9 @@ export class LazyFieldEncryption {
|
|||||||
legacyFieldName,
|
legacyFieldName,
|
||||||
);
|
);
|
||||||
return decrypted;
|
return decrypted;
|
||||||
} catch (legacyError) {}
|
} catch {
|
||||||
|
// Ignore legacy format errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sensitiveFields = [
|
const sensitiveFields = [
|
||||||
@@ -145,7 +147,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 +168,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,
|
||||||
@@ -253,7 +257,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 +268,7 @@ export class LazyFieldEncryption {
|
|||||||
legacyFieldName,
|
legacyFieldName,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (legacyError) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class SimpleDBOps {
|
|||||||
table: SQLiteTable<any>,
|
table: SQLiteTable<any>,
|
||||||
tableName: TableName,
|
tableName: TableName,
|
||||||
where: any,
|
where: any,
|
||||||
userId: string,
|
_userId: string,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
const result = await getDb().delete(table).where(where).returning();
|
const result = await getDb().delete(table).where(where).returning();
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class SimpleDBOps {
|
|||||||
|
|
||||||
static async selectEncrypted(
|
static async selectEncrypted(
|
||||||
query: any,
|
query: any,
|
||||||
tableName: TableName,
|
_tableName: TableName,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
const results = await query;
|
const results = await query;
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -242,7 +246,9 @@ export function parseSSHKey(
|
|||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { getDb } from "../database/db/index.js";
|
import { getDb } from "../database/db/index.js";
|
||||||
import { settings, users } from "../database/db/schema.js";
|
import { settings } from "../database/db/schema.js";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -636,10 +637,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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -719,28 +719,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);
|
||||||
|
|||||||
@@ -327,7 +327,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 });
|
||||||
@@ -458,8 +457,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],
|
||||||
|
|||||||
@@ -62,22 +62,18 @@ export function DiffViewer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ export function HostManagerEditor({
|
|||||||
|
|
||||||
setFolders(uniqueFolders);
|
setFolders(uniqueFolders);
|
||||||
setSshConfigurations(uniqueConfigurations);
|
setSshConfigurations(uniqueConfigurations);
|
||||||
} catch (error) {
|
} catch {
|
||||||
|
// Failed to load hosts data
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -152,7 +153,8 @@ export function HostManagerEditor({
|
|||||||
|
|
||||||
setFolders(uniqueFolders);
|
setFolders(uniqueFolders);
|
||||||
setSshConfigurations(uniqueConfigurations);
|
setSshConfigurations(uniqueConfigurations);
|
||||||
} catch (error) {
|
} catch {
|
||||||
|
// Failed to reload hosts after credential change
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (terminal && typeof (terminal as any).refresh === "function") {
|
if (terminal && typeof (terminal as any).refresh === "function") {
|
||||||
(terminal as any).refresh(0, terminal.rows - 1);
|
(terminal as any).refresh(0, terminal.rows - 1);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Ignore terminal refresh errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTotpSubmit(code: string) {
|
function handleTotpSubmit(code: string) {
|
||||||
@@ -183,7 +185,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
scheduleNotify(cols, rows);
|
scheduleNotify(cols, rows);
|
||||||
hardRefresh();
|
hardRefresh();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Ignore resize notification errors
|
||||||
|
}
|
||||||
},
|
},
|
||||||
refresh: () => hardRefresh(),
|
refresh: () => hardRefresh(),
|
||||||
}),
|
}),
|
||||||
@@ -505,7 +509,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Clipboard API not available, fallback to textarea method
|
||||||
|
}
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
textarea.value = text;
|
textarea.value = text;
|
||||||
textarea.style.position = "fixed";
|
textarea.style.position = "fixed";
|
||||||
@@ -525,7 +531,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
return await navigator.clipboard.readText();
|
return await navigator.clipboard.readText();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Clipboard read not available or not permitted
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +593,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const pasteText = await readTextFromClipboard();
|
const pasteText = await readTextFromClipboard();
|
||||||
if (pasteText) terminal.paste(pasteText);
|
if (pasteText) terminal.paste(pasteText);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Ignore clipboard operation errors
|
||||||
|
}
|
||||||
};
|
};
|
||||||
element?.addEventListener("contextmenu", handleContextMenu);
|
element?.addEventListener("contextmenu", handleContextMenu);
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fetchTunnelStatuses();
|
await fetchTunnelStatuses();
|
||||||
} catch (err) {
|
} catch {
|
||||||
|
// Ignore tunnel action errors
|
||||||
} finally {
|
} finally {
|
||||||
setTunnelActions((prev) => ({ ...prev, [tunnelName]: false }));
|
setTunnelActions((prev) => ({ ...prev, [tunnelName]: false }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export function ServerConfig({
|
|||||||
setServerUrl(config.serverUrl);
|
setServerUrl(config.serverUrl);
|
||||||
setConnectionStatus("success");
|
setConnectionStatus("success");
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// Ignore config loading errors
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ export function HomepageAuth({
|
|||||||
const clearJWTOnLoad = async () => {
|
const clearJWTOnLoad = async () => {
|
||||||
try {
|
try {
|
||||||
await logoutUser();
|
await logoutUser();
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// Ignore logout errors on initial load
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
clearJWTOnLoad();
|
clearJWTOnLoad();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
}
|
}
|
||||||
const m = t.title.match(
|
const m = t.title.match(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`^${root.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`,
|
`^${root.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (m) {
|
if (m) {
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (terminal && typeof (terminal as any).refresh === "function") {
|
if (terminal && typeof (terminal as any).refresh === "function") {
|
||||||
(terminal as any).refresh(0, terminal.rows - 1);
|
(terminal as any).refresh(0, terminal.rows - 1);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Ignore terminal refresh errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNotify(cols: number, rows: number) {
|
function scheduleNotify(cols: number, rows: number) {
|
||||||
@@ -122,7 +124,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
scheduleNotify(cols, rows);
|
scheduleNotify(cols, rows);
|
||||||
hardRefresh();
|
hardRefresh();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// Ignore resize notification errors
|
||||||
|
}
|
||||||
},
|
},
|
||||||
refresh: () => hardRefresh(),
|
refresh: () => hardRefresh(),
|
||||||
}),
|
}),
|
||||||
@@ -175,7 +179,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// Ignore message parsing errors
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("close", (event) => {
|
ws.addEventListener("close", (event) => {
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ export function TerminalKeyboard({
|
|||||||
if (navigator.vibrate) {
|
if (navigator.vibrate) {
|
||||||
navigator.vibrate(20);
|
navigator.vibrate(20);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// Ignore vibration errors on unsupported devices
|
||||||
|
}
|
||||||
|
|
||||||
onSendInput(input);
|
onSendInput(input);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ export function useDragToSystemDesktop({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// IndexedDB not available or failed to retrieve directory
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +77,9 @@ export function useDragToSystemDesktop({
|
|||||||
store.put({ handle: dirHandle }, "lastSaveDir");
|
store.put({ handle: dirHandle }, "lastSaveDir");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// Failed to save directory handle
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFileSystemAPISupported = () => {
|
const isFileSystemAPISupported = () => {
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ function isDev(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiHost = import.meta.env.VITE_API_HOST || "localhost";
|
const apiHost = import.meta.env.VITE_API_HOST || "localhost";
|
||||||
let apiPort = 30001;
|
let apiPort = 30001;
|
||||||
let configuredServerUrl: string | null = null;
|
let configuredServerUrl: string | null = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user