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:
|
# 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