Improve logging more, fix credentials sync issues, migrate more to be toasts
This commit is contained in:
@@ -35,7 +35,6 @@ class GitHubCache {
|
||||
timestamp: now,
|
||||
expiresAt: now + this.CACHE_DURATION
|
||||
});
|
||||
// Cache entry set
|
||||
}
|
||||
|
||||
get(key: string): any | null {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`}});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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')}));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user