Improve logging more, fix credentials sync issues, migrate more to be toasts

This commit is contained in:
LukeGus
2025-09-09 18:31:29 -05:00
parent 4c33b43a0f
commit 797e022d6e
26 changed files with 1650 additions and 611 deletions

View File

@@ -35,7 +35,6 @@ class GitHubCache {
timestamp: now,
expiresAt: now + this.CACHE_DURATION
});
// Cache entry set
}
get(key: string): any | null {

View File

@@ -177,7 +177,6 @@ router.post('/dismiss', async (req, res) => {
alertId
});
authLogger.success(`Alert ${alertId} dismissed by user ${userId}. Insert result: ${JSON.stringify(result)}`);
res.json({message: 'Alert dismissed successfully'});
} catch (error) {
authLogger.error('Failed to dismiss alert', error);
@@ -233,8 +232,6 @@ router.delete('/dismiss', async (req, res) => {
if (result.changes === 0) {
return res.status(404).json({error: 'Dismissed alert not found'});
}
authLogger.success(`Alert ${alertId} undismissed by user ${userId}`);
res.json({message: 'Alert undismissed successfully'});
} catch (error) {
authLogger.error('Failed to undismiss alert', error);

View File

@@ -54,8 +54,6 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
keyType
} = req.body;
authLogger.info('Credential creation request received', { operation: 'credential_create', userId, name, authType, username });
if (!isNonEmptyString(userId) || !isNonEmptyString(name) || !isNonEmptyString(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'});
@@ -75,8 +73,6 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
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'});
}
authLogger.info('Preparing credential data for database insertion', { operation: 'credential_create', userId, name, authType, hasPassword: !!password, hasKey: !!key });
const plainPassword = (authType === 'password' && password) ? password : null;
const plainKey = (authType === 'key' && key) ? key : null;
const plainKeyPassword = (authType === 'key' && keyPassword) ? keyPassword : null;
@@ -97,10 +93,18 @@ router.post('/', authenticateJWT, async (req: Request, res: Response) => {
lastUsed: null,
};
authLogger.info('Inserting credential into database', { operation: 'credential_create', userId, name, authType, username });
const result = await db.insert(sshCredentials).values(credentialData).returning();
const created = result[0];
authLogger.success('Credential created successfully', { operation: 'credential_create', userId, name, credentialId: created.id, authType, username });
authLogger.success(`SSH credential created: ${name} (${authType}) by user ${userId}`, {
operation: 'credential_create_success',
userId,
credentialId: created.id,
name,
authType,
username
});
res.status(201).json(formatCredentialOutput(created));
} catch (err) {
authLogger.error('Failed to create credential in database', err, { operation: 'credential_create', userId, name, authType, username });
@@ -280,6 +284,16 @@ router.put('/:id', authenticateJWT, async (req: Request, res: Response) => {
.from(sshCredentials)
.where(eq(sshCredentials.id, parseInt(id)));
const credential = updated[0];
authLogger.success(`SSH credential updated: ${credential.name} (${credential.authType}) by user ${userId}`, {
operation: 'credential_update_success',
userId,
credentialId: parseInt(id),
name: credential.name,
authType: credential.authType,
username: credential.username
});
res.json(formatCredentialOutput(updated[0]));
} catch (err) {
authLogger.error('Failed to update credential', err);
@@ -301,6 +315,18 @@ router.delete('/:id', authenticateJWT, async (req: Request, res: Response) => {
}
try {
const credentialToDelete = await db
.select()
.from(sshCredentials)
.where(and(
eq(sshCredentials.id, parseInt(id)),
eq(sshCredentials.userId, userId)
));
if (credentialToDelete.length === 0) {
return res.status(404).json({error: 'Credential not found'});
}
const hostsUsingCredential = await db
.select()
.from(sshData)
@@ -339,6 +365,16 @@ router.delete('/:id', authenticateJWT, async (req: Request, res: Response) => {
eq(sshCredentials.userId, userId)
));
const credential = credentialToDelete[0];
authLogger.success(`SSH credential deleted: ${credential.name} (${credential.authType}) by user ${userId}`, {
operation: 'credential_delete_success',
userId,
credentialId: parseInt(id),
name: credential.name,
authType: credential.authType,
username: credential.username
});
res.json({message: 'Credential deleted successfully'});
} catch (err) {
authLogger.error('Failed to delete credential', err);
@@ -489,4 +525,33 @@ function formatSSHHostOutput(host: any): any {
};
}
// Rename a credential folder
// PUT /credentials/folders/rename
router.put('/folders/rename', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
const { oldName, newName } = req.body;
if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) {
return res.status(400).json({ error: 'Both oldName and newName are required' });
}
if (oldName === newName) {
return res.status(400).json({ error: 'Old name and new name cannot be the same' });
}
try {
await db.update(sshCredentials)
.set({ folder: newName })
.where(and(
eq(sshCredentials.userId, userId),
eq(sshCredentials.folder, oldName)
));
res.json({ success: true, message: 'Folder renamed successfully' });
} catch (error) {
authLogger.error('Error renaming credential folder:', error);
res.status(500).json({ error: 'Failed to rename folder' });
}
});
export default router;

View File

@@ -182,6 +182,16 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
const resolvedHost = await resolveHostCredentials(baseHost) || baseHost;
sshLogger.success(`SSH host created: ${name} (${ip}:${port}) by user ${userId}`, {
operation: 'host_create_success',
userId,
hostId: createdHost.id,
name,
ip,
port,
authType: effectiveAuthType
});
res.json(resolvedHost);
} catch (err) {
sshLogger.error('Failed to save SSH host to database', err, { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType });
@@ -315,6 +325,16 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
const resolvedHost = await resolveHostCredentials(baseHost) || baseHost;
sshLogger.success(`SSH host updated: ${name} (${ip}:${port}) by user ${userId}`, {
operation: 'host_update_success',
userId,
hostId: parseInt(hostId),
name,
ip,
port,
authType: effectiveAuthType
});
res.json(resolvedHost);
} catch (err) {
sshLogger.error('Failed to update SSH host in database', err, { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType });
@@ -407,8 +427,29 @@ router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Respons
return res.status(400).json({error: 'Invalid userId or id'});
}
try {
const hostToDelete = await db
.select()
.from(sshData)
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
if (hostToDelete.length === 0) {
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'});
}
const result = await db.delete(sshData)
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
const host = hostToDelete[0];
sshLogger.success(`SSH host deleted: ${host.name} (${host.ip}:${host.port}) by user ${userId}`, {
operation: 'host_delete_success',
userId,
hostId: parseInt(hostId),
name: host.name,
ip: host.ip,
port: host.port
});
res.json({message: 'SSH host deleted'});
} catch (err) {
sshLogger.error('Failed to delete SSH host from database', err, { operation: 'host_delete', hostId: parseInt(hostId), userId });
@@ -777,15 +818,6 @@ router.put('/folders/rename', authenticateJWT, async (req: Request, res: Respons
))
.returning();
sshLogger.success('Folder renamed successfully', {
operation: 'folder_rename',
userId,
oldName,
newName,
updatedHosts: updatedHosts.length,
updatedCredentials: updatedCredentials.length
});
res.json({
message: 'Folder renamed successfully',
updatedHosts: updatedHosts.length,
@@ -797,4 +829,103 @@ router.put('/folders/rename', authenticateJWT, async (req: Request, res: Respons
}
});
// Route: Bulk import SSH hosts (requires JWT)
// POST /ssh/bulk-import
router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
const { hosts } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) {
return res.status(400).json({ error: 'Hosts array is required and must not be empty' });
}
if (hosts.length > 100) {
return res.status(400).json({ error: 'Maximum 100 hosts allowed per import' });
}
const results = {
success: 0,
failed: 0,
errors: [] as string[]
};
for (let i = 0; i < hosts.length; i++) {
const hostData = hosts[i];
try {
// Validate required fields
if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) {
results.failed++;
results.errors.push(`Host ${i + 1}: Missing required fields (ip, port, username)`);
continue;
}
// Validate authType
if (!['password', 'key', 'credential'].includes(hostData.authType)) {
results.failed++;
results.errors.push(`Host ${i + 1}: Invalid authType. Must be 'password', 'key', or 'credential'`);
continue;
}
// Validate authentication data based on authType
if (hostData.authType === 'password' && !isNonEmptyString(hostData.password)) {
results.failed++;
results.errors.push(`Host ${i + 1}: Password required for password authentication`);
continue;
}
if (hostData.authType === 'key' && !isNonEmptyString(hostData.key)) {
results.failed++;
results.errors.push(`Host ${i + 1}: Key required for key authentication`);
continue;
}
if (hostData.authType === 'credential' && !hostData.credentialId) {
results.failed++;
results.errors.push(`Host ${i + 1}: credentialId required for credential authentication`);
continue;
}
// Prepare host data for insertion
const sshDataObj: any = {
userId: userId,
name: hostData.name || `${hostData.username}@${hostData.ip}`,
folder: hostData.folder || 'Default',
tags: Array.isArray(hostData.tags) ? hostData.tags.join(',') : '',
ip: hostData.ip,
port: hostData.port,
username: hostData.username,
password: hostData.authType === 'password' ? hostData.password : null,
authType: hostData.authType,
credentialId: hostData.authType === 'credential' ? hostData.credentialId : null,
key: hostData.authType === 'key' ? hostData.key : null,
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : null,
keyType: hostData.authType === 'key' ? (hostData.keyType || 'auto') : null,
pin: hostData.pin || false,
enableTerminal: hostData.enableTerminal !== false,
enableTunnel: hostData.enableTunnel !== false,
enableFileManager: hostData.enableFileManager !== false,
defaultPath: hostData.defaultPath || '/',
tunnelConnections: hostData.tunnelConnections ? JSON.stringify(hostData.tunnelConnections) : '[]',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
await db.insert(sshData).values(sshDataObj);
results.success++;
} catch (error) {
results.failed++;
results.errors.push(`Host ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
res.json({
message: `Import completed: ${results.success} successful, ${results.failed} failed`,
success: results.success,
failed: results.failed,
errors: results.errors
});
});
export default router;

View File

@@ -135,8 +135,9 @@ router.post('/create', async (req, res) => {
if (row && (row as any).value !== 'true') {
return res.status(403).json({error: 'Registration is currently disabled'});
}
} catch (e) {
}
} catch (e) {
authLogger.warn('Failed to check registration status', { operation: 'registration_check', error: e });
}
const {username, password} = req.body;
@@ -159,19 +160,14 @@ router.post('/create', async (req, res) => {
try {
const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get();
isFirstUser = ((countResult as any)?.count || 0) === 0;
authLogger.info('Checked user count for admin status', { operation: 'user_create', username, isFirstUser });
} catch (e) {
isFirstUser = true;
authLogger.warn('Failed to check user count, assuming first user', { operation: 'user_create', username, error: e });
}
authLogger.info('Hashing password for new user', { operation: 'user_create', username, saltRounds: parseInt(process.env.SALT || '10', 10) });
const saltRounds = parseInt(process.env.SALT || '10', 10);
const password_hash = await bcrypt.hash(password, saltRounds);
const id = nanoid();
authLogger.info('Generated user ID and hashed password', { operation: 'user_create', username, userId: id });
authLogger.info('Inserting new user into database', { operation: 'user_create', username, userId: id, isAdmin: isFirstUser });
await db.insert(users).values({
id,
username,
@@ -192,7 +188,7 @@ router.post('/create', async (req, res) => {
});
authLogger.success(`Traditional user created: ${username} (is_admin: ${isFirstUser})`, { operation: 'user_create', username, isAdmin: isFirstUser, userId: id });
res.json({message: 'User created', is_admin: isFirstUser});
res.json({message: 'User created', is_admin: isFirstUser, toast: {type: 'success', message: `User created: ${username}`}});
} catch (err) {
authLogger.error('Failed to create user', err);
res.status(500).json({error: 'Failed to create user'});
@@ -220,27 +216,6 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
name_path,
scopes
} = req.body;
authLogger.info('OIDC config update request received', {
operation: 'oidc_config_update',
userId,
hasClientId: !!client_id,
hasClientSecret: !!client_secret,
hasIssuerUrl: !!issuer_url,
hasAuthUrl: !!authorization_url,
hasTokenUrl: !!token_url,
hasIdentifierPath: !!identifier_path,
hasNamePath: !!name_path,
clientIdValue: `"${client_id}"`,
clientSecretValue: client_secret ? '[REDACTED]' : `"${client_secret}"`,
issuerUrlValue: `"${issuer_url}"`,
authUrlValue: `"${authorization_url}"`,
tokenUrlValue: `"${token_url}"`,
identifierPathValue: `"${identifier_path}"`,
namePathValue: `"${name_path}"`,
scopesValue: `"${scopes}"`,
userinfoUrlValue: `"${userinfo_url}"`
});
const isDisableRequest = (client_id === '' || client_id === null || client_id === undefined) &&
(client_secret === '' || client_secret === null || client_secret === undefined) &&
@@ -253,29 +228,6 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
isNonEmptyString(token_url) && isNonEmptyString(identifier_path) &&
isNonEmptyString(name_path);
authLogger.info('OIDC validation results', {
operation: 'oidc_config_update',
userId,
isDisableRequest,
isEnableRequest,
disableChecks: {
clientIdEmpty: client_id === '' || client_id === null || client_id === undefined,
clientSecretEmpty: client_secret === '' || client_secret === null || client_secret === undefined,
issuerUrlEmpty: issuer_url === '' || issuer_url === null || issuer_url === undefined,
authUrlEmpty: authorization_url === '' || authorization_url === null || authorization_url === undefined,
tokenUrlEmpty: token_url === '' || token_url === null || token_url === undefined
},
enableChecks: {
clientIdPresent: isNonEmptyString(client_id),
clientSecretPresent: isNonEmptyString(client_secret),
issuerUrlPresent: isNonEmptyString(issuer_url),
authUrlPresent: isNonEmptyString(authorization_url),
tokenUrlPresent: isNonEmptyString(token_url),
identifierPathPresent: isNonEmptyString(identifier_path),
namePathPresent: isNonEmptyString(name_path)
}
});
if (!isDisableRequest && !isEnableRequest) {
authLogger.warn('OIDC validation failed - neither disable nor enable request', {
operation: 'oidc_config_update',
@@ -287,7 +239,6 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
}
if (isDisableRequest) {
// Disable OIDC by removing the configuration
db.$client.prepare("DELETE FROM settings WHERE key = 'oidc_config'").run();
authLogger.info('OIDC configuration disabled', { operation: 'oidc_disable', userId });
res.json({message: 'OIDC configuration disabled'});
@@ -324,8 +275,6 @@ router.delete('/oidc-config', authenticateJWT, async (req, res) => {
if (!user || user.length === 0 || !user[0].is_admin) {
return res.status(403).json({error: 'Not authorized'});
}
authLogger.info('OIDC disable request received', { operation: 'oidc_disable', userId });
db.$client.prepare("DELETE FROM settings WHERE key = 'oidc_config'").run();
authLogger.success('OIDC configuration disabled', { operation: 'oidc_disable', userId });
@@ -480,7 +429,6 @@ router.get('/oidc/callback', async (req, res) => {
if (tokenData.id_token) {
try {
userInfo = await verifyOIDCToken(tokenData.id_token, config.issuer_url, config.client_id);
authLogger.info('Successfully verified ID token and extracted user info');
} catch (error) {
authLogger.error('OIDC token verification failed, trying userinfo endpoints', error);
try {
@@ -488,7 +436,6 @@ router.get('/oidc/callback', async (req, res) => {
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
userInfo = payload;
authLogger.info('Successfully decoded ID token payload without verification');
}
} catch (decodeError) {
authLogger.error('Failed to decode ID token payload:', decodeError);
@@ -586,6 +533,8 @@ router.get('/oidc/callback', async (req, res) => {
.select()
.from(users)
.where(eq(users.id, id));
// OIDC user created - toast notification handled by frontend
} else {
await db.update(users)
.set({username: name})
@@ -595,6 +544,8 @@ router.get('/oidc/callback', async (req, res) => {
.select()
.from(users)
.where(eq(users.id, user[0].id));
// OIDC user logged in - toast notification handled by frontend
}
const userRecord = user[0];
@@ -660,34 +611,29 @@ router.post('/login', async (req, res) => {
return res.status(403).json({error: 'This user uses external authentication'});
}
authLogger.info('Verifying password for user login', { operation: 'user_login', username, userId: userRecord.id });
const isMatch = await bcrypt.compare(password, userRecord.password_hash);
if (!isMatch) {
authLogger.warn(`Incorrect password for user: ${username}`, { operation: 'user_login', username, userId: userRecord.id });
return res.status(401).json({error: 'Incorrect password'});
}
authLogger.info('Password verified, generating JWT token', { operation: 'user_login', username, userId: userRecord.id, totpEnabled: userRecord.totp_enabled });
const jwtSecret = process.env.JWT_SECRET || 'secret';
const token = jwt.sign({userId: userRecord.id}, jwtSecret, {
expiresIn: '50d',
});
// Traditional user logged in - toast notification handled by frontend
if (userRecord.totp_enabled) {
authLogger.info('User has TOTP enabled, requiring additional verification', { operation: 'user_login', username, userId: userRecord.id });
const tempToken = jwt.sign(
{userId: userRecord.id, pending_totp: true},
jwtSecret,
{expiresIn: '10m'}
);
authLogger.success('TOTP verification required for login', { operation: 'user_login', username, userId: userRecord.id });
return res.json({
requires_totp: true,
temp_token: tempToken
});
}
authLogger.success('User login successful', { operation: 'user_login', username, userId: userRecord.id, isAdmin: !!userRecord.is_admin });
return res.json({
token,
is_admin: !!userRecord.is_admin,
@@ -1022,6 +968,7 @@ router.post('/make-admin', authenticateJWT, async (req, res) => {
.where(eq(users.username, 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`});
} catch (err) {
@@ -1064,6 +1011,7 @@ router.post('/remove-admin', authenticateJWT, async (req, res) => {
.where(eq(users.username, 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}`});
} catch (err) {
@@ -1125,6 +1073,8 @@ router.post('/totp/verify-login', async (req, res) => {
expiresIn: '50d',
});
// TOTP login completed - toast notification handled by frontend
return res.json({
token,
is_admin: !!userRecord.is_admin,
@@ -1224,6 +1174,7 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => {
})
.where(eq(users.id, userId));
// 2FA enabled - toast notification handled by frontend
res.json({
message: 'TOTP enabled successfully',
backup_codes: backupCodes
@@ -1285,6 +1236,7 @@ router.post('/totp/disable', authenticateJWT, async (req, res) => {
})
.where(eq(users.id, userId));
// 2FA disabled - toast notification handled by frontend
res.json({message: 'TOTP disabled successfully'});
} catch (err) {
@@ -1401,6 +1353,7 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => {
await db.delete(users).where(eq(users.id, targetUserId));
authLogger.success(`User ${username} deleted by admin ${adminUser[0].username}`);
// User deleted - toast notification handled by frontend
res.json({message: `User ${username} deleted successfully`});
} catch (err) {

View File

@@ -307,8 +307,8 @@ app.get('/ssh/file_manager/ssh/readFile', (req, res) => {
});
});
app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
const {sessionId, path: filePath, content} = req.body;
app.post('/ssh/file_manager/ssh/writeFile', async (req, res) => {
const {sessionId, path: filePath, content, hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -371,7 +371,7 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
if (hasError || hasFinished) return;
hasFinished = true;
if (!res.headersSent) {
res.json({message: 'File written successfully', path: filePath});
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
}
});
@@ -379,7 +379,7 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
if (hasError || hasFinished) return;
hasFinished = true;
if (!res.headersSent) {
res.json({message: 'File written successfully', path: filePath});
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
}
});
@@ -430,10 +430,10 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
stream.on('close', (code) => {
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'File written successfully', path: filePath});
}
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'File written successfully', path: filePath, toast: {type: 'success', message: `File written: ${filePath}`}});
}
} else {
fileLogger.error(`Fallback write failed with code ${code}: ${errorData}`);
if (!res.headersSent) {
@@ -462,8 +462,8 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
trySFTP();
});
app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
const {sessionId, path: filePath, content, fileName} = req.body;
app.post('/ssh/file_manager/ssh/uploadFile', async (req, res) => {
const {sessionId, path: filePath, content, fileName, hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -527,7 +527,7 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
if (hasError || hasFinished) return;
hasFinished = true;
if (!res.headersSent) {
res.json({message: 'File uploaded successfully', path: fullPath});
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
}
});
@@ -535,7 +535,7 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
if (hasError || hasFinished) return;
hasFinished = true;
if (!res.headersSent) {
res.json({message: 'File uploaded successfully', path: fullPath});
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
}
});
@@ -597,9 +597,8 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
if (outputData.includes('SUCCESS')) {
fileLogger.success(`File uploaded successfully via fallback: ${fullPath}`);
if (!res.headersSent) {
res.json({message: 'File uploaded successfully', path: fullPath});
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
}
} else {
fileLogger.error(`Fallback upload failed with code ${code}: ${errorData}`);
@@ -655,9 +654,8 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
if (outputData.includes('SUCCESS')) {
fileLogger.success(`File uploaded successfully via chunked fallback: ${fullPath}`);
if (!res.headersSent) {
res.json({message: 'File uploaded successfully', path: fullPath});
res.json({message: 'File uploaded successfully', path: fullPath, toast: {type: 'success', message: `File uploaded: ${fullPath}`}});
}
} else {
fileLogger.error(`Chunked fallback upload failed with code ${code}: ${errorData}`);
@@ -686,8 +684,8 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
trySFTP();
});
app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
const {sessionId, path: filePath, fileName, content = ''} = req.body;
app.post('/ssh/file_manager/ssh/createFile', async (req, res) => {
const {sessionId, path: filePath, fileName, content = '', hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -742,7 +740,7 @@ app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
stream.on('close', (code) => {
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'File created successfully', path: fullPath});
res.json({message: 'File created successfully', path: fullPath, toast: {type: 'success', message: `File created: ${fullPath}`}});
}
return;
}
@@ -756,7 +754,7 @@ app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
}
if (!res.headersSent) {
res.json({message: 'File created successfully', path: fullPath});
res.json({message: 'File created successfully', path: fullPath, toast: {type: 'success', message: `File created: ${fullPath}`}});
}
});
@@ -769,8 +767,8 @@ app.post('/ssh/file_manager/ssh/createFile', (req, res) => {
});
});
app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
const {sessionId, path: folderPath, folderName} = req.body;
app.post('/ssh/file_manager/ssh/createFolder', async (req, res) => {
const {sessionId, path: folderPath, folderName, hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -826,7 +824,7 @@ app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
stream.on('close', (code) => {
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'Folder created successfully', path: fullPath});
res.json({message: 'Folder created successfully', path: fullPath, toast: {type: 'success', message: `Folder created: ${fullPath}`}});
}
return;
}
@@ -840,7 +838,7 @@ app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
}
if (!res.headersSent) {
res.json({message: 'Folder created successfully', path: fullPath});
res.json({message: 'Folder created successfully', path: fullPath, toast: {type: 'success', message: `Folder created: ${fullPath}`}});
}
});
@@ -853,8 +851,8 @@ app.post('/ssh/file_manager/ssh/createFolder', (req, res) => {
});
});
app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
const {sessionId, path: itemPath, isDirectory} = req.body;
app.delete('/ssh/file_manager/ssh/deleteItem', async (req, res) => {
const {sessionId, path: itemPath, isDirectory, hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -909,7 +907,7 @@ app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
stream.on('close', (code) => {
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'Item deleted successfully', path: itemPath});
res.json({message: 'Item deleted successfully', path: itemPath, toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}});
}
return;
}
@@ -923,7 +921,7 @@ app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
}
if (!res.headersSent) {
res.json({message: 'Item deleted successfully', path: itemPath});
res.json({message: 'Item deleted successfully', path: itemPath, toast: {type: 'success', message: `${isDirectory ? 'Directory' : 'File'} deleted: ${itemPath}`}});
}
});
@@ -936,8 +934,8 @@ app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => {
});
});
app.put('/ssh/file_manager/ssh/renameItem', (req, res) => {
const {sessionId, oldPath, newName} = req.body;
app.put('/ssh/file_manager/ssh/renameItem', async (req, res) => {
const {sessionId, oldPath, newName, hostId, userId} = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -994,7 +992,7 @@ app.put('/ssh/file_manager/ssh/renameItem', (req, res) => {
stream.on('close', (code) => {
if (outputData.includes('SUCCESS')) {
if (!res.headersSent) {
res.json({message: 'Item renamed successfully', oldPath, newPath});
res.json({message: 'Item renamed successfully', oldPath, newPath, toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}});
}
return;
}
@@ -1008,7 +1006,7 @@ app.put('/ssh/file_manager/ssh/renameItem', (req, res) => {
}
if (!res.headersSent) {
res.json({message: 'Item renamed successfully', oldPath, newPath});
res.json({message: 'Item renamed successfully', oldPath, newPath, toast: {type: 'success', message: `Item renamed: ${oldPath} -> ${newPath}`}});
}
});

View File

@@ -16,8 +16,6 @@ wss.on('connection', (ws: WebSocket) => {
let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null;
sshLogger.info('New WebSocket connection established', { operation: 'websocket_connect' });
ws.on('close', () => {
@@ -40,7 +38,6 @@ wss.on('connection', (ws: WebSocket) => {
switch (type) {
case 'connectToHost':
sshLogger.info('SSH connection request received', { operation: 'ssh_connect', hostId: data.hostConfig?.id, ip: data.hostConfig?.ip, port: data.hostConfig?.port });
handleConnectToHost(data).catch(error => {
sshLogger.error('Failed to connect to host', 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')}));

View File

@@ -382,12 +382,7 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
const tunnelName = tunnelConfig.name;
const tunnelMarker = getTunnelMarker(tunnelName);
if (retryAttempt === 0) {
tunnelLogger.info('SSH tunnel connection attempt started', { operation: 'tunnel_connect', tunnelName, sourceIP: tunnelConfig.sourceIP, sourcePort: tunnelConfig.sourceSSHPort });
}
if (manualDisconnects.has(tunnelName)) {
tunnelLogger.info('Tunnel connection cancelled due to manual disconnect', { operation: 'tunnel_connect', tunnelName });
return;
}

View File

@@ -6,15 +6,23 @@ import './ssh/terminal.js';
import './ssh/tunnel.js';
import './ssh/file-manager.js';
import './ssh/server-stats.js';
import { systemLogger } from './utils/logger.js';
import { systemLogger, versionLogger } from './utils/logger.js';
import 'dotenv/config';
(async () => {
try {
const version = process.env.VERSION || 'unknown';
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
operation: 'startup',
version: version
});
systemLogger.info("Initializing backend services...", { operation: 'startup' });
systemLogger.success("All backend services initialized successfully", {
operation: 'startup_complete',
services: ['database', 'terminal', 'tunnel', 'file_manager', 'stats']
services: ['database', 'terminal', 'tunnel', 'file_manager', 'stats'],
version: version
});
process.on('SIGINT', () => {

View File

@@ -158,13 +158,14 @@ class Logger {
}
}
export const databaseLogger = new Logger('DATABASE', '🗄️', '#1e3a8a');
export const sshLogger = new Logger('SSH', '🖥️', '#1e3a8a');
export const tunnelLogger = new Logger('TUNNEL', '📡', '#1e3a8a');
export const fileLogger = new Logger('FILE', '📁', '#1e3a8a');
export const databaseLogger = new Logger('DATABASE', '🗄️', '#6366f1');
export const sshLogger = new Logger('SSH', '🖥️', '#0ea5e9');
export const tunnelLogger = new Logger('TUNNEL', '📡', '#a855f7');
export const fileLogger = new Logger('FILE', '📁', '#f59e0b');
export const statsLogger = new Logger('STATS', '📊', '#22c55e');
export const apiLogger = new Logger('API', '🌐', '#3b82f6');
export const authLogger = new Logger('AUTH', '🔐', '#dc2626');
export const systemLogger = new Logger('SYSTEM', '🚀', '#1e3a8a');
export const authLogger = new Logger('AUTH', '🔐', '#ef4444');
export const systemLogger = new Logger('SYSTEM', '🚀', '#14b8a6');
export const versionLogger = new Logger('VERSION', '📦', '#8b5cf6');
export const logger = systemLogger;