Feature engineering improvements #376
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:
|
||||
build
|
||||
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",
|
||||
"scripts": {
|
||||
"clean": "npx prettier . --write",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"type-check": "tsc --noEmit",
|
||||
"dev": "vite",
|
||||
"build": "vite build && 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-targz": "npm run build && electron-builder --linux tar.gz",
|
||||
"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": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
@@ -105,6 +111,8 @@
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
@@ -123,9 +131,19 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"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;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,6 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
|
||||
|
||||
app.get("/encryption/status", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const authManager = AuthManager.getInstance();
|
||||
const securityStatus = {
|
||||
initialized: 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) => {
|
||||
try {
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
apiLogger.warn("System JWT secret regenerated via 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) => {
|
||||
try {
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
apiLogger.warn("JWT secret regenerated via API", {
|
||||
operation: "jwt_secret_regenerate_api",
|
||||
});
|
||||
@@ -970,7 +965,7 @@ app.post(
|
||||
try {
|
||||
importDb = new Database(req.file.path, { readonly: true });
|
||||
|
||||
const tables = importDb
|
||||
importDb
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all();
|
||||
} catch (sqliteError) {
|
||||
@@ -1061,7 +1056,7 @@ app.post(
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (tableError) {
|
||||
} catch {
|
||||
apiLogger.info("ssh_data table not found in import file, skipping");
|
||||
}
|
||||
|
||||
@@ -1122,7 +1117,7 @@ app.post(
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (tableError) {
|
||||
} catch {
|
||||
apiLogger.info(
|
||||
"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`);
|
||||
}
|
||||
}
|
||||
@@ -1231,7 +1226,7 @@ app.post(
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (tableError) {
|
||||
} catch {
|
||||
apiLogger.info(
|
||||
"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");
|
||||
}
|
||||
} else {
|
||||
@@ -1290,7 +1285,7 @@ app.post(
|
||||
|
||||
try {
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupError) {
|
||||
} catch {
|
||||
apiLogger.warn("Failed to clean up uploaded file", {
|
||||
operation: "file_cleanup_warning",
|
||||
filePath: req.file.path,
|
||||
@@ -1316,7 +1311,7 @@ app.post(
|
||||
if (req.file?.path && fs.existsSync(req.file.path)) {
|
||||
try {
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupError) {
|
||||
} catch {
|
||||
apiLogger.warn("Failed to clean up uploaded file after error", {
|
||||
operation: "file_cleanup_error",
|
||||
filePath: req.file.path,
|
||||
@@ -1339,11 +1334,7 @@ app.post(
|
||||
app.post("/database/export/preview", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const {
|
||||
format = "encrypted",
|
||||
scope = "user_data",
|
||||
includeCredentials = true,
|
||||
} = req.body;
|
||||
const { scope = "user_data", includeCredentials = true } = req.body;
|
||||
|
||||
const exportData = await UserDataExport.exportUserData(userId, {
|
||||
format: "encrypted",
|
||||
@@ -1420,7 +1411,8 @@ app.use(
|
||||
err: unknown,
|
||||
req: express.Request,
|
||||
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, {
|
||||
operation: "error_handler",
|
||||
@@ -1433,7 +1425,6 @@ app.use(
|
||||
);
|
||||
|
||||
const HTTP_PORT = 30001;
|
||||
const HTTPS_PORT = process.env.SSL_PORT || 8443;
|
||||
|
||||
async function initializeSecurity() {
|
||||
try {
|
||||
@@ -1446,13 +1437,6 @@ async function initializeSecurity() {
|
||||
if (!isValid) {
|
||||
throw new Error("Security system validation failed");
|
||||
}
|
||||
|
||||
const securityStatus = {
|
||||
initialized: true,
|
||||
system: { hasSecret: true, isValid: true },
|
||||
activeSessions: {},
|
||||
activeSessionCount: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to initialize security system", error, {
|
||||
operation: "security_init_error",
|
||||
@@ -1484,13 +1468,17 @@ app.get(
|
||||
if (status.hasUnencryptedDb) {
|
||||
try {
|
||||
unencryptedSize = fs.statSync(dbPath).size;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore file access errors
|
||||
}
|
||||
}
|
||||
|
||||
if (status.hasEncryptedDb) {
|
||||
try {
|
||||
encryptedSize = fs.statSync(encryptedDbPath).size;
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore file access errors
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -23,7 +23,7 @@ const enableFileEncryption = process.env.DB_FILE_ENCRYPTION !== "false";
|
||||
const dbPath = path.join(dataDir, "db.sqlite");
|
||||
const encryptedDbPath = `${dbPath}.encrypted`;
|
||||
|
||||
let actualDbPath = ":memory:";
|
||||
const actualDbPath = ":memory:";
|
||||
let memoryDatabase: Database.Database;
|
||||
let isNewDatabase = false;
|
||||
let sqlite: Database.Database;
|
||||
@@ -31,7 +31,8 @@ let sqlite: Database.Database;
|
||||
async function initializeDatabaseAsync(): Promise<void> {
|
||||
const systemCrypto = SystemCrypto.getInstance();
|
||||
|
||||
const dbKey = await systemCrypto.getDatabaseKey();
|
||||
// Ensure database key is initialized
|
||||
await systemCrypto.getDatabaseKey();
|
||||
if (enableFileEncryption) {
|
||||
try {
|
||||
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
|
||||
@@ -288,7 +289,7 @@ const addColumnIfNotExists = (
|
||||
FROM ${table} LIMIT 1`,
|
||||
)
|
||||
.get();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
try {
|
||||
sqlite.exec(`ALTER TABLE ${table}
|
||||
ADD COLUMN ${column} ${definition};`);
|
||||
@@ -487,21 +488,29 @@ async function cleanupDatabase() {
|
||||
for (const file of files) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(tempDir, file));
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmdirSync(tempDir);
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
process.on("exit", () => {
|
||||
if (sqlite) {
|
||||
try {
|
||||
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" });
|
||||
}
|
||||
|
||||
const result = await db.insert(dismissedAlerts).values({
|
||||
await db.insert(dismissedAlerts).values({
|
||||
userId,
|
||||
alertId,
|
||||
});
|
||||
|
||||
@@ -2,15 +2,13 @@ import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js";
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { Request, Response } from "express";
|
||||
import { authLogger } from "../../utils/logger.js";
|
||||
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import {
|
||||
parseSSHKey,
|
||||
parsePublicKey,
|
||||
detectKeyType,
|
||||
validateKeyPair,
|
||||
} from "../../utils/ssh-key-utils.js";
|
||||
import crypto from "crypto";
|
||||
@@ -970,7 +968,7 @@ router.post(
|
||||
|
||||
try {
|
||||
let privateKeyObj;
|
||||
let parseAttempts = [];
|
||||
const parseAttempts = [];
|
||||
|
||||
try {
|
||||
privateKeyObj = crypto.createPrivateKey({
|
||||
@@ -1093,7 +1091,9 @@ router.post(
|
||||
finalPublicKey = `${keyType} ${base64Data}`;
|
||||
formatType = "ssh";
|
||||
}
|
||||
} catch (sshError) {}
|
||||
} catch {
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
@@ -1119,13 +1119,13 @@ router.post(
|
||||
async function deploySSHKeyToHost(
|
||||
hostConfig: any,
|
||||
publicKey: string,
|
||||
credentialData: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_credentialData: any,
|
||||
): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
let connectionTimeout: NodeJS.Timeout;
|
||||
|
||||
connectionTimeout = setTimeout(() => {
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
conn.destroy();
|
||||
resolve({ success: false, error: "Connection timeout" });
|
||||
}, 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) {
|
||||
actualPublicKey = parsed.data;
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
const keyParts = actualPublicKey.trim().split(" ");
|
||||
if (keyParts.length < 2) {
|
||||
@@ -1202,7 +1206,7 @@ async function deploySSHKeyToHost(
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
stream.on("close", (code) => {
|
||||
stream.on("close", () => {
|
||||
clearTimeout(checkTimeout);
|
||||
const exists = output.trim() === "0";
|
||||
resolveCheck(exists);
|
||||
@@ -1229,7 +1233,9 @@ async function deploySSHKeyToHost(
|
||||
if (parsed.data) {
|
||||
actualPublicKey = parsed.data;
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
const escapedKey = actualPublicKey
|
||||
.replace(/\\/g, "\\\\")
|
||||
@@ -1269,7 +1275,9 @@ async function deploySSHKeyToHost(
|
||||
if (parsed.data) {
|
||||
actualPublicKey = parsed.data;
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
const keyParts = actualPublicKey.trim().split(" ");
|
||||
if (keyParts.length < 2) {
|
||||
@@ -1295,7 +1303,7 @@ async function deploySSHKeyToHost(
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
stream.on("close", (code) => {
|
||||
stream.on("close", () => {
|
||||
clearTimeout(verifyTimeout);
|
||||
const verified = output.trim() === "0";
|
||||
resolveVerify(verified);
|
||||
@@ -1521,7 +1529,7 @@ router.post(
|
||||
|
||||
const hostData = targetHost[0];
|
||||
|
||||
let hostConfig = {
|
||||
const hostConfig = {
|
||||
ip: hostData.ip,
|
||||
port: hostData.port,
|
||||
username: hostData.username,
|
||||
@@ -1571,7 +1579,7 @@ router.post(
|
||||
error: "Host credential not found",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to resolve host credentials",
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
fileManagerShortcuts,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and, desc, isNotNull, or } from "drizzle-orm";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { Request, Response } from "express";
|
||||
import multer from "multer";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
|
||||
@@ -816,7 +815,7 @@ router.delete(
|
||||
),
|
||||
);
|
||||
|
||||
const result = await db
|
||||
await db
|
||||
.delete(sshData)
|
||||
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
|
||||
|
||||
@@ -943,7 +942,7 @@ router.delete(
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn("Invalid data for recent file deletion");
|
||||
@@ -1063,7 +1062,7 @@ router.delete(
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn("Invalid data for pinned file deletion");
|
||||
@@ -1183,7 +1182,7 @@ router.delete(
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn("Invalid data for shortcut deletion");
|
||||
@@ -1573,7 +1572,7 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
const updateResult = await db
|
||||
await db
|
||||
.update(sshData)
|
||||
.set({
|
||||
autostartPassword: decryptedConfig.password || null,
|
||||
@@ -1630,7 +1629,7 @@ router.delete(
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
await db
|
||||
.update(sshData)
|
||||
.set({
|
||||
autostartPassword: null,
|
||||
|
||||
@@ -18,7 +18,6 @@ import QRCode from "qrcode";
|
||||
import type { Request, Response } from "express";
|
||||
import { authLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { UserCrypto } from "../../utils/user-crypto.js";
|
||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||
import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js";
|
||||
|
||||
@@ -29,94 +28,89 @@ async function verifyOIDCToken(
|
||||
issuerUrl: string,
|
||||
clientId: string,
|
||||
): 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 {
|
||||
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 {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} catch (discoveryError) {
|
||||
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -125,15 +119,8 @@ function isNonEmptyString(val: any): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
const authenticateJWT = authManager.createAuthMiddleware();
|
||||
const requireAdmin = authManager.createAdminMiddleware();
|
||||
const requireDataAccess = authManager.createDataAccessMiddleware();
|
||||
|
||||
// Route: Create traditional user (username/password)
|
||||
// POST /users/create
|
||||
@@ -451,7 +438,7 @@ router.get("/oidc-config", async (req, res) => {
|
||||
} else {
|
||||
config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
|
||||
}
|
||||
} catch (decryptError) {
|
||||
} catch {
|
||||
authLogger.warn("Failed to decrypt OIDC config for admin", {
|
||||
operation: "oidc_config_decrypt_failed",
|
||||
userId,
|
||||
@@ -504,7 +491,7 @@ router.get("/oidc/authorize", async (req, res) => {
|
||||
|
||||
let origin =
|
||||
req.get("Origin") ||
|
||||
req.get("Referer")?.replace(/\/[^\/]*$/, "") ||
|
||||
req.get("Referer")?.replace(/\/[^/]*$/, "") ||
|
||||
"http://localhost:5173";
|
||||
|
||||
if (origin.includes("localhost")) {
|
||||
@@ -606,15 +593,12 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const tokenData = (await tokenResponse.json()) as any;
|
||||
|
||||
let userInfo: any = null;
|
||||
let userInfoUrls: string[] = [];
|
||||
const userInfoUrls: string[] = [];
|
||||
|
||||
const normalizedIssuerUrl = config.issuer_url.endsWith("/")
|
||||
? config.issuer_url.slice(0, -1)
|
||||
: config.issuer_url;
|
||||
const baseUrl = normalizedIssuerUrl.replace(
|
||||
/\/application\/o\/[^\/]+$/,
|
||||
"",
|
||||
);
|
||||
const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^/]+$/, "");
|
||||
|
||||
try {
|
||||
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
|
||||
@@ -651,7 +635,8 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
config.issuer_url,
|
||||
config.client_id,
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Fallback to manual decoding
|
||||
try {
|
||||
const parts = tokenData.id_token.split(".");
|
||||
if (parts.length === 3) {
|
||||
@@ -911,7 +896,7 @@ router.post("/login", async (req, res) => {
|
||||
if (kekSalt.length === 0) {
|
||||
await authManager.registerUser(userRecord.id, password);
|
||||
}
|
||||
} catch (setupError) {
|
||||
} catch {
|
||||
// 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
|
||||
? JSON.parse(userRecord.totp_backup_codes)
|
||||
: [];
|
||||
} catch (parseError) {
|
||||
} catch {
|
||||
backupCodes = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,9 @@ function cleanupSession(sessionId: string) {
|
||||
if (session) {
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore connection close errors
|
||||
}
|
||||
clearTimeout(session.timeout);
|
||||
delete sshSessions[sessionId];
|
||||
}
|
||||
@@ -598,13 +600,12 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 9) {
|
||||
const permissions = parts[0];
|
||||
const linkCount = parts[1];
|
||||
const owner = parts[2];
|
||||
const group = parts[3];
|
||||
const size = parseInt(parts[4], 10);
|
||||
|
||||
let dateStr = "";
|
||||
let nameStartIndex = 8;
|
||||
const nameStartIndex = 8;
|
||||
|
||||
if (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) => {
|
||||
const { sessionId, path: filePath, content, hostId, userId } = req.body;
|
||||
const { sessionId, path: filePath, content } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const {
|
||||
sessionId,
|
||||
path: filePath,
|
||||
content,
|
||||
fileName,
|
||||
hostId,
|
||||
userId,
|
||||
} = req.body;
|
||||
const { sessionId, path: filePath, content, fileName } = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
|
||||
if (!sessionId) {
|
||||
@@ -1165,8 +1159,6 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
||||
}
|
||||
|
||||
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 writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
|
||||
@@ -1231,13 +1223,11 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
|
||||
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
||||
|
||||
let writeCommand = `> '${escapedPath}'`;
|
||||
|
||||
chunks.forEach((chunk, index) => {
|
||||
chunks.forEach((chunk) => {
|
||||
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) => {
|
||||
const {
|
||||
sessionId,
|
||||
path: filePath,
|
||||
fileName,
|
||||
content = "",
|
||||
hostId,
|
||||
userId,
|
||||
} = req.body;
|
||||
const { sessionId, path: filePath, fileName } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const { sessionId, path: folderPath, folderName, hostId, userId } = req.body;
|
||||
const { sessionId, path: folderPath, folderName } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const { sessionId, path: itemPath, isDirectory, hostId, userId } = req.body;
|
||||
const { sessionId, path: itemPath, isDirectory } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const { sessionId, oldPath, newName, hostId, userId } = req.body;
|
||||
const { sessionId, oldPath, newName } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const { sessionId, oldPath, newPath, hostId, userId } = req.body;
|
||||
const { sessionId, oldPath, newPath } = req.body;
|
||||
const sshConn = sshSessions[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) => {
|
||||
const { sessionId, filePath, hostId, userId } = req.body;
|
||||
const { sessionId, filePath } = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
|
||||
if (!sshConn || !sshConn.isConnected) {
|
||||
@@ -2165,7 +2148,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
|
||||
checkResult += data.toString();
|
||||
});
|
||||
|
||||
checkStream.on("close", (code) => {
|
||||
checkStream.on("close", () => {
|
||||
if (!checkResult.includes("EXECUTABLE")) {
|
||||
return res.status(400).json({ error: "File is not executable" });
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class SSHConnectionPool {
|
||||
return client;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const checkAvailable = () => {
|
||||
const available = connections.find((conn) => !conn.inUse);
|
||||
if (available) {
|
||||
@@ -157,7 +157,9 @@ class SSHConnectionPool {
|
||||
if (!conn.inUse && now - conn.lastUsed > maxAge) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore errors when closing stale connections
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -177,7 +179,9 @@ class SSHConnectionPool {
|
||||
for (const conn of connections) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore errors when closing connections during cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
this.connections.clear();
|
||||
@@ -215,7 +219,9 @@ class RequestQueue {
|
||||
if (request) {
|
||||
try {
|
||||
await request();
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore errors from queued requests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +877,9 @@ function tcpPing(
|
||||
settled = true;
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore errors when destroying socket
|
||||
}
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
|
||||
@@ -217,7 +217,9 @@ function cleanupTunnelResources(
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
try {
|
||||
verification?.conn.end();
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -282,7 +284,9 @@ function handleDisconnect(
|
||||
const verification = tunnelVerifications.get(tunnelName);
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
verification?.conn.end();
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -518,9 +522,7 @@ async function connectSSHTunnel(
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authMethod: credential.auth_type || credential.authType,
|
||||
};
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn("Failed to resolve source credentials from database", {
|
||||
@@ -605,7 +607,6 @@ async function connectSSHTunnel(
|
||||
credentialId: tunnelConfig.endpointCredentialId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn(
|
||||
@@ -631,7 +632,9 @@ async function connectSSHTunnel(
|
||||
|
||||
try {
|
||||
conn.end();
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
activeTunnels.delete(tunnelName);
|
||||
|
||||
@@ -771,7 +774,9 @@ async function connectSSHTunnel(
|
||||
const verification = tunnelVerifications.get(tunnelName);
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
verification?.conn.end();
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -823,12 +828,12 @@ async function connectSSHTunnel(
|
||||
});
|
||||
|
||||
stream.stdout?.on("data", (data: Buffer) => {
|
||||
const output = data.toString().trim();
|
||||
if (output) {
|
||||
}
|
||||
// Silently consume stdout data
|
||||
});
|
||||
|
||||
stream.on("error", (err: Error) => {});
|
||||
stream.on("error", () => {
|
||||
// Silently consume stream errors
|
||||
});
|
||||
|
||||
stream.stderr.on("data", (data) => {
|
||||
const errorMsg = data.toString().trim();
|
||||
@@ -1034,7 +1039,6 @@ async function killRemoteTunnelByMarker(
|
||||
authMethod: credential.auth_type || credential.authType,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
|
||||
@@ -1122,7 +1126,7 @@ async function killRemoteTunnelByMarker(
|
||||
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`;
|
||||
|
||||
conn.exec(checkCmd, (err, stream) => {
|
||||
conn.exec(checkCmd, (_err, stream) => {
|
||||
let foundProcesses = false;
|
||||
|
||||
stream.on("data", (data) => {
|
||||
@@ -1150,7 +1154,7 @@ async function killRemoteTunnelByMarker(
|
||||
|
||||
function executeNextKillCommand() {
|
||||
if (commandIndex >= killCmds.length) {
|
||||
conn.exec(checkCmd, (err, verifyStream) => {
|
||||
conn.exec(checkCmd, (_err, verifyStream) => {
|
||||
let stillRunning = false;
|
||||
|
||||
verifyStream.on("data", (data) => {
|
||||
@@ -1183,18 +1187,15 @@ async function killRemoteTunnelByMarker(
|
||||
tunnelLogger.warn(
|
||||
`Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
stream.on("close", (code) => {
|
||||
stream.on("close", () => {
|
||||
commandIndex++;
|
||||
executeNextKillCommand();
|
||||
});
|
||||
|
||||
stream.on("data", (data) => {
|
||||
const output = data.toString().trim();
|
||||
if (output) {
|
||||
}
|
||||
stream.on("data", () => {
|
||||
// Silently consume stream data
|
||||
});
|
||||
|
||||
stream.stderr.on("data", (data) => {
|
||||
|
||||
@@ -21,7 +21,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
if (persistentConfig.parsed) {
|
||||
Object.assign(process.env, persistentConfig.parsed);
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore errors if .env file doesn't exist
|
||||
}
|
||||
|
||||
let version = "unknown";
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ class AuthManager {
|
||||
|
||||
if (migrationResult.migrated) {
|
||||
await saveMemoryDatabaseToFile();
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.error("Lazy encryption migration failed", error, {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { execSync } from "child_process";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { systemLogger } from "./logger.js";
|
||||
|
||||
export class AutoSSLSetup {
|
||||
@@ -234,7 +233,9 @@ IP.3 = 0.0.0.0
|
||||
let envContent = "";
|
||||
try {
|
||||
envContent = await fs.readFile(this.ENV_FILE, "utf8");
|
||||
} catch {}
|
||||
} catch {
|
||||
// File doesn't exist yet, will create with SSL config
|
||||
}
|
||||
|
||||
let updatedContent = envContent;
|
||||
let hasChanges = false;
|
||||
|
||||
@@ -55,7 +55,6 @@ export class DatabaseMigration {
|
||||
|
||||
if (hasEncryptedDb && hasUnencryptedDb) {
|
||||
const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;
|
||||
const encryptedSize = fs.statSync(this.encryptedDbPath).size;
|
||||
|
||||
if (unencryptedSize === 0) {
|
||||
needsMigration = false;
|
||||
@@ -168,9 +167,6 @@ export class DatabaseMigration {
|
||||
return false;
|
||||
}
|
||||
|
||||
let totalOriginalRows = 0;
|
||||
let totalMemoryRows = 0;
|
||||
|
||||
for (const table of originalTables) {
|
||||
const originalCount = originalDb
|
||||
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
|
||||
@@ -179,9 +175,6 @@ export class DatabaseMigration {
|
||||
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
|
||||
.get() as { count: number };
|
||||
|
||||
totalOriginalRows += originalCount.count;
|
||||
totalMemoryRows += memoryCount.count;
|
||||
|
||||
if (originalCount.count !== memoryCount.count) {
|
||||
databaseLogger.error(
|
||||
"Row count mismatch for table during migration verification",
|
||||
|
||||
@@ -21,8 +21,9 @@ class FieldCrypto {
|
||||
"totp_secret",
|
||||
"totp_backup_codes",
|
||||
"oidc_identifier",
|
||||
"oidcIdentifier",
|
||||
]),
|
||||
ssh_data: new Set(["password", "key", "key_password"]),
|
||||
ssh_data: new Set(["password", "key", "key_password", "keyPassword"]),
|
||||
ssh_credentials: new Set([
|
||||
"password",
|
||||
"private_key",
|
||||
@@ -47,7 +48,11 @@ class FieldCrypto {
|
||||
);
|
||||
|
||||
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");
|
||||
encrypted += cipher.final("hex");
|
||||
@@ -89,7 +94,7 @@ class FieldCrypto {
|
||||
this.ALGORITHM,
|
||||
fieldKey,
|
||||
Buffer.from(encrypted.iv, "hex"),
|
||||
) as any;
|
||||
) as crypto.DecipherGCM;
|
||||
decipher.setAuthTag(Buffer.from(encrypted.tag, "hex"));
|
||||
|
||||
let decrypted = decipher.update(encrypted.data, "hex", "utf8");
|
||||
|
||||
@@ -39,7 +39,7 @@ export class LazyFieldEncryption {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (jsonError) {
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,9 @@ export class LazyFieldEncryption {
|
||||
legacyFieldName,
|
||||
);
|
||||
return decrypted;
|
||||
} catch (legacyError) {}
|
||||
} catch {
|
||||
// Ignore legacy format errors
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
@@ -145,7 +147,7 @@ export class LazyFieldEncryption {
|
||||
wasPlaintext: false,
|
||||
wasLegacyEncryption: false,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
|
||||
if (legacyFieldName) {
|
||||
try {
|
||||
@@ -166,7 +168,9 @@ export class LazyFieldEncryption {
|
||||
wasPlaintext: false,
|
||||
wasLegacyEncryption: true,
|
||||
};
|
||||
} catch (legacyError) {}
|
||||
} catch {
|
||||
// Ignore legacy format errors
|
||||
}
|
||||
}
|
||||
return {
|
||||
encrypted: fieldValue,
|
||||
@@ -253,7 +257,7 @@ export class LazyFieldEncryption {
|
||||
try {
|
||||
FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName);
|
||||
return false;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
|
||||
if (legacyFieldName) {
|
||||
try {
|
||||
@@ -264,7 +268,7 @@ export class LazyFieldEncryption {
|
||||
legacyFieldName,
|
||||
);
|
||||
return true;
|
||||
} catch (legacyError) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class SimpleDBOps {
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
where: any,
|
||||
userId: string,
|
||||
_userId: string,
|
||||
): Promise<any[]> {
|
||||
const result = await getDb().delete(table).where(where).returning();
|
||||
|
||||
@@ -146,7 +146,7 @@ class SimpleDBOps {
|
||||
|
||||
static async selectEncrypted(
|
||||
query: any,
|
||||
tableName: TableName,
|
||||
_tableName: TableName,
|
||||
): Promise<any[]> {
|
||||
const results = await query;
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ function detectKeyTypeFromContent(keyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Cannot decode key, fallback to length-based detection
|
||||
}
|
||||
|
||||
if (content.length < 800) {
|
||||
return "ssh-ed25519";
|
||||
@@ -140,7 +142,9 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
|
||||
} else if (decodedString.includes("1.3.101.112")) {
|
||||
return "ssh-ed25519";
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Cannot decode key, fallback to length-based detection
|
||||
}
|
||||
|
||||
if (content.length < 400) {
|
||||
return "ssh-ed25519";
|
||||
@@ -242,7 +246,9 @@ export function parseSSHKey(
|
||||
|
||||
useSSH2 = true;
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// SSH2 parsing failed, will use fallback method
|
||||
}
|
||||
}
|
||||
|
||||
if (!useSSH2) {
|
||||
@@ -268,7 +274,9 @@ export function parseSSHKey(
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
} catch (fallbackError) {}
|
||||
} catch {
|
||||
// Fallback parsing also failed
|
||||
}
|
||||
|
||||
return {
|
||||
privateKey: privateKeyData,
|
||||
|
||||
@@ -37,7 +37,9 @@ class SystemCrypto {
|
||||
process.env.JWT_SECRET = jwtMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new secret
|
||||
}
|
||||
|
||||
await this.generateAndGuideUser();
|
||||
} catch (error) {
|
||||
@@ -74,7 +76,9 @@ class SystemCrypto {
|
||||
process.env.DATABASE_KEY = dbKeyMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new key
|
||||
}
|
||||
|
||||
await this.generateAndGuideDatabaseKey();
|
||||
} catch (error) {
|
||||
@@ -111,7 +115,9 @@ class SystemCrypto {
|
||||
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore file read errors, will generate new token
|
||||
}
|
||||
|
||||
await this.generateAndGuideInternalAuthToken();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
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 { databaseLogger } from "./logger.js";
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { eq, and } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import { UserDataExport, type UserExportData } from "./user-data-export.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface ImportOptions {
|
||||
replaceExisting?: boolean;
|
||||
|
||||
@@ -79,7 +79,8 @@ export function CredentialEditor({
|
||||
].sort() as string[];
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Failed to load credentials
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -636,10 +637,6 @@ export function CredentialEditor({
|
||||
form.setValue("key", null);
|
||||
form.setValue("keyPassword", "");
|
||||
form.setValue("keyType", "auto");
|
||||
|
||||
if (newAuthType === "password") {
|
||||
} else if (newAuthType === "key") {
|
||||
}
|
||||
}}
|
||||
className="flex-1 flex flex-col h-full min-h-0"
|
||||
>
|
||||
|
||||
@@ -719,28 +719,24 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
let currentSessionId = sshSessionId;
|
||||
try {
|
||||
const status = await getSSHStatus(currentSessionId);
|
||||
if (!status.connected) {
|
||||
const result = await connectSSH(currentSessionId, {
|
||||
hostId: currentHost.id,
|
||||
host: currentHost.ip,
|
||||
port: currentHost.port,
|
||||
username: currentHost.username,
|
||||
authType: currentHost.authType,
|
||||
password: currentHost.password,
|
||||
key: currentHost.key,
|
||||
keyPassword: currentHost.keyPassword,
|
||||
credentialId: currentHost.credentialId,
|
||||
});
|
||||
const currentSessionId = sshSessionId;
|
||||
const status = await getSSHStatus(currentSessionId);
|
||||
if (!status.connected) {
|
||||
const result = await connectSSH(currentSessionId, {
|
||||
hostId: currentHost.id,
|
||||
host: currentHost.ip,
|
||||
port: currentHost.port,
|
||||
username: currentHost.username,
|
||||
authType: currentHost.authType,
|
||||
password: currentHost.password,
|
||||
key: currentHost.key,
|
||||
keyPassword: currentHost.keyPassword,
|
||||
credentialId: currentHost.credentialId,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(t("fileManager.failedToReconnectSSH"));
|
||||
}
|
||||
if (!result.success) {
|
||||
throw new Error(t("fileManager.failedToReconnectSSH"));
|
||||
}
|
||||
} catch (sessionErr) {
|
||||
throw sessionErr;
|
||||
}
|
||||
|
||||
const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path);
|
||||
|
||||
@@ -327,7 +327,6 @@ export function FileManagerGrid({
|
||||
dragState.files[0].type === "file"
|
||||
) {
|
||||
onFileDiff?.(dragState.files[0], targetFile);
|
||||
} else {
|
||||
}
|
||||
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
@@ -458,8 +457,6 @@ export function FileManagerGrid({
|
||||
type: "external",
|
||||
counter: prev.counter + 1,
|
||||
}));
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[dragState.type],
|
||||
|
||||
@@ -62,22 +62,18 @@ export function DiffViewer({
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
try {
|
||||
await connectSSH(sshSessionId, {
|
||||
hostId: sshHost.id,
|
||||
ip: sshHost.ip,
|
||||
port: sshHost.port,
|
||||
username: sshHost.username,
|
||||
password: sshHost.password,
|
||||
sshKey: sshHost.key,
|
||||
keyPassword: sshHost.keyPassword,
|
||||
authType: sshHost.authType,
|
||||
credentialId: sshHost.credentialId,
|
||||
userId: sshHost.userId,
|
||||
});
|
||||
} catch (reconnectError) {
|
||||
throw reconnectError;
|
||||
}
|
||||
await connectSSH(sshSessionId, {
|
||||
hostId: sshHost.id,
|
||||
ip: sshHost.ip,
|
||||
port: sshHost.port,
|
||||
username: sshHost.username,
|
||||
password: sshHost.password,
|
||||
sshKey: sshHost.key,
|
||||
keyPassword: sshHost.keyPassword,
|
||||
authType: sshHost.authType,
|
||||
credentialId: sshHost.credentialId,
|
||||
userId: sshHost.userId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ export function HostManagerEditor({
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
setSshConfigurations(uniqueConfigurations);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Failed to load hosts data
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -152,7 +153,8 @@ export function HostManagerEditor({
|
||||
|
||||
setFolders(uniqueFolders);
|
||||
setSshConfigurations(uniqueConfigurations);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Failed to reload hosts after credential change
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
if (terminal && typeof (terminal as any).refresh === "function") {
|
||||
(terminal as any).refresh(0, terminal.rows - 1);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
}
|
||||
|
||||
function handleTotpSubmit(code: string) {
|
||||
@@ -183,7 +185,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
@@ -505,7 +509,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Clipboard API not available, fallback to textarea method
|
||||
}
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
@@ -525,7 +531,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
return await navigator.clipboard.readText();
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Clipboard read not available or not permitted
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -585,7 +593,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const pasteText = await readTextFromClipboard();
|
||||
if (pasteText) terminal.paste(pasteText);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Ignore clipboard operation errors
|
||||
}
|
||||
};
|
||||
element?.addEventListener("contextmenu", handleContextMenu);
|
||||
|
||||
|
||||
@@ -191,7 +191,8 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||
}
|
||||
|
||||
await fetchTunnelStatuses();
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Ignore tunnel action errors
|
||||
} finally {
|
||||
setTunnelActions((prev) => ({ ...prev, [tunnelName]: false }));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ export function ServerConfig({
|
||||
setServerUrl(config.serverUrl);
|
||||
setConnectionStatus("success");
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore config loading errors
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
|
||||
@@ -105,7 +105,9 @@ export function HomepageAuth({
|
||||
const clearJWTOnLoad = async () => {
|
||||
try {
|
||||
await logoutUser();
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore logout errors on initial load
|
||||
}
|
||||
};
|
||||
|
||||
clearJWTOnLoad();
|
||||
|
||||
@@ -69,7 +69,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
}
|
||||
const m = t.title.match(
|
||||
new RegExp(
|
||||
`^${root.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`,
|
||||
`^${root.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`,
|
||||
),
|
||||
);
|
||||
if (m) {
|
||||
|
||||
@@ -73,7 +73,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
if (terminal && typeof (terminal as any).refresh === "function") {
|
||||
(terminal as any).refresh(0, terminal.rows - 1);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNotify(cols: number, rows: number) {
|
||||
@@ -122,7 +124,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
@@ -175,7 +179,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
||||
);
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Ignore message parsing errors
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
|
||||
@@ -110,7 +110,9 @@ export function TerminalKeyboard({
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(20);
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {
|
||||
// Ignore vibration errors on unsupported devices
|
||||
}
|
||||
|
||||
onSendInput(input);
|
||||
},
|
||||
|
||||
@@ -59,7 +59,9 @@ export function useDragToSystemDesktop({
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// IndexedDB not available or failed to retrieve directory
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -75,7 +77,9 @@ export function useDragToSystemDesktop({
|
||||
store.put({ handle: dirHandle }, "lastSaveDir");
|
||||
};
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {
|
||||
// Failed to save directory handle
|
||||
}
|
||||
};
|
||||
|
||||
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 configuredServerUrl: string | null = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user