diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 18b8416a..2d1bec89 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -691,4 +691,116 @@ router.delete('/config_editor/shortcuts', authenticateJWT, async (req: Request, } }); +// Route: Bulk import SSH hosts from JSON (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) { + logger.warn('Invalid bulk import data - hosts array is required and must not be empty'); + return res.status(400).json({error: 'Hosts array is required and must not be empty'}); + } + + if (hosts.length > 100) { + logger.warn(`Bulk import attempted with too many hosts: ${hosts.length}`); + 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 { + if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) { + results.failed++; + results.errors.push(`Host ${i + 1}: Missing or invalid required fields (ip, port, username)`); + continue; + } + + if (hostData.authType !== 'password' && hostData.authType !== 'key') { + results.failed++; + results.errors.push(`Host ${i + 1}: Invalid authType. Must be 'password' or 'key'`); + continue; + } + + 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}: SSH key required for key authentication`); + continue; + } + + if (hostData.enableTunnel && Array.isArray(hostData.tunnelConnections)) { + for (let j = 0; j < hostData.tunnelConnections.length; j++) { + const conn = hostData.tunnelConnections[j]; + if (!isValidPort(conn.sourcePort) || !isValidPort(conn.endpointPort) || !isNonEmptyString(conn.endpointHost)) { + results.failed++; + results.errors.push(`Host ${i + 1}, Tunnel ${j + 1}: Invalid tunnel connection data`); + break; + } + } + } + + const sshDataObj: any = { + userId: userId, + name: hostData.name || '', + folder: hostData.folder || '', + tags: Array.isArray(hostData.tags) ? hostData.tags.join(',') : (hostData.tags || ''), + ip: hostData.ip, + port: hostData.port, + username: hostData.username, + authType: hostData.authType, + pin: !!hostData.pin ? 1 : 0, + enableTerminal: !!hostData.enableTerminal ? 1 : 0, + enableTunnel: !!hostData.enableTunnel ? 1 : 0, + tunnelConnections: Array.isArray(hostData.tunnelConnections) ? JSON.stringify(hostData.tunnelConnections) : null, + enableConfigEditor: !!hostData.enableConfigEditor ? 1 : 0, + defaultPath: hostData.defaultPath || null, + }; + + if (hostData.authType === 'password') { + sshDataObj.password = hostData.password; + sshDataObj.key = null; + sshDataObj.keyPassword = null; + sshDataObj.keyType = null; + } else if (hostData.authType === 'key') { + sshDataObj.key = hostData.key; + sshDataObj.keyPassword = hostData.keyPassword || null; + sshDataObj.keyType = hostData.keyType || null; + sshDataObj.password = null; + } + + await db.insert(sshData).values(sshDataObj); + results.success++; + + } catch (err) { + results.failed++; + results.errors.push(`Host ${i + 1}: ${err instanceof Error ? err.message : 'Unknown error'}`); + logger.error(`Failed to import host ${i + 1}:`, err); + } + } + + if (results.success > 0) { + logger.success(`Bulk import completed: ${results.success} successful, ${results.failed} failed`); + } else { + logger.warn(`Bulk import failed: ${results.failed} failed`); + } + + res.json({ + message: `Import completed: ${results.success} successful, ${results.failed} failed`, + ...results + }); +}); + export default router; \ No newline at end of file