Clean up backend files
This commit is contained in:
@@ -27,12 +27,10 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Set environment variables for native module compilation
|
|
||||||
ENV npm_config_target_platform=linux
|
ENV npm_config_target_platform=linux
|
||||||
ENV npm_config_target_arch=x64
|
ENV npm_config_target_arch=x64
|
||||||
ENV npm_config_target_libc=glibc
|
ENV npm_config_target_libc=glibc
|
||||||
|
|
||||||
# Rebuild native modules for the target platform
|
|
||||||
RUN npm rebuild better-sqlite3 --force
|
RUN npm rebuild better-sqlite3 --force
|
||||||
|
|
||||||
RUN npm run build:backend
|
RUN npm run build:backend
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ ipcMain.handle('get-platform', () => {
|
|||||||
return process.platform;
|
return process.platform;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Server configuration handlers
|
|
||||||
ipcMain.handle('get-server-config', () => {
|
ipcMain.handle('get-server-config', () => {
|
||||||
try {
|
try {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
@@ -114,7 +113,6 @@ ipcMain.handle('save-server-config', (event, config) => {
|
|||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
const configPath = path.join(userDataPath, 'server-config.json');
|
const configPath = path.join(userDataPath, 'server-config.json');
|
||||||
|
|
||||||
// Ensure userData directory exists
|
|
||||||
if (!fs.existsSync(userDataPath)) {
|
if (!fs.existsSync(userDataPath)) {
|
||||||
fs.mkdirSync(userDataPath, {recursive: true});
|
fs.mkdirSync(userDataPath, {recursive: true});
|
||||||
}
|
}
|
||||||
@@ -130,13 +128,10 @@ ipcMain.handle('save-server-config', (event, config) => {
|
|||||||
|
|
||||||
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
||||||
try {
|
try {
|
||||||
// Use Node.js built-in fetch (available in Node 18+) or fallback to https module
|
|
||||||
let fetch;
|
let fetch;
|
||||||
try {
|
try {
|
||||||
// Try to use built-in fetch first (Node 18+)
|
|
||||||
fetch = globalThis.fetch || require('node:fetch');
|
fetch = globalThis.fetch || require('node:fetch');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback to https module for older Node versions
|
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const {URL} = require('url');
|
const {URL} = require('url');
|
||||||
@@ -178,10 +173,8 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the server URL (remove trailing slash)
|
|
||||||
const normalizedServerUrl = serverUrl.replace(/\/$/, '');
|
const normalizedServerUrl = serverUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
// Test the health endpoint specifically - this is required for a valid Termix server
|
|
||||||
const healthUrl = `${normalizedServerUrl}/health`;
|
const healthUrl = `${normalizedServerUrl}/health`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,18 +186,18 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
|
|
||||||
// Reject if response looks like HTML (YouTube, etc.)
|
|
||||||
if (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
if (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
||||||
console.log('Health endpoint returned HTML instead of JSON - not a Termix server');
|
console.log('Health endpoint returned HTML instead of JSON - not a Termix server');
|
||||||
return { success: false, error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.' };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// A valid Termix health check should return JSON with specific structure
|
|
||||||
try {
|
try {
|
||||||
const healthData = JSON.parse(data);
|
const healthData = JSON.parse(data);
|
||||||
// Check if it has the expected Termix health check structure
|
|
||||||
if (healthData && (
|
if (healthData && (
|
||||||
healthData.status === 'ok' || // Termix returns {status: 'ok'}
|
healthData.status === 'ok' ||
|
||||||
healthData.status === 'healthy' ||
|
healthData.status === 'healthy' ||
|
||||||
healthData.healthy === true ||
|
healthData.healthy === true ||
|
||||||
healthData.database === 'connected'
|
healthData.database === 'connected'
|
||||||
@@ -212,7 +205,6 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
return {success: true, status: response.status, testedUrl: healthUrl};
|
return {success: true, status: response.status, testedUrl: healthUrl};
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// If not JSON, reject - Termix health endpoint should return JSON
|
|
||||||
console.log('Health endpoint did not return valid JSON');
|
console.log('Health endpoint did not return valid JSON');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +212,6 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
console.error('Health check failed:', urlError);
|
console.error('Health check failed:', urlError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If health check fails, try version endpoint as fallback
|
|
||||||
try {
|
try {
|
||||||
const versionUrl = `${normalizedServerUrl}/version`;
|
const versionUrl = `${normalizedServerUrl}/version`;
|
||||||
const response = await fetch(versionUrl, {
|
const response = await fetch(versionUrl, {
|
||||||
@@ -231,24 +222,29 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
|
|
||||||
// Reject if response looks like HTML (YouTube, etc.)
|
|
||||||
if (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
if (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
||||||
console.log('Version endpoint returned HTML instead of JSON - not a Termix server');
|
console.log('Version endpoint returned HTML instead of JSON - not a Termix server');
|
||||||
return { success: false, error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.' };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const versionData = JSON.parse(data);
|
const versionData = JSON.parse(data);
|
||||||
// Check if it looks like a Termix version response - must be JSON and contain version-specific fields
|
|
||||||
if (versionData && (
|
if (versionData && (
|
||||||
versionData.status === 'up_to_date' ||
|
versionData.status === 'up_to_date' ||
|
||||||
versionData.status === 'requires_update' ||
|
versionData.status === 'requires_update' ||
|
||||||
(versionData.localVersion && versionData.version && versionData.latest_release)
|
(versionData.localVersion && versionData.version && versionData.latest_release)
|
||||||
)) {
|
)) {
|
||||||
return { success: true, status: response.status, testedUrl: versionUrl, warning: 'Health endpoint not available, but server appears to be running' };
|
return {
|
||||||
|
success: true,
|
||||||
|
status: response.status,
|
||||||
|
testedUrl: versionUrl,
|
||||||
|
warning: 'Health endpoint not available, but server appears to be running'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// If not JSON, reject - Termix version endpoint should return JSON
|
|
||||||
console.log('Version endpoint did not return valid JSON');
|
console.log('Version endpoint did not return valid JSON');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +252,10 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
console.error('Version check failed:', versionError);
|
console.error('Version check failed:', versionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.' };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.'
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {success: false, error: error.message};
|
return {success: false, error: error.message};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,27 @@
|
|||||||
const {contextBridge, ipcRenderer} = require('electron');
|
const {contextBridge, ipcRenderer} = require('electron');
|
||||||
|
|
||||||
console.log('Preload script loaded');
|
|
||||||
|
|
||||||
// Expose protected methods that allow the renderer process to use
|
|
||||||
// the ipcRenderer without exposing the entire object
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
// App info
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
||||||
|
|
||||||
// Server configuration
|
|
||||||
getServerConfig: () => ipcRenderer.invoke('get-server-config'),
|
getServerConfig: () => ipcRenderer.invoke('get-server-config'),
|
||||||
saveServerConfig: (config) => ipcRenderer.invoke('save-server-config', config),
|
saveServerConfig: (config) => ipcRenderer.invoke('save-server-config', config),
|
||||||
testServerConnection: (serverUrl) => ipcRenderer.invoke('test-server-connection', serverUrl),
|
testServerConnection: (serverUrl) => ipcRenderer.invoke('test-server-connection', serverUrl),
|
||||||
|
|
||||||
// File dialogs
|
|
||||||
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
||||||
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
||||||
|
|
||||||
// Update events
|
|
||||||
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
|
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
|
||||||
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
|
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
|
||||||
|
|
||||||
// Utility
|
|
||||||
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
isDev: process.env.NODE_ENV === 'development',
|
isDev: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
// Generic invoke method
|
|
||||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also set the legacy IS_ELECTRON flag for backward compatibility
|
|
||||||
window.IS_ELECTRON = true;
|
window.IS_ELECTRON = true;
|
||||||
|
|
||||||
console.log('electronAPI exposed to window');
|
console.log('electronAPI exposed to window');
|
||||||
|
|||||||
@@ -181,8 +181,6 @@ app.get('/releases/rss', async (req, res) => {
|
|||||||
const per_page = Math.min(parseInt(req.query.per_page as string) || 20, 100);
|
const per_page = Math.min(parseInt(req.query.per_page as string) || 20, 100);
|
||||||
const cacheKey = `releases_rss_${page}_${per_page}`;
|
const cacheKey = `releases_rss_${page}_${per_page}`;
|
||||||
|
|
||||||
// RSS releases requested
|
|
||||||
|
|
||||||
const releasesData = await fetchGitHubAPI(
|
const releasesData = await fetchGitHubAPI(
|
||||||
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
|
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
|
||||||
cacheKey
|
cacheKey
|
||||||
@@ -218,8 +216,6 @@ app.get('/releases/rss', async (req, res) => {
|
|||||||
cache_age: releasesData.cache_age
|
cache_age: releasesData.cache_age
|
||||||
};
|
};
|
||||||
|
|
||||||
// RSS releases generated successfully
|
|
||||||
|
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error('Failed to generate RSS format', error, {operation: 'rss_releases'});
|
databaseLogger.error('Failed to generate RSS format', error, {operation: 'rss_releases'});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
import {sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||||
import {sql} from 'drizzle-orm';
|
import {sql} from 'drizzle-orm';
|
||||||
import { databaseLogger } from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export const users = sqliteTable('users', {
|
export const users = sqliteTable('users', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
@@ -40,12 +39,12 @@ export const sshData = sqliteTable('ssh_data', {
|
|||||||
tags: text('tags'),
|
tags: text('tags'),
|
||||||
pin: integer('pin', {mode: 'boolean'}).notNull().default(false),
|
pin: integer('pin', {mode: 'boolean'}).notNull().default(false),
|
||||||
authType: text('auth_type').notNull(),
|
authType: text('auth_type').notNull(),
|
||||||
// Legacy credential fields - kept for backward compatibility
|
|
||||||
password: text('password'),
|
password: text('password'),
|
||||||
key: text('key', {length: 8192}),
|
key: text('key', {length: 8192}),
|
||||||
keyPassword: text('key_password'),
|
keyPassword: text('key_password'),
|
||||||
keyType: text('key_type'),
|
keyType: text('key_type'),
|
||||||
// New credential management
|
|
||||||
credentialId: integer('credential_id').references(() => sshCredentials.id),
|
credentialId: integer('credential_id').references(() => sshCredentials.id),
|
||||||
enableTerminal: integer('enable_terminal', {mode: 'boolean'}).notNull().default(true),
|
enableTerminal: integer('enable_terminal', {mode: 'boolean'}).notNull().default(true),
|
||||||
enableTunnel: integer('enable_tunnel', {mode: 'boolean'}).notNull().default(true),
|
enableTunnel: integer('enable_tunnel', {mode: 'boolean'}).notNull().default(true),
|
||||||
@@ -90,7 +89,6 @@ export const dismissedAlerts = sqliteTable('dismissed_alerts', {
|
|||||||
dismissedAt: text('dismissed_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
dismissedAt: text('dismissed_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
// SSH Credentials Management Tables
|
|
||||||
export const sshCredentials = sqliteTable('ssh_credentials', {
|
export const sshCredentials = sqliteTable('ssh_credentials', {
|
||||||
id: integer('id').primaryKey({autoIncrement: true}),
|
id: integer('id').primaryKey({autoIncrement: true}),
|
||||||
userId: text('user_id').notNull().references(() => users.id),
|
userId: text('user_id').notNull().references(() => users.id),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {db} from '../db/index.js';
|
|||||||
import {dismissedAlerts} from '../db/schema.js';
|
import {dismissedAlerts} from '../db/schema.js';
|
||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import type {Request, Response, NextFunction} from 'express';
|
|
||||||
import {authLogger} from '../../utils/logger.js';
|
import {authLogger} from '../../utils/logger.js';
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +75,11 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
authLogger.warn('GitHub API returned error status', { operation: 'alerts_fetch', status: response.status, statusText: response.statusText });
|
authLogger.warn('GitHub API returned error status', {
|
||||||
|
operation: 'alerts_fetch',
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText
|
||||||
|
});
|
||||||
throw new Error(`GitHub raw content error: ${response.status} ${response.statusText}`);
|
throw new Error(`GitHub raw content error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +96,10 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
|||||||
alertCache.set(cacheKey, validAlerts);
|
alertCache.set(cacheKey, validAlerts);
|
||||||
return validAlerts;
|
return validAlerts;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.error('Failed to fetch alerts from GitHub', { operation: 'alerts_fetch', error: error instanceof Error ? error.message : 'Unknown error' });
|
authLogger.error('Failed to fetch alerts from GitHub', {
|
||||||
|
operation: 'alerts_fetch',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!isNonEmptyString(userId) || !isNonEmptyString(name) || !isNonEmptyString(username)) {
|
if (!isNonEmptyString(userId) || !isNonEmptyString(name) || !isNonEmptyString(username)) {
|
||||||
authLogger.warn('Invalid credential creation data validation failed', { operation: 'credential_create', userId, hasName: !!name, hasUsername: !!username });
|
authLogger.warn('Invalid credential creation data validation failed', {
|
||||||
|
operation: 'credential_create',
|
||||||
|
userId,
|
||||||
|
hasName: !!name,
|
||||||
|
hasUsername: !!username
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Name and username are required'});
|
return res.status(400).json({error: 'Name and username are required'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +71,21 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (authType === 'password' && !password) {
|
if (authType === 'password' && !password) {
|
||||||
authLogger.warn('Password required for password authentication', { operation: 'credential_create', userId, name, authType });
|
authLogger.warn('Password required for password authentication', {
|
||||||
|
operation: 'credential_create',
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
authType
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Password is required for password authentication'});
|
return res.status(400).json({error: 'Password is required for password authentication'});
|
||||||
}
|
}
|
||||||
if (authType === 'key' && !key) {
|
if (authType === 'key' && !key) {
|
||||||
authLogger.warn('SSH key required for key authentication', { operation: 'credential_create', userId, name, authType });
|
authLogger.warn('SSH key required for key authentication', {
|
||||||
|
operation: 'credential_create',
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
authType
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'SSH key is required for key authentication'});
|
return res.status(400).json({error: 'SSH key is required for key authentication'});
|
||||||
}
|
}
|
||||||
const plainPassword = (authType === 'password' && password) ? password : null;
|
const plainPassword = (authType === 'password' && password) ? password : null;
|
||||||
@@ -107,7 +122,13 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
res.status(201).json(formatCredentialOutput(created));
|
res.status(201).json(formatCredentialOutput(created));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error('Failed to create credential in database', err, { operation: 'credential_create', userId, name, authType, username });
|
authLogger.error('Failed to create credential in database', err, {
|
||||||
|
operation: 'credential_create',
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
authType,
|
||||||
|
username
|
||||||
|
});
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: err instanceof Error ? err.message : 'Failed to create credential'
|
error: err instanceof Error ? err.message : 'Failed to create credential'
|
||||||
});
|
});
|
||||||
@@ -427,14 +448,12 @@ router.post('/:id/apply-to-host/:hostId', authenticateJWT, async (req: Request,
|
|||||||
eq(sshData.userId, userId)
|
eq(sshData.userId, userId)
|
||||||
));
|
));
|
||||||
|
|
||||||
// Record credential usage
|
|
||||||
await db.insert(sshCredentialUsage).values({
|
await db.insert(sshCredentialUsage).values({
|
||||||
credentialId: parseInt(credentialId),
|
credentialId: parseInt(credentialId),
|
||||||
hostId: parseInt(hostId),
|
hostId: parseInt(hostId),
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update credential usage stats
|
|
||||||
await db
|
await db
|
||||||
.update(sshCredentials)
|
.update(sshCredentials)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
|||||||
try {
|
try {
|
||||||
hostData = JSON.parse(req.body.data);
|
hostData = JSON.parse(req.body.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_create', userId, error: err });
|
sshLogger.warn('Invalid JSON data in multipart request', {
|
||||||
|
operation: 'host_create',
|
||||||
|
userId,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid JSON data'});
|
return res.status(400).json({error: 'Invalid JSON data'});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -194,7 +198,14 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
|||||||
|
|
||||||
res.json(resolvedHost);
|
res.json(resolvedHost);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error('Failed to save SSH host to database', err, { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType });
|
sshLogger.error('Failed to save SSH host to database', err, {
|
||||||
|
operation: 'host_create',
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
authType: effectiveAuthType
|
||||||
|
});
|
||||||
res.status(500).json({error: 'Failed to save SSH data'});
|
res.status(500).json({error: 'Failed to save SSH data'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -211,11 +222,20 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
|||||||
try {
|
try {
|
||||||
hostData = JSON.parse(req.body.data);
|
hostData = JSON.parse(req.body.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, error: err });
|
sshLogger.warn('Invalid JSON data in multipart request', {
|
||||||
|
operation: 'host_update',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid JSON data'});
|
return res.status(400).json({error: 'Invalid JSON data'});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sshLogger.warn('Missing data field in multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId });
|
sshLogger.warn('Missing data field in multipart request', {
|
||||||
|
operation: 'host_update',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Missing data field'});
|
return res.status(400).json({error: 'Missing data field'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +328,11 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
|||||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||||
|
|
||||||
if (updatedHosts.length === 0) {
|
if (updatedHosts.length === 0) {
|
||||||
sshLogger.warn('Updated host not found after update', { operation: 'host_update', hostId: parseInt(hostId), userId });
|
sshLogger.warn('Updated host not found after update', {
|
||||||
|
operation: 'host_update',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
return res.status(404).json({error: 'Host not found after update'});
|
return res.status(404).json({error: 'Host not found after update'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +361,15 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
|||||||
|
|
||||||
res.json(resolvedHost);
|
res.json(resolvedHost);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error('Failed to update SSH host in database', err, { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType });
|
sshLogger.error('Failed to update SSH host in database', err, {
|
||||||
|
operation: 'host_update',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
authType: effectiveAuthType
|
||||||
|
});
|
||||||
res.status(500).json({error: 'Failed to update SSH data'});
|
res.status(500).json({error: 'Failed to update SSH data'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -384,7 +416,11 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response)
|
|||||||
const userId = (req as any).userId;
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
if (!isNonEmptyString(userId) || !hostId) {
|
if (!isNonEmptyString(userId) || !hostId) {
|
||||||
sshLogger.warn('Invalid userId or hostId for SSH host fetch by ID', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
sshLogger.warn('Invalid userId or hostId for SSH host fetch by ID', {
|
||||||
|
operation: 'host_fetch_by_id',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid userId or hostId'});
|
return res.status(400).json({error: 'Invalid userId or hostId'});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -411,7 +447,11 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response)
|
|||||||
|
|
||||||
res.json(await resolveHostCredentials(result) || result);
|
res.json(await resolveHostCredentials(result) || result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error('Failed to fetch SSH host by ID from database', err, { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
sshLogger.error('Failed to fetch SSH host by ID from database', err, {
|
||||||
|
operation: 'host_fetch_by_id',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
res.status(500).json({error: 'Failed to fetch SSH host'});
|
res.status(500).json({error: 'Failed to fetch SSH host'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -423,7 +463,11 @@ router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Respons
|
|||||||
const hostId = req.params.id;
|
const hostId = req.params.id;
|
||||||
|
|
||||||
if (!isNonEmptyString(userId) || !hostId) {
|
if (!isNonEmptyString(userId) || !hostId) {
|
||||||
sshLogger.warn('Invalid userId or hostId for SSH host delete', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
sshLogger.warn('Invalid userId or hostId for SSH host delete', {
|
||||||
|
operation: 'host_delete',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid userId or id'});
|
return res.status(400).json({error: 'Invalid userId or id'});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -433,7 +477,11 @@ router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Respons
|
|||||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||||
|
|
||||||
if (hostToDelete.length === 0) {
|
if (hostToDelete.length === 0) {
|
||||||
sshLogger.warn('SSH host not found for deletion', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
sshLogger.warn('SSH host not found for deletion', {
|
||||||
|
operation: 'host_delete',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
return res.status(404).json({error: 'SSH host not found'});
|
return res.status(404).json({error: 'SSH host not found'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +500,11 @@ router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Respons
|
|||||||
|
|
||||||
res.json({message: 'SSH host deleted'});
|
res.json({message: 'SSH host deleted'});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error('Failed to delete SSH host from database', err, { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
sshLogger.error('Failed to delete SSH host from database', err, {
|
||||||
|
operation: 'host_delete',
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId
|
||||||
|
});
|
||||||
res.status(500).json({error: 'Failed to delete SSH host'});
|
res.status(500).json({error: 'Failed to delete SSH host'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -500,7 +552,6 @@ router.post('/file_manager/recent', authenticateJWT, async (req: Request, res: R
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file already exists
|
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
.from(fileManagerRecent)
|
.from(fileManagerRecent)
|
||||||
@@ -511,13 +562,11 @@ router.post('/file_manager/recent', authenticateJWT, async (req: Request, res: R
|
|||||||
));
|
));
|
||||||
|
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
// Update last opened time
|
|
||||||
await db
|
await db
|
||||||
.update(fileManagerRecent)
|
.update(fileManagerRecent)
|
||||||
.set({lastOpened: new Date().toISOString()})
|
.set({lastOpened: new Date().toISOString()})
|
||||||
.where(eq(fileManagerRecent.id, existing[0].id));
|
.where(eq(fileManagerRecent.id, existing[0].id));
|
||||||
} else {
|
} else {
|
||||||
// Insert new record
|
|
||||||
await db.insert(fileManagerRecent).values({
|
await db.insert(fileManagerRecent).values({
|
||||||
userId,
|
userId,
|
||||||
hostId,
|
hostId,
|
||||||
@@ -603,7 +652,6 @@ router.post('/file_manager/pinned', authenticateJWT, async (req: Request, res: R
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if file already exists
|
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
.from(fileManagerPinned)
|
.from(fileManagerPinned)
|
||||||
@@ -701,7 +749,6 @@ router.post('/file_manager/shortcuts', authenticateJWT, async (req: Request, res
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if shortcut already exists
|
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
.from(fileManagerShortcuts)
|
.from(fileManagerShortcuts)
|
||||||
@@ -804,7 +851,6 @@ router.put('/folders/rename', authenticateJWT, async (req: Request, res: Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update all hosts with the old folder name
|
|
||||||
const updatedHosts = await db
|
const updatedHosts = await db
|
||||||
.update(sshData)
|
.update(sshData)
|
||||||
.set({
|
.set({
|
||||||
@@ -817,7 +863,6 @@ router.put('/folders/rename', authenticateJWT, async (req: Request, res: Respons
|
|||||||
))
|
))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Update all credentials with the old folder name
|
|
||||||
const updatedCredentials = await db
|
const updatedCredentials = await db
|
||||||
.update(sshCredentials)
|
.update(sshCredentials)
|
||||||
.set({
|
.set({
|
||||||
@@ -865,21 +910,18 @@ router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response)
|
|||||||
const hostData = hosts[i];
|
const hostData = hosts[i];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate required fields
|
|
||||||
if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) {
|
if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
results.errors.push(`Host ${i + 1}: Missing required fields (ip, port, username)`);
|
results.errors.push(`Host ${i + 1}: Missing required fields (ip, port, username)`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate authType
|
|
||||||
if (!['password', 'key', 'credential'].includes(hostData.authType)) {
|
if (!['password', 'key', 'credential'].includes(hostData.authType)) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
results.errors.push(`Host ${i + 1}: Invalid authType. Must be 'password', 'key', or 'credential'`);
|
results.errors.push(`Host ${i + 1}: Invalid authType. Must be 'password', 'key', or 'credential'`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate authentication data based on authType
|
|
||||||
if (hostData.authType === 'password' && !isNonEmptyString(hostData.password)) {
|
if (hostData.authType === 'password' && !isNonEmptyString(hostData.password)) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
results.errors.push(`Host ${i + 1}: Password required for password authentication`);
|
results.errors.push(`Host ${i + 1}: Password required for password authentication`);
|
||||||
@@ -898,7 +940,6 @@ router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare host data for insertion
|
|
||||||
const sshDataObj: any = {
|
const sshDataObj: any = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
name: hostData.name || `${hostData.username}@${hostData.ip}`,
|
name: hostData.name || `${hostData.username}@${hostData.ip}`,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import {db} from '../db/index.js';
|
import {db} from '../db/index.js';
|
||||||
import {users, settings, sshData, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts} from '../db/schema.js';
|
import {
|
||||||
|
users,
|
||||||
|
sshData,
|
||||||
|
fileManagerRecent,
|
||||||
|
fileManagerPinned,
|
||||||
|
fileManagerShortcuts,
|
||||||
|
dismissedAlerts
|
||||||
|
} from '../db/schema.js';
|
||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import {nanoid} from 'nanoid';
|
import {nanoid} from 'nanoid';
|
||||||
@@ -111,7 +118,11 @@ interface JWTPayload {
|
|||||||
function authenticateJWT(req: Request, res: Response, next: NextFunction) {
|
function authenticateJWT(req: Request, res: Response, next: NextFunction) {
|
||||||
const authHeader = req.headers['authorization'];
|
const authHeader = req.headers['authorization'];
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
authLogger.warn('Missing or invalid Authorization header', { operation: 'auth', method: req.method, url: req.url });
|
authLogger.warn('Missing or invalid Authorization header', {
|
||||||
|
operation: 'auth',
|
||||||
|
method: req.method,
|
||||||
|
url: req.url
|
||||||
|
});
|
||||||
return res.status(401).json({error: 'Missing or invalid Authorization header'});
|
return res.status(401).json({error: 'Missing or invalid Authorization header'});
|
||||||
}
|
}
|
||||||
const token = authHeader.split(' ')[1];
|
const token = authHeader.split(' ')[1];
|
||||||
@@ -119,7 +130,6 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) {
|
|||||||
try {
|
try {
|
||||||
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
||||||
(req as any).userId = payload.userId;
|
(req as any).userId = payload.userId;
|
||||||
// JWT authentication successful
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.warn('Invalid or expired token', {operation: 'auth', method: req.method, url: req.url, error: err});
|
authLogger.warn('Invalid or expired token', {operation: 'auth', method: req.method, url: req.url, error: err});
|
||||||
@@ -142,7 +152,11 @@ router.post('/create', async (req, res) => {
|
|||||||
const {username, password} = req.body;
|
const {username, password} = req.body;
|
||||||
|
|
||||||
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
||||||
authLogger.warn('Invalid user creation attempt - missing username or password', { operation: 'user_create', hasUsername: !!username, hasPassword: !!password });
|
authLogger.warn('Invalid user creation attempt - missing username or password', {
|
||||||
|
operation: 'user_create',
|
||||||
|
hasUsername: !!username,
|
||||||
|
hasPassword: !!password
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Username and password are required'});
|
return res.status(400).json({error: 'Username and password are required'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +176,11 @@ router.post('/create', async (req, res) => {
|
|||||||
isFirstUser = ((countResult as any)?.count || 0) === 0;
|
isFirstUser = ((countResult as any)?.count || 0) === 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isFirstUser = true;
|
isFirstUser = true;
|
||||||
authLogger.warn('Failed to check user count, assuming first user', { operation: 'user_create', username, error: e });
|
authLogger.warn('Failed to check user count, assuming first user', {
|
||||||
|
operation: 'user_create',
|
||||||
|
username,
|
||||||
|
error: e
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const saltRounds = parseInt(process.env.SALT || '10', 10);
|
const saltRounds = parseInt(process.env.SALT || '10', 10);
|
||||||
@@ -187,8 +205,17 @@ router.post('/create', async (req, res) => {
|
|||||||
totp_backup_codes: null,
|
totp_backup_codes: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
authLogger.success(`Traditional user created: ${username} (is_admin: ${isFirstUser})`, { operation: 'user_create', username, isAdmin: isFirstUser, userId: id });
|
authLogger.success(`Traditional user created: ${username} (is_admin: ${isFirstUser})`, {
|
||||||
res.json({message: 'User created', is_admin: isFirstUser, toast: {type: 'success', message: `User created: ${username}`}});
|
operation: 'user_create',
|
||||||
|
username,
|
||||||
|
isAdmin: isFirstUser,
|
||||||
|
userId: id
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
message: 'User created',
|
||||||
|
is_admin: isFirstUser,
|
||||||
|
toast: {type: 'success', message: `User created: ${username}`}
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authLogger.error('Failed to create user', err);
|
authLogger.error('Failed to create user', err);
|
||||||
res.status(500).json({error: 'Failed to create user'});
|
res.status(500).json({error: 'Failed to create user'});
|
||||||
@@ -243,7 +270,6 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
|
|||||||
authLogger.info('OIDC configuration disabled', {operation: 'oidc_disable', userId});
|
authLogger.info('OIDC configuration disabled', {operation: 'oidc_disable', userId});
|
||||||
res.json({message: 'OIDC configuration disabled'});
|
res.json({message: 'OIDC configuration disabled'});
|
||||||
} else {
|
} else {
|
||||||
// Enable OIDC by storing the configuration
|
|
||||||
const config = {
|
const config = {
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
@@ -257,7 +283,11 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('oidc_config', ?)").run(JSON.stringify(config));
|
db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('oidc_config', ?)").run(JSON.stringify(config));
|
||||||
authLogger.info('OIDC configuration updated', { operation: 'oidc_update', userId, hasUserinfoUrl: !!userinfo_url });
|
authLogger.info('OIDC configuration updated', {
|
||||||
|
operation: 'oidc_update',
|
||||||
|
userId,
|
||||||
|
hasUserinfoUrl: !!userinfo_url
|
||||||
|
});
|
||||||
res.json({message: 'OIDC configuration updated'});
|
res.json({message: 'OIDC configuration updated'});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -533,8 +563,6 @@ router.get('/oidc/callback', async (req, res) => {
|
|||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.id, id));
|
.where(eq(users.id, id));
|
||||||
|
|
||||||
// OIDC user created - toast notification handled by frontend
|
|
||||||
} else {
|
} else {
|
||||||
await db.update(users)
|
await db.update(users)
|
||||||
.set({username: name})
|
.set({username: name})
|
||||||
@@ -544,8 +572,6 @@ router.get('/oidc/callback', async (req, res) => {
|
|||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.id, user[0].id));
|
.where(eq(users.id, user[0].id));
|
||||||
|
|
||||||
// OIDC user logged in - toast notification handled by frontend
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRecord = user[0];
|
const userRecord = user[0];
|
||||||
@@ -589,7 +615,11 @@ router.post('/login', async (req, res) => {
|
|||||||
const {username, password} = req.body;
|
const {username, password} = req.body;
|
||||||
|
|
||||||
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
|
||||||
authLogger.warn('Invalid traditional login attempt', { operation: 'user_login', hasUsername: !!username, hasPassword: !!password });
|
authLogger.warn('Invalid traditional login attempt', {
|
||||||
|
operation: 'user_login',
|
||||||
|
hasUsername: !!username,
|
||||||
|
hasPassword: !!password
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid username or password'});
|
return res.status(400).json({error: 'Invalid username or password'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,13 +637,21 @@ router.post('/login', async (req, res) => {
|
|||||||
const userRecord = user[0];
|
const userRecord = user[0];
|
||||||
|
|
||||||
if (userRecord.is_oidc) {
|
if (userRecord.is_oidc) {
|
||||||
authLogger.warn('OIDC user attempted traditional login', { operation: 'user_login', username, userId: userRecord.id });
|
authLogger.warn('OIDC user attempted traditional login', {
|
||||||
|
operation: 'user_login',
|
||||||
|
username,
|
||||||
|
userId: userRecord.id
|
||||||
|
});
|
||||||
return res.status(403).json({error: 'This user uses external authentication'});
|
return res.status(403).json({error: 'This user uses external authentication'});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, userRecord.password_hash);
|
const isMatch = await bcrypt.compare(password, userRecord.password_hash);
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
authLogger.warn(`Incorrect password for user: ${username}`, { operation: 'user_login', username, userId: userRecord.id });
|
authLogger.warn(`Incorrect password for user: ${username}`, {
|
||||||
|
operation: 'user_login',
|
||||||
|
username,
|
||||||
|
userId: userRecord.id
|
||||||
|
});
|
||||||
return res.status(401).json({error: 'Incorrect password'});
|
return res.status(401).json({error: 'Incorrect password'});
|
||||||
}
|
}
|
||||||
const jwtSecret = process.env.JWT_SECRET || 'secret';
|
const jwtSecret = process.env.JWT_SECRET || 'secret';
|
||||||
@@ -621,8 +659,6 @@ router.post('/login', async (req, res) => {
|
|||||||
expiresIn: '50d',
|
expiresIn: '50d',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Traditional user logged in - toast notification handled by frontend
|
|
||||||
|
|
||||||
if (userRecord.totp_enabled) {
|
if (userRecord.totp_enabled) {
|
||||||
const tempToken = jwt.sign(
|
const tempToken = jwt.sign(
|
||||||
{userId: userRecord.id, pending_totp: true},
|
{userId: userRecord.id, pending_totp: true},
|
||||||
@@ -886,7 +922,6 @@ router.post('/complete-reset', async (req, res) => {
|
|||||||
const expiresAt = new Date(tempTokenData.expiresAt);
|
const expiresAt = new Date(tempTokenData.expiresAt);
|
||||||
|
|
||||||
if (now > expiresAt) {
|
if (now > expiresAt) {
|
||||||
// Clean up expired token
|
|
||||||
db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`temp_reset_token_${username}`);
|
db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`temp_reset_token_${username}`);
|
||||||
return res.status(400).json({error: 'Temporary token has expired'});
|
return res.status(400).json({error: 'Temporary token has expired'});
|
||||||
}
|
}
|
||||||
@@ -968,7 +1003,6 @@ router.post('/make-admin', authenticateJWT, async (req, res) => {
|
|||||||
.where(eq(users.username, username));
|
.where(eq(users.username, username));
|
||||||
|
|
||||||
authLogger.success(`User ${username} made admin by ${adminUser[0].username}`);
|
authLogger.success(`User ${username} made admin by ${adminUser[0].username}`);
|
||||||
// User made admin - toast notification handled by frontend
|
|
||||||
res.json({message: `User ${username} is now an admin`});
|
res.json({message: `User ${username} is now an admin`});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1011,7 +1045,6 @@ router.post('/remove-admin', authenticateJWT, async (req, res) => {
|
|||||||
.where(eq(users.username, username));
|
.where(eq(users.username, username));
|
||||||
|
|
||||||
authLogger.success(`Admin status removed from ${username} by ${adminUser[0].username}`);
|
authLogger.success(`Admin status removed from ${username} by ${adminUser[0].username}`);
|
||||||
// Admin status removed - toast notification handled by frontend
|
|
||||||
res.json({message: `Admin status removed from ${username}`});
|
res.json({message: `Admin status removed from ${username}`});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1073,8 +1106,6 @@ router.post('/totp/verify-login', async (req, res) => {
|
|||||||
expiresIn: '50d',
|
expiresIn: '50d',
|
||||||
});
|
});
|
||||||
|
|
||||||
// TOTP login completed - toast notification handled by frontend
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
token,
|
token,
|
||||||
is_admin: !!userRecord.is_admin,
|
is_admin: !!userRecord.is_admin,
|
||||||
@@ -1174,7 +1205,6 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => {
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, userId));
|
.where(eq(users.id, userId));
|
||||||
|
|
||||||
// 2FA enabled - toast notification handled by frontend
|
|
||||||
res.json({
|
res.json({
|
||||||
message: 'TOTP enabled successfully',
|
message: 'TOTP enabled successfully',
|
||||||
backup_codes: backupCodes
|
backup_codes: backupCodes
|
||||||
@@ -1236,7 +1266,6 @@ router.post('/totp/disable', authenticateJWT, async (req, res) => {
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, userId));
|
.where(eq(users.id, userId));
|
||||||
|
|
||||||
// 2FA disabled - toast notification handled by frontend
|
|
||||||
res.json({message: 'TOTP disabled successfully'});
|
res.json({message: 'TOTP disabled successfully'});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1353,7 +1382,6 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => {
|
|||||||
await db.delete(users).where(eq(users.id, targetUserId));
|
await db.delete(users).where(eq(users.id, targetUserId));
|
||||||
|
|
||||||
authLogger.success(`User ${username} deleted by admin ${adminUser[0].username}`);
|
authLogger.success(`User ${username} deleted by admin ${adminUser[0].username}`);
|
||||||
// User deleted - toast notification handled by frontend
|
|
||||||
res.json({message: `User ${username} deleted successfully`});
|
res.json({message: `User ${username} deleted successfully`});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import {Client as SSHClient} from 'ssh2';
|
import {Client as SSHClient} from 'ssh2';
|
||||||
import {db} from '../database/db/index.js';
|
import {db} from '../database/db/index.js';
|
||||||
import {sshData, sshCredentials} from '../database/db/schema.js';
|
import {sshCredentials} from '../database/db/schema.js';
|
||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import {fileLogger} from '../utils/logger.js';
|
import {fileLogger} from '../utils/logger.js';
|
||||||
|
|
||||||
@@ -47,12 +47,28 @@ function scheduleSessionCleanup(sessionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||||
const {sessionId, hostId, ip, port, username, password, sshKey, keyPassword, authType, credentialId, userId} = req.body;
|
const {
|
||||||
|
sessionId,
|
||||||
// Connection request received
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
sshKey,
|
||||||
|
keyPassword,
|
||||||
|
authType,
|
||||||
|
credentialId,
|
||||||
|
userId
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
if (!sessionId || !ip || !username || !port) {
|
if (!sessionId || !ip || !username || !port) {
|
||||||
fileLogger.warn('Missing SSH connection parameters for file manager', { operation: 'file_connect', sessionId, hasIp: !!ip, hasUsername: !!username, hasPort: !!port });
|
fileLogger.warn('Missing SSH connection parameters for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hasIp: !!ip,
|
||||||
|
hasUsername: !!username,
|
||||||
|
hasPort: !!port
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Missing SSH connection parameters'});
|
return res.status(400).json({error: 'Missing SSH connection parameters'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,13 +97,31 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
|||||||
authType: credential.authType
|
authType: credential.authType
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
fileLogger.warn('No credentials found in database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, userId });
|
fileLogger.warn('No credentials found in database for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
credentialId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fileLogger.warn('Failed to resolve credentials from database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
fileLogger.warn('Failed to resolve credentials from database for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
credentialId,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (credentialId && hostId) {
|
} else if (credentialId && hostId) {
|
||||||
fileLogger.warn('Missing userId for credential resolution in file manager', { operation: 'file_connect', sessionId, hostId, credentialId, hasUserId: !!userId });
|
fileLogger.warn('Missing userId for credential resolution in file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
credentialId,
|
||||||
|
hasUserId: !!userId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: any = {
|
const config: any = {
|
||||||
@@ -146,13 +180,22 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
|||||||
if (resolvedCredentials.keyPassword) config.passphrase = resolvedCredentials.keyPassword;
|
if (resolvedCredentials.keyPassword) config.passphrase = resolvedCredentials.keyPassword;
|
||||||
|
|
||||||
} catch (keyError) {
|
} catch (keyError) {
|
||||||
fileLogger.error('SSH key format error for file manager', { operation: 'file_connect', sessionId, hostId, error: keyError.message });
|
fileLogger.error('SSH key format error for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
error: keyError.message
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Invalid SSH key format'});
|
return res.status(400).json({error: 'Invalid SSH key format'});
|
||||||
}
|
}
|
||||||
} else if (resolvedCredentials.password && resolvedCredentials.password.trim()) {
|
} else if (resolvedCredentials.password && resolvedCredentials.password.trim()) {
|
||||||
config.password = resolvedCredentials.password;
|
config.password = resolvedCredentials.password;
|
||||||
} else {
|
} else {
|
||||||
fileLogger.warn('No authentication method provided for file manager', { operation: 'file_connect', sessionId, hostId });
|
fileLogger.warn('No authentication method provided for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId
|
||||||
|
});
|
||||||
return res.status(400).json({error: 'Either password or SSH key must be provided'});
|
return res.status(400).json({error: 'Either password or SSH key must be provided'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +211,15 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
|||||||
client.on('error', (err) => {
|
client.on('error', (err) => {
|
||||||
if (responseSent) return;
|
if (responseSent) return;
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
fileLogger.error('SSH connection failed for file manager', { operation: 'file_connect', sessionId, hostId, ip, port, username, error: err.message });
|
fileLogger.error('SSH connection failed for file manager', {
|
||||||
|
operation: 'file_connect',
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
res.status(500).json({status: 'error', message: err.message});
|
res.status(500).json({status: 'error', message: err.message});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,7 +422,11 @@ app.post('/ssh/file_manager/ssh/writeFile', async (req, res) => {
|
|||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
|
res.json({
|
||||||
|
message: 'File written successfully',
|
||||||
|
path: filePath,
|
||||||
|
toast: {type: 'success', message: `File written: ${filePath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -379,7 +434,11 @@ app.post('/ssh/file_manager/ssh/writeFile', async (req, res) => {
|
|||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
|
res.json({
|
||||||
|
message: 'File written successfully',
|
||||||
|
path: filePath,
|
||||||
|
toast: {type: 'success', message: `File written: ${filePath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,7 +470,10 @@ app.post('/ssh/file_manager/ssh/writeFile', async (req, res) => {
|
|||||||
|
|
||||||
fileLogger.error('Fallback write command failed:', err);
|
fileLogger.error('Fallback write command failed:', err);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
return res.status(500).json({error: `Write failed: ${err.message}`, toast: {type: 'error', message: `Write failed: ${err.message}`}});
|
return res.status(500).json({
|
||||||
|
error: `Write failed: ${err.message}`,
|
||||||
|
toast: {type: 'error', message: `Write failed: ${err.message}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -432,12 +494,19 @@ app.post('/ssh/file_manager/ssh/writeFile', async (req, res) => {
|
|||||||
|
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
|
res.json({
|
||||||
|
message: 'File written successfully',
|
||||||
|
path: filePath,
|
||||||
|
toast: {type: 'success', message: `File written: ${filePath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileLogger.error(`Fallback write failed with code ${code}: ${errorData}`);
|
fileLogger.error(`Fallback write failed with code ${code}: ${errorData}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({error: `Write failed: ${errorData}`, toast: {type: 'error', message: `Write failed: ${errorData}`}});
|
res.status(500).json({
|
||||||
|
error: `Write failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Write failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -527,7 +596,11 @@ app.post('/ssh/file_manager/ssh/uploadFile', async (req, res) => {
|
|||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File uploaded successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File uploaded: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -535,7 +608,11 @@ app.post('/ssh/file_manager/ssh/uploadFile', async (req, res) => {
|
|||||||
if (hasError || hasFinished) return;
|
if (hasError || hasFinished) return;
|
||||||
hasFinished = true;
|
hasFinished = true;
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File uploaded successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File uploaded: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -598,12 +675,19 @@ app.post('/ssh/file_manager/ssh/uploadFile', async (req, res) => {
|
|||||||
|
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File uploaded successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File uploaded: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileLogger.error(`Fallback upload failed with code ${code}: ${errorData}`);
|
fileLogger.error(`Fallback upload failed with code ${code}: ${errorData}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({error: `Upload failed: ${errorData}`, toast: {type: 'error', message: `Upload failed: ${errorData}`}});
|
res.status(500).json({
|
||||||
|
error: `Upload failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Upload failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -655,12 +739,19 @@ app.post('/ssh/file_manager/ssh/uploadFile', async (req, res) => {
|
|||||||
|
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File uploaded successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File uploaded: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileLogger.error(`Chunked fallback upload failed with code ${code}: ${errorData}`);
|
fileLogger.error(`Chunked fallback upload failed with code ${code}: ${errorData}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({error: `Chunked upload failed: ${errorData}`, toast: {type: 'error', message: `Chunked upload failed: ${errorData}`}});
|
res.status(500).json({
|
||||||
|
error: `Chunked upload failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Chunked upload failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -740,7 +831,11 @@ app.post('/ssh/file_manager/ssh/createFile', async (req, res) => {
|
|||||||
stream.on('close', (code) => {
|
stream.on('close', (code) => {
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File created successfully', path: fullPath, toast: {type: 'success', message: `File created: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File created successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File created: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -748,13 +843,20 @@ app.post('/ssh/file_manager/ssh/createFile', async (req, res) => {
|
|||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
fileLogger.error(`SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
fileLogger.error(`SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
return res.status(500).json({error: `Command failed: ${errorData}`, toast: {type: 'error', message: `File creation failed: ${errorData}`}});
|
return res.status(500).json({
|
||||||
|
error: `Command failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `File creation failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'File created successfully', path: fullPath, toast: {type: 'success', message: `File created: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'File created successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `File created: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -824,7 +926,11 @@ app.post('/ssh/file_manager/ssh/createFolder', async (req, res) => {
|
|||||||
stream.on('close', (code) => {
|
stream.on('close', (code) => {
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Folder created successfully', path: fullPath, toast: {type: 'success', message: `Folder created: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'Folder created successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `Folder created: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -832,13 +938,20 @@ app.post('/ssh/file_manager/ssh/createFolder', async (req, res) => {
|
|||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
fileLogger.error(`SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
fileLogger.error(`SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
return res.status(500).json({error: `Command failed: ${errorData}`, toast: {type: 'error', message: `Folder creation failed: ${errorData}`}});
|
return res.status(500).json({
|
||||||
|
error: `Command failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Folder creation failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Folder created successfully', path: fullPath, toast: {type: 'success', message: `Folder created: ${fullPath}`}});
|
res.json({
|
||||||
|
message: 'Folder created successfully',
|
||||||
|
path: fullPath,
|
||||||
|
toast: {type: 'success', message: `Folder created: ${fullPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -907,7 +1020,11 @@ app.delete('/ssh/file_manager/ssh/deleteItem', async (req, res) => {
|
|||||||
stream.on('close', (code) => {
|
stream.on('close', (code) => {
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Item deleted successfully', path: itemPath, toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}});
|
res.json({
|
||||||
|
message: 'Item deleted successfully',
|
||||||
|
path: itemPath,
|
||||||
|
toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -915,13 +1032,20 @@ app.delete('/ssh/file_manager/ssh/deleteItem', async (req, res) => {
|
|||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
fileLogger.error(`SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
fileLogger.error(`SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
return res.status(500).json({error: `Command failed: ${errorData}`, toast: {type: 'error', message: `Delete failed: ${errorData}`}});
|
return res.status(500).json({
|
||||||
|
error: `Command failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Delete failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Item deleted successfully', path: itemPath, toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}});
|
res.json({
|
||||||
|
message: 'Item deleted successfully',
|
||||||
|
path: itemPath,
|
||||||
|
toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -992,7 +1116,12 @@ app.put('/ssh/file_manager/ssh/renameItem', async (req, res) => {
|
|||||||
stream.on('close', (code) => {
|
stream.on('close', (code) => {
|
||||||
if (outputData.includes('SUCCESS')) {
|
if (outputData.includes('SUCCESS')) {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Item renamed successfully', oldPath, newPath, toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}});
|
res.json({
|
||||||
|
message: 'Item renamed successfully',
|
||||||
|
oldPath,
|
||||||
|
newPath,
|
||||||
|
toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1000,13 +1129,21 @@ app.put('/ssh/file_manager/ssh/renameItem', async (req, res) => {
|
|||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
fileLogger.error(`SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
fileLogger.error(`SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
return res.status(500).json({error: `Command failed: ${errorData}`, toast: {type: 'error', message: `Rename failed: ${errorData}`}});
|
return res.status(500).json({
|
||||||
|
error: `Command failed: ${errorData}`,
|
||||||
|
toast: {type: 'error', message: `Rename failed: ${errorData}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.json({message: 'Item renamed successfully', oldPath, newPath, toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}});
|
res.json({
|
||||||
|
message: 'Item renamed successfully',
|
||||||
|
oldPath,
|
||||||
|
newPath,
|
||||||
|
toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import {Client, type ConnectConfig} from 'ssh2';
|
import {Client, type ConnectConfig} from 'ssh2';
|
||||||
@@ -8,9 +7,6 @@ import {sshData, sshCredentials} from '../database/db/schema.js';
|
|||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import {statsLogger} from '../utils/logger.js';
|
import {statsLogger} from '../utils/logger.js';
|
||||||
|
|
||||||
// Rate limiting removed - not needed for internal application
|
|
||||||
|
|
||||||
// Connection pooling
|
|
||||||
interface PooledConnection {
|
interface PooledConnection {
|
||||||
client: Client;
|
client: Client;
|
||||||
lastUsed: number;
|
lastUsed: number;
|
||||||
@@ -112,7 +108,7 @@ class SSHConnectionPool {
|
|||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxAge = 10 * 60 * 1000; // 10 minutes
|
const maxAge = 10 * 60 * 1000;
|
||||||
|
|
||||||
for (const [hostKey, connections] of this.connections.entries()) {
|
for (const [hostKey, connections] of this.connections.entries()) {
|
||||||
const activeConnections = connections.filter(conn => {
|
const activeConnections = connections.filter(conn => {
|
||||||
@@ -150,7 +146,6 @@ class SSHConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request queuing to prevent race conditions
|
|
||||||
class RequestQueue {
|
class RequestQueue {
|
||||||
private queues = new Map<number, Array<() => Promise<any>>>();
|
private queues = new Map<number, Array<() => Promise<any>>>();
|
||||||
private processing = new Set<number>();
|
private processing = new Set<number>();
|
||||||
@@ -195,7 +190,6 @@ class RequestQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics caching
|
|
||||||
interface CachedMetrics {
|
interface CachedMetrics {
|
||||||
data: any;
|
data: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -204,7 +198,7 @@ interface CachedMetrics {
|
|||||||
|
|
||||||
class MetricsCache {
|
class MetricsCache {
|
||||||
private cache = new Map<number, CachedMetrics>();
|
private cache = new Map<number, CachedMetrics>();
|
||||||
private ttl = 30000; // 30 seconds
|
private ttl = 30000;
|
||||||
|
|
||||||
get(hostId: number): any | null {
|
get(hostId: number): any | null {
|
||||||
const cached = this.cache.get(hostId);
|
const cached = this.cache.get(hostId);
|
||||||
@@ -231,7 +225,6 @@ class MetricsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global instances
|
|
||||||
const connectionPool = new SSHConnectionPool();
|
const connectionPool = new SSHConnectionPool();
|
||||||
const requestQueue = new RequestQueue();
|
const requestQueue = new RequestQueue();
|
||||||
const metricsCache = new MetricsCache();
|
const metricsCache = new MetricsCache();
|
||||||
@@ -268,9 +261,6 @@ type StatusEntry = {
|
|||||||
lastChecked: string;
|
lastChecked: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rate limiting middleware removed
|
|
||||||
|
|
||||||
// Input validation middleware
|
|
||||||
function validateHostId(req: express.Request, res: express.Response, next: express.NextFunction) {
|
function validateHostId(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
if (!id || !Number.isInteger(id) || id <= 0) {
|
if (!id || !Number.isInteger(id) || id <= 0) {
|
||||||
@@ -294,7 +284,7 @@ app.use((req, res, next) => {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use(express.json({ limit: '1mb' })); // Add request size limit
|
app.use(express.json({limit: '1mb'}));
|
||||||
|
|
||||||
|
|
||||||
const hostStatuses: Map<number, StatusEntry> = new Map();
|
const hostStatuses: Map<number, StatusEntry> = new Map();
|
||||||
@@ -514,7 +504,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
memory: { percent: number | null; usedGiB: number | null; totalGiB: number | null };
|
memory: { percent: number | null; usedGiB: number | null; totalGiB: number | null };
|
||||||
disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null };
|
disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null };
|
||||||
}> {
|
}> {
|
||||||
// Check cache first
|
|
||||||
const cached = metricsCache.get(host.id);
|
const cached = metricsCache.get(host.id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
@@ -527,14 +516,12 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
let loadTriplet: [number, number, number] | null = null;
|
let loadTriplet: [number, number, number] | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute all commands in parallel for better performance
|
|
||||||
const [stat1, loadAvgOut, coresOut] = await Promise.all([
|
const [stat1, loadAvgOut, coresOut] = await Promise.all([
|
||||||
execCommand(client, 'cat /proc/stat'),
|
execCommand(client, 'cat /proc/stat'),
|
||||||
execCommand(client, 'cat /proc/loadavg'),
|
execCommand(client, 'cat /proc/loadavg'),
|
||||||
execCommand(client, 'nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo')
|
execCommand(client, 'nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Wait for CPU calculation
|
|
||||||
await new Promise(r => setTimeout(r, 500));
|
await new Promise(r => setTimeout(r, 500));
|
||||||
const stat2 = await execCommand(client, 'cat /proc/stat');
|
const stat2 = await execCommand(client, 'cat /proc/stat');
|
||||||
|
|
||||||
@@ -633,7 +620,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
disk: {percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman},
|
disk: {percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
metricsCache.set(host.id, result);
|
metricsCache.set(host.id, result);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@@ -684,7 +670,12 @@ async function pollStatusesOnce(): Promise<void> {
|
|||||||
const results = await Promise.allSettled(checks);
|
const results = await Promise.allSettled(checks);
|
||||||
const onlineCount = results.filter(r => r.status === 'fulfilled' && r.value === true).length;
|
const onlineCount = results.filter(r => r.status === 'fulfilled' && r.value === true).length;
|
||||||
const offlineCount = hosts.length - onlineCount;
|
const offlineCount = hosts.length - onlineCount;
|
||||||
statsLogger.success('Status polling completed', { operation: 'status_poll', totalHosts: hosts.length, onlineCount, offlineCount });
|
statsLogger.success('Status polling completed', {
|
||||||
|
operation: 'status_poll',
|
||||||
|
totalHosts: hosts.length,
|
||||||
|
onlineCount,
|
||||||
|
offlineCount
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/status', async (req, res) => {
|
app.get('/status', async (req, res) => {
|
||||||
@@ -733,7 +724,6 @@ app.get('/metrics/:id', validateHostId, async (req, res) => {
|
|||||||
return res.status(404).json({error: 'Host not found'});
|
return res.status(404).json({error: 'Host not found'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if host is online first
|
|
||||||
const isOnline = await tcpPing(host.ip, host.port, 5000);
|
const isOnline = await tcpPing(host.ip, host.port, 5000);
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
@@ -750,7 +740,6 @@ app.get('/metrics/:id', validateHostId, async (req, res) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
statsLogger.error('Failed to collect metrics', err);
|
statsLogger.error('Failed to collect metrics', err);
|
||||||
|
|
||||||
// Return proper error response instead of empty data
|
|
||||||
if (err instanceof Error && err.message.includes('timeout')) {
|
if (err instanceof Error && err.message.includes('timeout')) {
|
||||||
return res.status(504).json({
|
return res.status(504).json({
|
||||||
error: 'Metrics collection timeout',
|
error: 'Metrics collection timeout',
|
||||||
@@ -771,7 +760,6 @@ app.get('/metrics/:id', validateHostId, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
statsLogger.info('Received SIGINT, shutting down gracefully');
|
statsLogger.info('Received SIGINT, shutting down gracefully');
|
||||||
connectionPool.destroy();
|
connectionPool.destroy();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {WebSocketServer, WebSocket, type RawData} from 'ws';
|
import {WebSocketServer, WebSocket, type RawData} from 'ws';
|
||||||
import {Client, type ClientChannel, type PseudoTtyOptions} from 'ssh2';
|
import {Client, type ClientChannel, type PseudoTtyOptions} from 'ssh2';
|
||||||
import {db} from '../database/db/index.js';
|
import {db} from '../database/db/index.js';
|
||||||
import {sshData, sshCredentials} from '../database/db/schema.js';
|
import {sshCredentials} from '../database/db/schema.js';
|
||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import {sshLogger} from '../utils/logger.js';
|
import {sshLogger} from '../utils/logger.js';
|
||||||
|
|
||||||
@@ -9,27 +9,24 @@ const wss = new WebSocketServer({port: 8082});
|
|||||||
|
|
||||||
sshLogger.success('SSH Terminal WebSocket server started', {operation: 'server_start', port: 8082});
|
sshLogger.success('SSH Terminal WebSocket server started', {operation: 'server_start', port: 8082});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
wss.on('connection', (ws: WebSocket) => {
|
wss.on('connection', (ws: WebSocket) => {
|
||||||
let sshConn: Client | null = null;
|
let sshConn: Client | null = null;
|
||||||
let sshStream: ClientChannel | null = null;
|
let sshStream: ClientChannel | null = null;
|
||||||
let pingInterval: NodeJS.Timeout | null = null;
|
let pingInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
cleanupSSH();
|
cleanupSSH();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', (msg: RawData) => {
|
ws.on('message', (msg: RawData) => {
|
||||||
|
|
||||||
|
|
||||||
let parsed: any;
|
let parsed: any;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(msg.toString());
|
parsed = JSON.parse(msg.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sshLogger.error('Invalid JSON received', e, { operation: 'websocket_message', messageLength: msg.toString().length });
|
sshLogger.error('Invalid JSON received', e, {
|
||||||
|
operation: 'websocket_message',
|
||||||
|
messageLength: msg.toString().length
|
||||||
|
});
|
||||||
ws.send(JSON.stringify({type: 'error', message: 'Invalid JSON'}));
|
ws.send(JSON.stringify({type: 'error', message: 'Invalid JSON'}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -39,8 +36,15 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'connectToHost':
|
case 'connectToHost':
|
||||||
handleConnectToHost(data).catch(error => {
|
handleConnectToHost(data).catch(error => {
|
||||||
sshLogger.error('Failed to connect to host', error, { operation: 'ssh_connect', hostId: data.hostConfig?.id, ip: data.hostConfig?.ip });
|
sshLogger.error('Failed to connect to host', error, {
|
||||||
ws.send(JSON.stringify({type: 'error', message: 'Failed to connect to host: ' + (error instanceof Error ? error.message : 'Unknown error')}));
|
operation: 'ssh_connect',
|
||||||
|
hostId: data.hostConfig?.id,
|
||||||
|
ip: data.hostConfig?.ip
|
||||||
|
});
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to connect to host: ' + (error instanceof Error ? error.message : 'Unknown error')
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -106,7 +110,13 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!port || typeof port !== 'number' || port <= 0) {
|
if (!port || typeof port !== 'number' || port <= 0) {
|
||||||
sshLogger.error('Invalid port provided', undefined, { operation: 'ssh_connect', hostId: id, ip, username, port });
|
sshLogger.error('Invalid port provided', undefined, {
|
||||||
|
operation: 'ssh_connect',
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
username,
|
||||||
|
port
|
||||||
|
});
|
||||||
ws.send(JSON.stringify({type: 'error', message: 'Invalid port provided'}));
|
ws.send(JSON.stringify({type: 'error', message: 'Invalid port provided'}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -115,7 +125,13 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
|
|
||||||
const connectionTimeout = setTimeout(() => {
|
const connectionTimeout = setTimeout(() => {
|
||||||
if (sshConn) {
|
if (sshConn) {
|
||||||
sshLogger.error('SSH connection timeout', undefined, { operation: 'ssh_connect', hostId: id, ip, port, username });
|
sshLogger.error('SSH connection timeout', undefined, {
|
||||||
|
operation: 'ssh_connect',
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username
|
||||||
|
});
|
||||||
ws.send(JSON.stringify({type: 'error', message: 'SSH connection timeout'}));
|
ws.send(JSON.stringify({type: 'error', message: 'SSH connection timeout'}));
|
||||||
cleanupSSH(connectionTimeout);
|
cleanupSSH(connectionTimeout);
|
||||||
}
|
}
|
||||||
@@ -142,13 +158,28 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
authType: credential.authType
|
authType: credential.authType
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
sshLogger.warn(`No credentials found for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, userId: hostConfig.userId });
|
sshLogger.warn(`No credentials found for host ${id}`, {
|
||||||
|
operation: 'ssh_credentials',
|
||||||
|
hostId: id,
|
||||||
|
credentialId,
|
||||||
|
userId: hostConfig.userId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sshLogger.warn(`Failed to resolve credentials for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
sshLogger.warn(`Failed to resolve credentials for host ${id}`, {
|
||||||
|
operation: 'ssh_credentials',
|
||||||
|
hostId: id,
|
||||||
|
credentialId,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (credentialId && id) {
|
} else if (credentialId && id) {
|
||||||
sshLogger.warn('Missing userId for credential resolution in terminal', { operation: 'ssh_credentials', hostId: id, credentialId, hasUserId: !!hostConfig.userId });
|
sshLogger.warn('Missing userId for credential resolution in terminal', {
|
||||||
|
operation: 'ssh_credentials',
|
||||||
|
hostId: id,
|
||||||
|
credentialId,
|
||||||
|
hasUserId: !!hostConfig.userId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sshConn.on('ready', () => {
|
sshConn.on('ready', () => {
|
||||||
@@ -189,7 +220,14 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
|
|
||||||
sshConn.on('error', (err: Error) => {
|
sshConn.on('error', (err: Error) => {
|
||||||
clearTimeout(connectionTimeout);
|
clearTimeout(connectionTimeout);
|
||||||
sshLogger.error('SSH connection error', err, { operation: 'ssh_connect', hostId: id, ip, port, username, authType: resolvedCredentials.authType });
|
sshLogger.error('SSH connection error', err, {
|
||||||
|
operation: 'ssh_connect',
|
||||||
|
hostId: id,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
authType: resolvedCredentials.authType
|
||||||
|
});
|
||||||
|
|
||||||
let errorMessage = 'SSH error: ' + err.message;
|
let errorMessage = 'SSH error: ' + err.message;
|
||||||
if (err.message.includes('No matching key exchange algorithm')) {
|
if (err.message.includes('No matching key exchange algorithm')) {
|
||||||
@@ -219,7 +257,6 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
cleanupSSH(connectionTimeout);
|
cleanupSSH(connectionTimeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const connectConfig: any = {
|
const connectConfig: any = {
|
||||||
host: ip,
|
host: ip,
|
||||||
port,
|
port,
|
||||||
@@ -359,6 +396,4 @@ wss.on('connection', (ws: WebSocket) => {
|
|||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,18 +3,14 @@ import cors from 'cors';
|
|||||||
import {Client} from 'ssh2';
|
import {Client} from 'ssh2';
|
||||||
import {ChildProcess} from 'child_process';
|
import {ChildProcess} from 'child_process';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as net from 'net';
|
|
||||||
import {db} from '../database/db/index.js';
|
import {db} from '../database/db/index.js';
|
||||||
import {sshData, sshCredentials} from '../database/db/schema.js';
|
import {sshCredentials} from '../database/db/schema.js';
|
||||||
import {eq, and} from 'drizzle-orm';
|
import {eq, and} from 'drizzle-orm';
|
||||||
import type {
|
import type {
|
||||||
SSHHost,
|
SSHHost,
|
||||||
TunnelConfig,
|
TunnelConfig,
|
||||||
TunnelConnection,
|
|
||||||
TunnelStatus,
|
TunnelStatus,
|
||||||
HostConfig,
|
|
||||||
VerificationData,
|
VerificationData,
|
||||||
ConnectionState,
|
|
||||||
ErrorType
|
ErrorType
|
||||||
} from '../../types/index.js';
|
} from '../../types/index.js';
|
||||||
import {CONNECTION_STATES} from '../../types/index.js';
|
import {CONNECTION_STATES} from '../../types/index.js';
|
||||||
@@ -42,18 +38,6 @@ const retryExhaustedTunnels = new Set<string>();
|
|||||||
const tunnelConfigs = new Map<string, TunnelConfig>();
|
const tunnelConfigs = new Map<string, TunnelConfig>();
|
||||||
const activeTunnelProcesses = new Map<string, ChildProcess>();
|
const activeTunnelProcesses = new Map<string, ChildProcess>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ERROR_TYPES = {
|
|
||||||
AUTH: "AUTHENTICATION_FAILED",
|
|
||||||
NETWORK: "NETWORK_ERROR",
|
|
||||||
PORT: "CONNECTION_FAILED",
|
|
||||||
PERMISSION: "CONNECTION_FAILED",
|
|
||||||
TIMEOUT: "TIMEOUT",
|
|
||||||
UNKNOWN: "UNKNOWN"
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
|
|
||||||
function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void {
|
function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void {
|
||||||
if (status.status === CONNECTION_STATES.CONNECTED && activeRetryTimers.has(tunnelName)) {
|
if (status.status === CONNECTION_STATES.CONNECTED && activeRetryTimers.has(tunnelName)) {
|
||||||
return;
|
return;
|
||||||
@@ -338,19 +322,7 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void {
|
function setupPingInterval(tunnelName: string): void {
|
||||||
if (isPeriodic) {
|
|
||||||
if (!activeTunnels.has(tunnelName)) {
|
|
||||||
broadcastTunnelStatus(tunnelName, {
|
|
||||||
connected: false,
|
|
||||||
status: CONNECTION_STATES.DISCONNECTED,
|
|
||||||
reason: 'Tunnel connection lost'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void {
|
|
||||||
const pingKey = `${tunnelName}_ping`;
|
const pingKey = `${tunnelName}_ping`;
|
||||||
if (verificationTimers.has(pingKey)) {
|
if (verificationTimers.has(pingKey)) {
|
||||||
clearInterval(verificationTimers.get(pingKey)!);
|
clearInterval(verificationTimers.get(pingKey)!);
|
||||||
@@ -403,7 +375,13 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!tunnelConfig || !tunnelConfig.sourceIP || !tunnelConfig.sourceUsername || !tunnelConfig.sourceSSHPort) {
|
if (!tunnelConfig || !tunnelConfig.sourceIP || !tunnelConfig.sourceUsername || !tunnelConfig.sourceSSHPort) {
|
||||||
tunnelLogger.error('Invalid tunnel connection details', { operation: 'tunnel_connect', tunnelName, hasSourceIP: !!tunnelConfig?.sourceIP, hasSourceUsername: !!tunnelConfig?.sourceUsername, hasSourceSSHPort: !!tunnelConfig?.sourceSSHPort });
|
tunnelLogger.error('Invalid tunnel connection details', {
|
||||||
|
operation: 'tunnel_connect',
|
||||||
|
tunnelName,
|
||||||
|
hasSourceIP: !!tunnelConfig?.sourceIP,
|
||||||
|
hasSourceUsername: !!tunnelConfig?.sourceUsername,
|
||||||
|
hasSourceSSHPort: !!tunnelConfig?.sourceSSHPort
|
||||||
|
});
|
||||||
broadcastTunnelStatus(tunnelName, {
|
broadcastTunnelStatus(tunnelName, {
|
||||||
connected: false,
|
connected: false,
|
||||||
status: CONNECTION_STATES.FAILED,
|
status: CONNECTION_STATES.FAILED,
|
||||||
@@ -440,14 +418,22 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
|||||||
authMethod: credential.authType
|
authMethod: credential.authType
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
tunnelLogger.warn('No source credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId });
|
tunnelLogger.warn('No source credentials found in database', {
|
||||||
|
operation: 'tunnel_connect',
|
||||||
|
tunnelName,
|
||||||
|
credentialId: tunnelConfig.sourceCredentialId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tunnelLogger.warn('Failed to resolve source credentials from database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
tunnelLogger.warn('Failed to resolve source credentials from database', {
|
||||||
|
operation: 'tunnel_connect',
|
||||||
|
tunnelName,
|
||||||
|
credentialId: tunnelConfig.sourceCredentialId,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve endpoint credentials if tunnel config has endpointCredentialId
|
|
||||||
let resolvedEndpointCredentials = {
|
let resolvedEndpointCredentials = {
|
||||||
password: tunnelConfig.endpointPassword,
|
password: tunnelConfig.endpointPassword,
|
||||||
sshKey: tunnelConfig.endpointSSHKey,
|
sshKey: tunnelConfig.endpointSSHKey,
|
||||||
@@ -476,13 +462,22 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
|||||||
authMethod: credential.authType
|
authMethod: credential.authType
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
tunnelLogger.warn('No endpoint credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId });
|
tunnelLogger.warn('No endpoint credentials found in database', {
|
||||||
|
operation: 'tunnel_connect',
|
||||||
|
tunnelName,
|
||||||
|
credentialId: tunnelConfig.endpointCredentialId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tunnelLogger.warn(`Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
tunnelLogger.warn(`Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} else if (tunnelConfig.endpointCredentialId) {
|
} else if (tunnelConfig.endpointCredentialId) {
|
||||||
tunnelLogger.warn('Missing userId for endpoint credential resolution', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId, hasUserId: !!tunnelConfig.endpointUserId });
|
tunnelLogger.warn('Missing userId for endpoint credential resolution', {
|
||||||
|
operation: 'tunnel_connect',
|
||||||
|
tunnelName,
|
||||||
|
credentialId: tunnelConfig.endpointCredentialId,
|
||||||
|
hasUserId: !!tunnelConfig.endpointUserId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conn = new Client();
|
const conn = new Client();
|
||||||
@@ -597,7 +592,7 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
|||||||
connected: true,
|
connected: true,
|
||||||
status: CONNECTION_STATES.CONNECTED
|
status: CONNECTION_STATES.CONNECTED
|
||||||
});
|
});
|
||||||
setupPingInterval(tunnelName, tunnelConfig);
|
setupPingInterval(tunnelName);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ class Logger {
|
|||||||
console.log(this.formatMessage('success', message, context));
|
console.log(this.formatMessage('success', message, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience methods for common operations
|
|
||||||
auth(message: string, context?: LogContext): void {
|
auth(message: string, context?: LogContext): void {
|
||||||
this.info(`AUTH: ${message}`, { ...context, operation: 'auth' });
|
this.info(`AUTH: ${message}`, { ...context, operation: 'auth' });
|
||||||
}
|
}
|
||||||
@@ -144,18 +143,6 @@ class Logger {
|
|||||||
retry(message: string, context?: LogContext): void {
|
retry(message: string, context?: LogContext): void {
|
||||||
this.warn(`RETRY: ${message}`, { ...context, operation: 'retry' });
|
this.warn(`RETRY: ${message}`, { ...context, operation: 'retry' });
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup(message: string, context?: LogContext): void {
|
|
||||||
this.info(`CLEANUP: ${message}`, { ...context, operation: 'cleanup' });
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics(message: string, context?: LogContext): void {
|
|
||||||
this.info(`METRICS: ${message}`, { ...context, operation: 'metrics' });
|
|
||||||
}
|
|
||||||
|
|
||||||
security(message: string, context?: LogContext): void {
|
|
||||||
this.warn(`SECURITY: ${message}`, { ...context, operation: 'security' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const databaseLogger = new Logger('DATABASE', '🗄️', '#6366f1');
|
export const databaseLogger = new Logger('DATABASE', '🗄️', '#6366f1');
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const isDev = process.env.NODE_ENV === 'development' &&
|
const isDev = process.env.NODE_ENV === 'development' &&
|
||||||
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
||||||
|
|
||||||
|
|
||||||
const wsUrl = isDev
|
const wsUrl = isDev
|
||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron
|
: isElectron
|
||||||
|
|||||||
Reference in New Issue
Block a user