Format code
This commit is contained in:
@@ -41,94 +41,340 @@ const dbPath = path.join(dataDir, 'db.sqlite');
|
||||
const sqlite = new Database(dbPath);
|
||||
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
is_oidc INTEGER NOT NULL DEFAULT 0,
|
||||
client_id TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL,
|
||||
issuer_url TEXT NOT NULL,
|
||||
authorization_url TEXT NOT NULL,
|
||||
token_url TEXT NOT NULL,
|
||||
redirect_uri TEXT,
|
||||
identifier_path TEXT NOT NULL,
|
||||
name_path TEXT NOT NULL,
|
||||
scopes TEXT NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
password_hash
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
is_admin
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
0,
|
||||
|
||||
is_oidc
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
0,
|
||||
client_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
client_secret
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
issuer_url
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
authorization_url
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
token_url
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
redirect_uri
|
||||
TEXT,
|
||||
identifier_path
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
name_path
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
scopes
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS settings
|
||||
(
|
||||
key
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
value
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
ip TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
folder TEXT,
|
||||
tags TEXT,
|
||||
pin INTEGER NOT NULL DEFAULT 0,
|
||||
auth_type TEXT NOT NULL,
|
||||
password TEXT,
|
||||
key TEXT,
|
||||
key_password TEXT,
|
||||
key_type TEXT,
|
||||
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
||||
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
||||
tunnel_connections TEXT,
|
||||
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
||||
default_path TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ssh_data
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
name
|
||||
TEXT,
|
||||
ip
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
port
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
folder
|
||||
TEXT,
|
||||
tags
|
||||
TEXT,
|
||||
pin
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
0,
|
||||
auth_type
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
password
|
||||
TEXT,
|
||||
key
|
||||
TEXT,
|
||||
key_password
|
||||
TEXT,
|
||||
key_type
|
||||
TEXT,
|
||||
enable_terminal
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
1,
|
||||
enable_tunnel
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
1,
|
||||
tunnel_connections
|
||||
TEXT,
|
||||
enable_file_manager
|
||||
INTEGER
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
1,
|
||||
default_path
|
||||
TEXT,
|
||||
created_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
updated_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
user_id
|
||||
) REFERENCES users
|
||||
(
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_manager_recent (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_manager_recent
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
host_id
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
name
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
path
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
last_opened
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
user_id
|
||||
) REFERENCES users
|
||||
(
|
||||
id
|
||||
),
|
||||
FOREIGN KEY
|
||||
(
|
||||
host_id
|
||||
) REFERENCES ssh_data
|
||||
(
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_manager_pinned (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_manager_pinned
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
host_id
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
name
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
path
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
pinned_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
user_id
|
||||
) REFERENCES users
|
||||
(
|
||||
id
|
||||
),
|
||||
FOREIGN KEY
|
||||
(
|
||||
host_id
|
||||
) REFERENCES ssh_data
|
||||
(
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_manager_shortcuts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_manager_shortcuts
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
host_id
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
name
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
path
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
created_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
user_id
|
||||
) REFERENCES users
|
||||
(
|
||||
id
|
||||
),
|
||||
FOREIGN KEY
|
||||
(
|
||||
host_id
|
||||
) REFERENCES ssh_data
|
||||
(
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dismissed_alerts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
alert_id TEXT NOT NULL,
|
||||
dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS dismissed_alerts
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
user_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
alert_id
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
dismissed_at
|
||||
TEXT
|
||||
NOT
|
||||
NULL
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
user_id
|
||||
) REFERENCES users
|
||||
(
|
||||
id
|
||||
)
|
||||
);
|
||||
`);
|
||||
|
||||
const addColumnIfNotExists = (table: string, column: string, definition: string) => {
|
||||
@@ -162,7 +408,7 @@ const migrateSchema = () => {
|
||||
logger.info('Removed redirect_uri column from users table');
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
|
||||
addColumnIfNotExists('users', 'identifier_path', 'TEXT');
|
||||
addColumnIfNotExists('users', 'name_path', 'TEXT');
|
||||
addColumnIfNotExists('users', 'scopes', 'TEXT');
|
||||
|
||||
@@ -90,10 +90,10 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}`;
|
||||
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@@ -108,13 +108,13 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
const alerts: TermixAlert[] = await response.json() as TermixAlert[];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
|
||||
const validAlerts = alerts.filter(alert => {
|
||||
const expiryDate = new Date(alert.expiresAt);
|
||||
const isValid = expiryDate > now;
|
||||
return isValid;
|
||||
});
|
||||
|
||||
|
||||
alertCache.set(cacheKey, validAlerts);
|
||||
return validAlerts;
|
||||
} catch (error) {
|
||||
@@ -146,11 +146,11 @@ router.get('/', async (req, res) => {
|
||||
router.get('/user/:userId', async (req, res) => {
|
||||
try {
|
||||
const {userId} = req.params;
|
||||
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({error: 'User ID is required'});
|
||||
}
|
||||
|
||||
|
||||
const allAlerts = await fetchAlertsFromGitHub();
|
||||
|
||||
const dismissedAlertRecords = await db
|
||||
@@ -215,7 +215,7 @@ router.post('/dismiss', async (req, res) => {
|
||||
router.get('/dismissed/:userId', async (req, res) => {
|
||||
try {
|
||||
const {userId} = req.params;
|
||||
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({error: 'User ID is required'});
|
||||
}
|
||||
|
||||
@@ -695,7 +695,7 @@ router.delete('/file_manager/shortcuts', authenticateJWT, async (req: Request, r
|
||||
// POST /ssh/bulk-import
|
||||
router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const { hosts } = req.body;
|
||||
const {hosts} = req.body;
|
||||
|
||||
if (!Array.isArray(hosts) || hosts.length === 0) {
|
||||
logger.warn('Invalid bulk import data - hosts array is required and must not be empty');
|
||||
@@ -715,7 +715,7 @@ router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response)
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
const hostData = hosts[i];
|
||||
|
||||
|
||||
try {
|
||||
if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) {
|
||||
results.failed++;
|
||||
|
||||
@@ -10,13 +10,9 @@ app.use(cors({
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
// Increase JSON body parser limit for larger file uploads
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
app.use(express.urlencoded({ limit: '100mb', extended: true }));
|
||||
|
||||
// Add raw body parser for very large files
|
||||
app.use(express.raw({ limit: '200mb', type: 'application/octet-stream' }));
|
||||
app.use(express.json({limit: '100mb'}));
|
||||
app.use(express.urlencoded({limit: '100mb', extended: true}));
|
||||
app.use(express.raw({limit: '200mb', type: 'application/octet-stream'}));
|
||||
|
||||
const sshIconSymbol = '📁';
|
||||
const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`);
|
||||
@@ -314,9 +310,8 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({error: 'SSH command timed out'});
|
||||
}
|
||||
}, 60000); // Increased timeout to 60 seconds
|
||||
}, 60000);
|
||||
|
||||
// Try SFTP first, fallback to command line if it fails
|
||||
const trySFTP = () => {
|
||||
try {
|
||||
sshConn.client.sftp((err, sftp) => {
|
||||
@@ -326,7 +321,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert content to buffer
|
||||
let fileBuffer;
|
||||
try {
|
||||
if (typeof content === 'string') {
|
||||
@@ -345,9 +339,8 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create write stream with error handling
|
||||
const writeStream = sftp.createWriteStream(filePath);
|
||||
|
||||
|
||||
let hasError = false;
|
||||
let hasFinished = false;
|
||||
|
||||
@@ -378,7 +371,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Write the buffer to the stream
|
||||
try {
|
||||
writeStream.write(fileBuffer);
|
||||
writeStream.end();
|
||||
@@ -395,14 +387,13 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback method using command line
|
||||
const tryFallbackMethod = () => {
|
||||
try {
|
||||
const base64Content = Buffer.from(content, 'utf8').toString('base64');
|
||||
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
|
||||
|
||||
const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
|
||||
|
||||
|
||||
sshConn.client.exec(writeCommand, (err, stream) => {
|
||||
if (err) {
|
||||
clearTimeout(commandTimeout);
|
||||
@@ -426,7 +417,7 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
|
||||
stream.on('close', (code) => {
|
||||
clearTimeout(commandTimeout);
|
||||
|
||||
|
||||
if (outputData.includes('SUCCESS')) {
|
||||
logger.success(`File written successfully via fallback: ${filePath}`);
|
||||
if (!res.headersSent) {
|
||||
@@ -457,11 +448,9 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Start with SFTP
|
||||
trySFTP();
|
||||
});
|
||||
|
||||
// Upload file route
|
||||
app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
const {sessionId, path: filePath, content, fileName} = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
@@ -488,9 +477,8 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({error: 'SSH command timed out'});
|
||||
}
|
||||
}, 60000); // Increased timeout to 60 seconds
|
||||
}, 60000);
|
||||
|
||||
// Try SFTP first, fallback to command line if it fails
|
||||
const trySFTP = () => {
|
||||
try {
|
||||
sshConn.client.sftp((err, sftp) => {
|
||||
@@ -500,7 +488,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert content to buffer
|
||||
let fileBuffer;
|
||||
try {
|
||||
if (typeof content === 'string') {
|
||||
@@ -519,9 +506,8 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create write stream with error handling
|
||||
const writeStream = sftp.createWriteStream(fullPath);
|
||||
|
||||
|
||||
let hasError = false;
|
||||
let hasFinished = false;
|
||||
|
||||
@@ -552,7 +538,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Write the buffer to the stream
|
||||
try {
|
||||
writeStream.write(fileBuffer);
|
||||
writeStream.end();
|
||||
@@ -569,26 +554,23 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback method using command line with chunked approach
|
||||
const tryFallbackMethod = () => {
|
||||
try {
|
||||
// Convert content to base64 and split into smaller chunks if needed
|
||||
const base64Content = Buffer.from(content, 'utf8').toString('base64');
|
||||
const chunkSize = 1000000; // 1MB chunks
|
||||
const chunkSize = 1000000;
|
||||
const chunks = [];
|
||||
|
||||
|
||||
for (let i = 0; i < base64Content.length; i += chunkSize) {
|
||||
chunks.push(base64Content.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
if (chunks.length === 1) {
|
||||
// Single chunk - use simple approach
|
||||
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
|
||||
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
||||
|
||||
const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
|
||||
|
||||
|
||||
sshConn.client.exec(writeCommand, (err, stream) => {
|
||||
if (err) {
|
||||
clearTimeout(commandTimeout);
|
||||
@@ -612,7 +594,7 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
|
||||
stream.on('close', (code) => {
|
||||
clearTimeout(commandTimeout);
|
||||
|
||||
|
||||
if (outputData.includes('SUCCESS')) {
|
||||
logger.success(`File uploaded successfully via fallback: ${fullPath}`);
|
||||
if (!res.headersSent) {
|
||||
@@ -635,17 +617,16 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Multiple chunks - use chunked approach
|
||||
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
|
||||
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
||||
|
||||
let writeCommand = `> '${escapedPath}'`; // Start with empty file
|
||||
|
||||
let writeCommand = `> '${escapedPath}'`;
|
||||
|
||||
chunks.forEach((chunk, index) => {
|
||||
writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`;
|
||||
});
|
||||
|
||||
|
||||
writeCommand += ` && echo "SUCCESS"`;
|
||||
|
||||
sshConn.client.exec(writeCommand, (err, stream) => {
|
||||
@@ -671,7 +652,7 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
|
||||
stream.on('close', (code) => {
|
||||
clearTimeout(commandTimeout);
|
||||
|
||||
|
||||
if (outputData.includes('SUCCESS')) {
|
||||
logger.success(`File uploaded successfully via chunked fallback: ${fullPath}`);
|
||||
if (!res.headersSent) {
|
||||
@@ -703,11 +684,9 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Start with SFTP
|
||||
trySFTP();
|
||||
});
|
||||
|
||||
// Create new file route
|
||||
app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
|
||||
const {sessionId, path: filePath, fileName, content = ''} = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
@@ -804,7 +783,6 @@ app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Create folder route
|
||||
app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
|
||||
const {sessionId, path: folderPath, folderName} = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
@@ -901,7 +879,6 @@ app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Delete file/folder route
|
||||
app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
|
||||
const {sessionId, path: itemPath, isDirectory} = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
@@ -930,7 +907,7 @@ app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
const deleteCommand = isDirectory
|
||||
const deleteCommand = isDirectory
|
||||
? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0`
|
||||
: `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
||||
|
||||
@@ -999,7 +976,6 @@ app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Rename file/folder route
|
||||
app.put('/ssh/file_manager/ssh/renameItem', (req, res) => {
|
||||
const {sessionId, oldPath, newName} = req.body;
|
||||
const sshConn = sshSessions[sessionId];
|
||||
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import fetch from 'node-fetch';
|
||||
import net from 'net';
|
||||
import cors from 'cors';
|
||||
import { Client, type ConnectConfig } from 'ssh2';
|
||||
import {Client, type ConnectConfig} from 'ssh2';
|
||||
|
||||
type HostRecord = {
|
||||
id: number;
|
||||
@@ -21,7 +21,7 @@ type HostStatus = 'online' | 'offline';
|
||||
|
||||
type StatusEntry = {
|
||||
status: HostStatus;
|
||||
lastChecked: string; // ISO string
|
||||
lastChecked: string;
|
||||
};
|
||||
|
||||
const app = express();
|
||||
@@ -30,7 +30,6 @@ app.use(cors({
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
// Fallback explicit CORS headers to cover any edge cases
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
@@ -42,7 +41,6 @@ app.use((req, res, next) => {
|
||||
});
|
||||
app.use(express.json());
|
||||
|
||||
// Logger (customized for Server Stats)
|
||||
const statsIconSymbol = '📡';
|
||||
const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`);
|
||||
const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => {
|
||||
@@ -69,15 +67,13 @@ const logger = {
|
||||
}
|
||||
};
|
||||
|
||||
// In-memory state of last known statuses
|
||||
const hostStatuses: Map<number, StatusEntry> = new Map();
|
||||
|
||||
// Fetch all hosts from the database service (internal endpoint, no JWT)
|
||||
async function fetchAllHosts(): Promise<HostRecord[]> {
|
||||
const url = 'http://localhost:8081/ssh/db/host/internal';
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: { 'x-internal-request': '1' }
|
||||
headers: {'x-internal-request': '1'}
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`DB service error: ${resp.status} ${resp.statusText}`);
|
||||
@@ -112,9 +108,7 @@ function buildSshConfig(host: HostRecord): ConnectConfig {
|
||||
port: host.port || 22,
|
||||
username: host.username || 'root',
|
||||
readyTimeout: 10_000,
|
||||
algorithms: {
|
||||
// keep defaults minimal to avoid negotiation issues
|
||||
}
|
||||
algorithms: {}
|
||||
} as ConnectConfig;
|
||||
|
||||
if (host.authType === 'password') {
|
||||
@@ -138,7 +132,10 @@ async function withSshConnection<T>(host: HostRecord, fn: (client: Client) => Pr
|
||||
const onError = (err: Error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
try { client.end(); } catch {}
|
||||
try {
|
||||
client.end();
|
||||
} catch {
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
@@ -148,7 +145,10 @@ async function withSshConnection<T>(host: HostRecord, fn: (client: Client) => Pr
|
||||
const result = await fn(client);
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
try { client.end(); } catch {}
|
||||
try {
|
||||
client.end();
|
||||
} catch {
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -166,16 +166,20 @@ async function withSshConnection<T>(host: HostRecord, fn: (client: Client) => Pr
|
||||
});
|
||||
}
|
||||
|
||||
function execCommand(client: Client, command: string): Promise<{ stdout: string; stderr: string; code: number | null; }> {
|
||||
function execCommand(client: Client, command: string): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.exec(command, { pty: false }, (err, stream) => {
|
||||
client.exec(command, {pty: false}, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let exitCode: number | null = null;
|
||||
stream.on('close', (code: number | undefined) => {
|
||||
exitCode = typeof code === 'number' ? code : null;
|
||||
resolve({ stdout, stderr, code: exitCode });
|
||||
resolve({stdout, stderr, code: exitCode});
|
||||
}).on('data', (data: Buffer) => {
|
||||
stdout += data.toString('utf8');
|
||||
}).stderr.on('data', (data: Buffer) => {
|
||||
@@ -190,9 +194,9 @@ function parseCpuLine(cpuLine: string): { total: number; idle: number } | undefi
|
||||
if (parts[0] !== 'cpu') return undefined;
|
||||
const nums = parts.slice(1).map(n => Number(n)).filter(n => Number.isFinite(n));
|
||||
if (nums.length < 4) return undefined;
|
||||
const idle = (nums[3] ?? 0) + (nums[4] ?? 0); // idle + iowait
|
||||
const idle = (nums[3] ?? 0) + (nums[4] ?? 0);
|
||||
const total = nums.reduce((a, b) => a + b, 0);
|
||||
return { total, idle };
|
||||
return {total, idle};
|
||||
}
|
||||
|
||||
function toFixedNum(n: number | null | undefined, digits = 2): number | null {
|
||||
@@ -210,7 +214,6 @@ async function collectMetrics(host: HostRecord): Promise<{
|
||||
disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null };
|
||||
}> {
|
||||
return withSshConnection(host, async (client) => {
|
||||
// CPU
|
||||
let cpuPercent: number | null = null;
|
||||
let cores: number | null = null;
|
||||
let loadTriplet: [number, number, number] | null = null;
|
||||
@@ -245,7 +248,6 @@ async function collectMetrics(host: HostRecord): Promise<{
|
||||
loadTriplet = null;
|
||||
}
|
||||
|
||||
// Memory
|
||||
let memPercent: number | null = null;
|
||||
let usedGiB: number | null = null;
|
||||
let totalGiB: number | null = null;
|
||||
@@ -256,7 +258,7 @@ async function collectMetrics(host: HostRecord): Promise<{
|
||||
const line = lines.find(l => l.startsWith(key));
|
||||
if (!line) return null;
|
||||
const m = line.match(/\d+/);
|
||||
return m ? Number(m[0]) : null; // in kB
|
||||
return m ? Number(m[0]) : null;
|
||||
};
|
||||
const totalKb = getVal('MemTotal:');
|
||||
const availKb = getVal('MemAvailable:');
|
||||
@@ -272,14 +274,12 @@ async function collectMetrics(host: HostRecord): Promise<{
|
||||
totalGiB = null;
|
||||
}
|
||||
|
||||
// Disk
|
||||
let diskPercent: number | null = null;
|
||||
let usedHuman: string | null = null;
|
||||
let totalHuman: string | null = null;
|
||||
try {
|
||||
const diskOut = await execCommand(client, 'df -h -P / | tail -n +2');
|
||||
const line = diskOut.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
|
||||
// Expected columns: Filesystem Size Used Avail Use% Mounted
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 6) {
|
||||
totalHuman = parts[1] || null;
|
||||
@@ -295,9 +295,13 @@ async function collectMetrics(host: HostRecord): Promise<{
|
||||
}
|
||||
|
||||
return {
|
||||
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
|
||||
memory: { percent: toFixedNum(memPercent, 0), usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null, totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null },
|
||||
disk: { percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman },
|
||||
cpu: {percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet},
|
||||
memory: {
|
||||
percent: toFixedNum(memPercent, 0),
|
||||
usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null,
|
||||
totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null
|
||||
},
|
||||
disk: {percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -310,7 +314,10 @@ function tcpPing(host: string, port: number, timeoutMs = 5000): Promise<boolean>
|
||||
const onDone = (result: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
try { socket.destroy(); } catch {}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {
|
||||
}
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
@@ -334,7 +341,7 @@ async function pollStatusesOnce(): Promise<void> {
|
||||
|
||||
const checks = hosts.map(async (h) => {
|
||||
const isOnline = await tcpPing(h.ip, h.port, 5000);
|
||||
hostStatuses.set(h.id, { status: isOnline ? 'online' : 'offline', lastChecked: now });
|
||||
hostStatuses.set(h.id, {status: isOnline ? 'online' : 'offline', lastChecked: now});
|
||||
return isOnline;
|
||||
});
|
||||
|
||||
@@ -344,7 +351,6 @@ async function pollStatusesOnce(): Promise<void> {
|
||||
}
|
||||
|
||||
app.get('/status', async (req, res) => {
|
||||
// Return current cached statuses; if empty, trigger a poll
|
||||
if (hostStatuses.size === 0) {
|
||||
await pollStatusesOnce();
|
||||
}
|
||||
@@ -358,7 +364,7 @@ app.get('/status', async (req, res) => {
|
||||
app.get('/status/:id', async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'Invalid id' });
|
||||
return res.status(400).json({error: 'Invalid id'});
|
||||
}
|
||||
|
||||
if (!hostStatuses.has(id)) {
|
||||
@@ -367,34 +373,34 @@ app.get('/status/:id', async (req, res) => {
|
||||
|
||||
const entry = hostStatuses.get(id);
|
||||
if (!entry) {
|
||||
return res.status(404).json({ error: 'Host not found' });
|
||||
return res.status(404).json({error: 'Host not found'});
|
||||
}
|
||||
res.json(entry);
|
||||
});
|
||||
|
||||
app.post('/refresh', async (req, res) => {
|
||||
await pollStatusesOnce();
|
||||
res.json({ message: 'Refreshed' });
|
||||
res.json({message: 'Refreshed'});
|
||||
});
|
||||
|
||||
app.get('/metrics/:id', async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'Invalid id' });
|
||||
return res.status(400).json({error: 'Invalid id'});
|
||||
}
|
||||
try {
|
||||
const host = await fetchHostById(id);
|
||||
if (!host) {
|
||||
return res.status(404).json({ error: 'Host not found' });
|
||||
return res.status(404).json({error: 'Host not found'});
|
||||
}
|
||||
const metrics = await collectMetrics(host);
|
||||
res.json({ ...metrics, lastChecked: new Date().toISOString() });
|
||||
res.json({...metrics, lastChecked: new Date().toISOString()});
|
||||
} catch (err) {
|
||||
logger.error('Failed to collect metrics', err);
|
||||
return res.json({
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedHuman: null, totalHuman: null },
|
||||
cpu: {percent: null, cores: null, load: null},
|
||||
memory: {percent: null, usedGiB: null, totalGiB: null},
|
||||
disk: {percent: null, usedHuman: null, totalHuman: null},
|
||||
lastChecked: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
@@ -409,7 +415,6 @@ app.listen(PORT, async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Background polling every minute
|
||||
setInterval(() => {
|
||||
pollStatusesOnce().catch(err => logger.error('Background poll failed', err));
|
||||
}, 60_000);
|
||||
|
||||
Reference in New Issue
Block a user