diff --git a/package.json b/package.json index 0c4ff32c..ac7c263e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "vite build", "build:backend": "tsc -p tsconfig.node.json", - "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js", + "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js", "lint": "eslint .", "preview": "vite preview" }, diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 173bda4a..ce7bb04b 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -1,6 +1,6 @@ { "credentials": { - "credentialsManager": "Credentials Manager", + "credentialsViewer": "Credentials Viewer", "manageYourSSHCredentials": "Manage your SSH credentials securely", "addCredential": "Add Credential", "createCredential": "Create Credential", diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 4e820f46..9b026f8a 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -35,23 +35,20 @@ class GitHubCache { timestamp: now, expiresAt: now + this.CACHE_DURATION }); - databaseLogger.debug(`Cache entry set`, { operation: 'cache_set', key, expiresIn: this.CACHE_DURATION }); + // Cache entry set } get(key: string): any | null { const entry = this.cache.get(key); if (!entry) { - databaseLogger.debug(`Cache miss`, { operation: 'cache_get', key }); return null; } if (Date.now() > entry.expiresAt) { this.cache.delete(key); - databaseLogger.debug(`Cache entry expired`, { operation: 'cache_get', key, expired: true }); return null; } - databaseLogger.debug(`Cache hit`, { operation: 'cache_get', key, age: Date.now() - entry.timestamp }); return entry.data; } } @@ -83,7 +80,6 @@ interface GitHubRelease { async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise { const cachedData = githubCache.get(cacheKey); if (cachedData) { - databaseLogger.debug(`Using cached GitHub API data`, { operation: 'github_api', endpoint, cached: true }); return { data: cachedData, cached: true, @@ -92,7 +88,6 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise } try { - databaseLogger.info(`Fetching from GitHub API`, { operation: 'github_api', endpoint }); const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { headers: { 'Accept': 'application/vnd.github+json', @@ -108,7 +103,6 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise const data = await response.json(); githubCache.set(cacheKey, data); - databaseLogger.success(`GitHub API data fetched successfully`, { operation: 'github_api', endpoint, dataSize: JSON.stringify(data).length }); return { data: data, cached: false @@ -122,12 +116,10 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise app.use(bodyParser.json()); app.get('/health', (req, res) => { - apiLogger.info(`Health check requested`, { operation: 'health_check' }); res.json({status: 'ok'}); }); app.get('/version', async (req, res) => { - apiLogger.info(`Version check requested`, { operation: 'version_check' }); let localVersion = process.env.VERSION; if (!localVersion) { @@ -135,7 +127,6 @@ app.get('/version', async (req, res) => { const packagePath = path.resolve(process.cwd(), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); localVersion = packageJson.version; - databaseLogger.debug(`Version read from package.json`, { operation: 'version_check', localVersion }); } catch (error) { databaseLogger.error('Failed to read version from package.json', error, { operation: 'version_check' }); } @@ -163,13 +154,6 @@ app.get('/version', async (req, res) => { } const isUpToDate = localVersion === remoteVersion; - databaseLogger.info(`Version comparison completed`, { - operation: 'version_check', - localVersion, - remoteVersion, - isUpToDate, - cached: releaseData.cached - }); const response = { status: isUpToDate ? 'up_to_date' : 'requires_update', @@ -198,7 +182,7 @@ app.get('/releases/rss', async (req, res) => { const per_page = Math.min(parseInt(req.query.per_page as string) || 20, 100); const cacheKey = `releases_rss_${page}_${per_page}`; - apiLogger.info(`RSS releases requested`, { operation: 'rss_releases', page, per_page }); + // RSS releases requested const releasesData = await fetchGitHubAPI( `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, @@ -235,13 +219,7 @@ app.get('/releases/rss', async (req, res) => { cache_age: releasesData.cache_age }; - databaseLogger.success(`RSS releases generated successfully`, { - operation: 'rss_releases', - itemCount: rssItems.length, - page, - per_page, - cached: releasesData.cached - }); + // RSS releases generated successfully res.json(response); } catch (error) { diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index b24d7d02..58551910 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -165,13 +165,6 @@ const migrateSchema = () => { addColumnIfNotExists('users', 'issuer_url', 'TEXT'); addColumnIfNotExists('users', 'authorization_url', 'TEXT'); addColumnIfNotExists('users', 'token_url', 'TEXT'); - try { - databaseLogger.debug('Attempting to drop redirect_uri column', { operation: 'schema_migration', table: 'users' }); - sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run(); - databaseLogger.success('redirect_uri column dropped', { operation: 'schema_migration', table: 'users' }); - } catch (e) { - databaseLogger.debug('redirect_uri column does not exist or could not be dropped', { operation: 'schema_migration', table: 'users' }); - } addColumnIfNotExists('users', 'identifier_path', 'TEXT'); addColumnIfNotExists('users', 'name_path', 'TEXT'); diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index 05301626..8927c629 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -63,11 +63,8 @@ async function fetchAlertsFromGitHub(): Promise { const cacheKey = 'termix_alerts'; const cachedData = alertCache.get(cacheKey); if (cachedData) { - authLogger.info('Returning cached alerts from GitHub', { operation: 'alerts_fetch', cacheKey, alertCount: cachedData.length }); return cachedData; } - - authLogger.info('Fetching alerts from GitHub', { operation: 'alerts_fetch', url: `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}` }); try { const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}`; @@ -84,7 +81,6 @@ async function fetchAlertsFromGitHub(): Promise { } const alerts: TermixAlert[] = await response.json() as TermixAlert[]; - authLogger.info('Successfully fetched alerts from GitHub', { operation: 'alerts_fetch', totalAlerts: alerts.length }); const now = new Date(); @@ -94,9 +90,7 @@ async function fetchAlertsFromGitHub(): Promise { return isValid; }); - authLogger.info('Filtered alerts by expiry date', { operation: 'alerts_fetch', totalAlerts: alerts.length, validAlerts: validAlerts.length }); alertCache.set(cacheKey, validAlerts); - authLogger.success('Alerts cached successfully', { operation: 'alerts_fetch', alertCount: validAlerts.length }); return validAlerts; } catch (error) { authLogger.error('Failed to fetch alerts from GitHub', { operation: 'alerts_fetch', error: error instanceof Error ? error.message : 'Unknown error' }); diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 222cdc15..d3b43a92 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -76,16 +76,12 @@ router.get('/db/host/internal', async (req: Request, res: Response) => { // POST /ssh/host router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => { const userId = (req as any).userId; - sshLogger.info('SSH host creation request received', { operation: 'host_create', userId, contentType: req.headers['content-type'] }); - let hostData: any; if (req.headers['content-type']?.includes('multipart/form-data')) { - sshLogger.info('Processing multipart form data for SSH host creation', { operation: 'host_create', userId }); if (req.body.data) { try { hostData = JSON.parse(req.body.data); - sshLogger.info('Successfully parsed JSON data from multipart request', { operation: 'host_create', userId, hasKey: !!req.file }); } catch (err) { sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_create', userId, error: err }); return res.status(400).json({error: 'Invalid JSON data'}); @@ -97,11 +93,9 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque if (req.file) { hostData.key = req.file.buffer.toString('utf8'); - sshLogger.info('SSH key file processed from multipart request', { operation: 'host_create', userId, keySize: req.file.size }); } } else { hostData = req.body; - sshLogger.info('Processing JSON data for SSH host creation', { operation: 'host_create', userId }); } const { @@ -147,11 +141,11 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque username, authType: effectiveAuthType, credentialId: credentialId || null, - pin: !pin ? 1 : 0, - enableTerminal: !enableTerminal ? 1 : 0, - enableTunnel: !enableTunnel ? 1 : 0, + pin: pin ? 1 : 0, + enableTerminal: enableTerminal ? 1 : 0, + enableTunnel: enableTunnel ? 1 : 0, tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null, - enableFileManager: !!enableFileManager ? 1 : 0, + enableFileManager: enableFileManager ? 1 : 0, defaultPath: defaultPath || null, }; @@ -160,20 +154,35 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque sshDataObj.key = null; sshDataObj.keyPassword = null; sshDataObj.keyType = null; - sshLogger.info('SSH host configured for password authentication', { operation: 'host_create', userId, name, ip, port }); } else if (effectiveAuthType === 'key') { sshDataObj.key = key || null; sshDataObj.keyPassword = keyPassword || null; sshDataObj.keyType = keyType; sshDataObj.password = null; - sshLogger.info('SSH host configured for key authentication', { operation: 'host_create', userId, name, ip, port, keyType }); } try { - sshLogger.info('Attempting to save SSH host to database', { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType }); - await db.insert(sshData).values(sshDataObj); - sshLogger.success('SSH host created successfully', { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType, enableTerminal, enableTunnel, enableFileManager }); - res.json({message: 'SSH data created'}); + const result = await db.insert(sshData).values(sshDataObj).returning(); + + if (result.length === 0) { + sshLogger.warn('No host returned after creation', { operation: 'host_create', userId, name, ip, port }); + return res.status(500).json({error: 'Failed to create host'}); + } + + const createdHost = result[0]; + const baseHost = { + ...createdHost, + tags: typeof createdHost.tags === 'string' ? (createdHost.tags ? createdHost.tags.split(',').filter(Boolean) : []) : [], + pin: !!createdHost.pin, + enableTerminal: !!createdHost.enableTerminal, + enableTunnel: !!createdHost.enableTunnel, + tunnelConnections: createdHost.tunnelConnections ? JSON.parse(createdHost.tunnelConnections) : [], + enableFileManager: !!createdHost.enableFileManager, + }; + + const resolvedHost = await resolveHostCredentials(baseHost) || baseHost; + + res.json(resolvedHost); } catch (err) { sshLogger.error('Failed to save SSH host to database', err, { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType }); res.status(500).json({error: 'Failed to save SSH data'}); @@ -185,16 +194,12 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => { const hostId = req.params.id; const userId = (req as any).userId; - sshLogger.info('SSH host update request received', { operation: 'host_update', hostId: parseInt(hostId), userId, contentType: req.headers['content-type'] }); - let hostData: any; if (req.headers['content-type']?.includes('multipart/form-data')) { - sshLogger.info('Processing multipart form data for SSH host update', { operation: 'host_update', hostId: parseInt(hostId), userId }); if (req.body.data) { try { hostData = JSON.parse(req.body.data); - sshLogger.info('Successfully parsed JSON data from multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, hasKey: !!req.file }); } catch (err) { sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, error: err }); return res.status(400).json({error: 'Invalid JSON data'}); @@ -206,11 +211,9 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re if (req.file) { hostData.key = req.file.buffer.toString('utf8'); - sshLogger.info('SSH key file processed from multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, keySize: req.file.size }); } } else { hostData = req.body; - sshLogger.info('Processing JSON data for SSH host update', { operation: 'host_update', hostId: parseInt(hostId), userId }); } const { @@ -256,11 +259,11 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re username, authType: effectiveAuthType, credentialId: credentialId || null, - pin: !pin ? 1 : 0, - enableTerminal: !enableTerminal ? 1 : 0, - enableTunnel: !enableTunnel ? 1 : 0, + pin: pin ? 1 : 0, + enableTerminal: enableTerminal ? 1 : 0, + enableTunnel: enableTunnel ? 1 : 0, tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null, - enableFileManager: !enableFileManager ? 1 : 0, + enableFileManager: enableFileManager ? 1 : 0, defaultPath: defaultPath || null, }; @@ -271,7 +274,6 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re sshDataObj.key = null; sshDataObj.keyPassword = null; sshDataObj.keyType = null; - sshLogger.info('SSH host update configured for password authentication', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port }); } else if (effectiveAuthType === 'key') { if (key) { sshDataObj.key = key; @@ -283,16 +285,37 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re sshDataObj.keyType = keyType; } sshDataObj.password = null; - sshLogger.info('SSH host update configured for key authentication', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, keyType }); } try { - sshLogger.info('Attempting to update SSH host in database', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType }); await db.update(sshData) .set(sshDataObj) .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); - sshLogger.success('SSH host updated successfully', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType, enableTerminal, enableTunnel, enableFileManager }); - res.json({message: 'SSH data updated'}); + + const updatedHosts = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); + + if (updatedHosts.length === 0) { + sshLogger.warn('Updated host not found after update', { operation: 'host_update', hostId: parseInt(hostId), userId }); + return res.status(404).json({error: 'Host not found after update'}); + } + + const updatedHost = updatedHosts[0]; + const baseHost = { + ...updatedHost, + tags: typeof updatedHost.tags === 'string' ? (updatedHost.tags ? updatedHost.tags.split(',').filter(Boolean) : []) : [], + pin: !!updatedHost.pin, + enableTerminal: !!updatedHost.enableTerminal, + enableTunnel: !!updatedHost.enableTunnel, + tunnelConnections: updatedHost.tunnelConnections ? JSON.parse(updatedHost.tunnelConnections) : [], + enableFileManager: !!updatedHost.enableFileManager, + }; + + const resolvedHost = await resolveHostCredentials(baseHost) || baseHost; + + 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 }); res.status(500).json({error: 'Failed to update SSH data'}); @@ -303,19 +326,16 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re // GET /ssh/host router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => { const userId = (req as any).userId; - sshLogger.info('SSH hosts fetch request received', { operation: 'host_fetch', userId }); if (!isNonEmptyString(userId)) { sshLogger.warn('Invalid userId for SSH data fetch', { operation: 'host_fetch', userId }); return res.status(400).json({error: 'Invalid userId'}); } try { - sshLogger.info('Fetching SSH hosts from database', { operation: 'host_fetch', userId }); const data = await db .select() .from(sshData) .where(eq(sshData.userId, userId)); - sshLogger.info('Processing SSH hosts and resolving credentials', { operation: 'host_fetch', userId, hostCount: data.length }); const result = await Promise.all(data.map(async (row: any) => { const baseHost = { ...row, @@ -330,7 +350,6 @@ router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => { return await resolveHostCredentials(baseHost) || baseHost; })); - sshLogger.success('SSH hosts fetched successfully', { operation: 'host_fetch', userId, hostCount: result.length }); res.json(result); } catch (err) { sshLogger.error('Failed to fetch SSH hosts from database', err, { operation: 'host_fetch', userId }); @@ -343,14 +362,12 @@ router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => { router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => { const hostId = req.params.id; const userId = (req as any).userId; - sshLogger.info('SSH host fetch by ID request received', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId }); if (!isNonEmptyString(userId) || !hostId) { sshLogger.warn('Invalid userId or hostId for SSH host fetch by ID', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId }); return res.status(400).json({error: 'Invalid userId or hostId'}); } try { - sshLogger.info('Fetching SSH host by ID from database', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId }); const data = await db .select() .from(sshData) @@ -372,7 +389,6 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response) enableFileManager: !!host.enableFileManager, }; - sshLogger.success('SSH host fetched by ID successfully', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId, hostName: result.name }); res.json(await resolveHostCredentials(result) || result); } catch (err) { sshLogger.error('Failed to fetch SSH host by ID from database', err, { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId }); @@ -385,17 +401,14 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response) router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => { const userId = (req as any).userId; const hostId = req.params.id; - sshLogger.info('SSH host deletion request received', { operation: 'host_delete', hostId: parseInt(hostId), userId }); if (!isNonEmptyString(userId) || !hostId) { sshLogger.warn('Invalid userId or hostId for SSH host delete', { operation: 'host_delete', hostId: parseInt(hostId), userId }); return res.status(400).json({error: 'Invalid userId or id'}); } try { - sshLogger.info('Attempting to delete SSH host from database', { operation: 'host_delete', hostId: parseInt(hostId), userId }); const result = await db.delete(sshData) .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); - sshLogger.success('SSH host deleted successfully', { operation: 'host_delete', hostId: parseInt(hostId), userId }); 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 }); @@ -722,4 +735,66 @@ async function resolveHostCredentials(host: any): Promise { } } +// Route: Rename folder (requires JWT) +// PUT /ssh/db/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(userId) || !oldName || !newName) { + sshLogger.warn('Invalid data for folder rename'); + return res.status(400).json({error: 'Old name and new name are required'}); + } + + if (oldName === newName) { + return res.json({message: 'Folder name unchanged'}); + } + + try { + // Update all hosts with the old folder name + const updatedHosts = await db + .update(sshData) + .set({ + folder: newName, + updatedAt: new Date().toISOString() + }) + .where(and( + eq(sshData.userId, userId), + eq(sshData.folder, oldName) + )) + .returning(); + + // Update all credentials with the old folder name + const updatedCredentials = await db + .update(sshCredentials) + .set({ + folder: newName, + updatedAt: new Date().toISOString() + }) + .where(and( + eq(sshCredentials.userId, userId), + eq(sshCredentials.folder, oldName) + )) + .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, + updatedCredentials: updatedCredentials.length + }); + } catch (err) { + sshLogger.error('Failed to rename folder', err, { operation: 'folder_rename', userId, oldName, newName }); + res.status(500).json({error: 'Failed to rename folder'}); + } +}); + export default router; \ No newline at end of file diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f56bb423..a3bc54fa 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -119,7 +119,7 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) { try { const payload = jwt.verify(token, jwtSecret) as JWTPayload; (req as any).userId = payload.userId; - authLogger.debug('JWT authentication successful', { operation: 'auth', userId: payload.userId, method: req.method, url: req.url }); + // JWT authentication successful next(); } catch (err) { authLogger.warn('Invalid or expired token', { operation: 'auth', method: req.method, url: req.url, error: err }); diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 886c6722..59be6bd6 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -49,7 +49,7 @@ function scheduleSessionCleanup(sessionId: string) { app.post('/ssh/file_manager/ssh/connect', async (req, res) => { const {sessionId, hostId, ip, port, username, password, sshKey, keyPassword, authType, credentialId, userId} = req.body; - fileLogger.info('File manager SSH connection request received', { operation: 'file_connect', sessionId, hostId, ip, port, username, authType, hasCredentialId: !!credentialId }); + // Connection request received if (!sessionId || !ip || !username || !port) { fileLogger.warn('Missing SSH connection parameters for file manager', { operation: 'file_connect', sessionId, hasIp: !!ip, hasUsername: !!username, hasPort: !!port }); @@ -57,14 +57,12 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => { } if (sshSessions[sessionId]?.isConnected) { - fileLogger.info('Cleaning up existing SSH session', { operation: 'file_connect', sessionId }); cleanupSession(sessionId); } const client = new SSHClient(); let resolvedCredentials = {password, sshKey, keyPassword, authType}; if (credentialId && hostId && userId) { - fileLogger.info('Resolving credentials from database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, userId }); try { const credentials = await db .select() @@ -82,15 +80,14 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => { keyPassword: credential.keyPassword, authType: credential.authType }; - fileLogger.success('Credentials resolved successfully for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, authType: credential.authType }); } else { - fileLogger.warn('No credentials found in database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId }); + fileLogger.warn('No credentials found in database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, userId }); } } catch (error) { fileLogger.warn('Failed to resolve credentials from database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, error: error instanceof Error ? error.message : 'Unknown error' }); } - } else { - fileLogger.info('Using direct credentials for file manager connection', { operation: 'file_connect', sessionId, hostId, authType }); + } else if (credentialId && hostId) { + fileLogger.warn('Missing userId for credential resolution in file manager', { operation: 'file_connect', sessionId, hostId, credentialId, hasUserId: !!userId }); } const config: any = { @@ -137,7 +134,6 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => { }; if (resolvedCredentials.sshKey && resolvedCredentials.sshKey.trim()) { - fileLogger.info('Configuring SSH key authentication for file manager', { operation: 'file_connect', sessionId, hostId, hasKeyPassword: !!resolvedCredentials.keyPassword }); try { if (!resolvedCredentials.sshKey.includes('-----BEGIN') || !resolvedCredentials.sshKey.includes('-----END')) { throw new Error('Invalid private key format'); @@ -149,13 +145,11 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => { if (resolvedCredentials.keyPassword) config.passphrase = resolvedCredentials.keyPassword; - fileLogger.success('SSH key authentication configured successfully for file manager', { operation: 'file_connect', sessionId, hostId }); } catch (keyError) { fileLogger.error('SSH key format error for file manager', { operation: 'file_connect', sessionId, hostId, error: keyError.message }); return res.status(400).json({error: 'Invalid SSH key format'}); } } else if (resolvedCredentials.password && resolvedCredentials.password.trim()) { - fileLogger.info('Configuring password authentication for file manager', { operation: 'file_connect', sessionId, hostId }); config.password = resolvedCredentials.password; } else { fileLogger.warn('No authentication method provided for file manager', { operation: 'file_connect', sessionId, hostId }); @@ -167,7 +161,6 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => { client.on('ready', () => { if (responseSent) return; responseSent = true; - fileLogger.success('SSH connection established for file manager', { operation: 'file_connect', sessionId, hostId, ip, port, username, authType: resolvedCredentials.authType }); sshSessions[sessionId] = {client, isConnected: true, lastActive: Date.now()}; res.json({status: 'success', message: 'SSH connection established'}); }); @@ -377,7 +370,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => { writeStream.on('finish', () => { if (hasError || hasFinished) return; hasFinished = true; - fileLogger.success(`File written successfully via SFTP: ${filePath}`); if (!res.headersSent) { res.json({message: 'File written successfully', path: filePath}); } @@ -386,7 +378,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => { writeStream.on('close', () => { if (hasError || hasFinished) return; hasFinished = true; - fileLogger.success(`File written successfully via SFTP: ${filePath}`); if (!res.headersSent) { res.json({message: 'File written successfully', path: filePath}); } @@ -440,7 +431,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => { if (outputData.includes('SUCCESS')) { - fileLogger.success(`File written successfully via fallback: ${filePath}`); if (!res.headersSent) { res.json({message: 'File written successfully', path: filePath}); } @@ -536,8 +526,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => { writeStream.on('finish', () => { if (hasError || hasFinished) return; hasFinished = true; - - fileLogger.success(`File uploaded successfully via SFTP: ${fullPath}`); if (!res.headersSent) { res.json({message: 'File uploaded successfully', path: fullPath}); } @@ -546,8 +534,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => { writeStream.on('close', () => { if (hasError || hasFinished) return; hasFinished = true; - - fileLogger.success(`File uploaded successfully via SFTP: ${fullPath}`); if (!res.headersSent) { res.json({message: 'File uploaded successfully', path: fullPath}); } diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 477c2ed7..695ffda2 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -101,8 +101,6 @@ async function fetchHostById(id: number): Promise { try { - statsLogger.info('Resolving credentials for host', { operation: 'host_credential_resolve', hostId: host.id, hostName: host.name, hasCredentialId: !!host.credentialId }); - const baseHost: any = { id: host.id, name: host.name, @@ -124,7 +122,6 @@ async function resolveHostCredentials(host: any): Promise } async function pollStatusesOnce(): Promise { - statsLogger.info('Starting status polling for all hosts', { operation: 'status_poll' }); const hosts = await fetchAllHosts(); if (hosts.length === 0) { statsLogger.warn('No hosts retrieved for status polling', { operation: 'status_poll' }); return; } - statsLogger.info('Polling status for hosts', { operation: 'status_poll', hostCount: hosts.length, hostIds: hosts.map(h => h.id) }); const now = new Date().toISOString(); const checks = hosts.map(async (h) => { - statsLogger.info('Checking host status', { operation: 'status_poll', hostId: h.id, hostName: h.name, ip: h.ip, port: h.port }); const isOnline = await tcpPing(h.ip, h.port, 5000); const now = new Date().toISOString(); const statusEntry: StatusEntry = {status: isOnline ? 'online' : 'offline', lastChecked: now}; hostStatuses.set(h.id, statusEntry); - statsLogger.info('Host status check completed', { operation: 'status_poll', hostId: h.id, hostName: h.name, status: isOnline ? 'online' : 'offline' }); return isOnline; }); diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 8d761bc8..9ab2f454 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -21,7 +21,6 @@ wss.on('connection', (ws: WebSocket) => { ws.on('close', () => { - sshLogger.info('WebSocket connection closed', { operation: 'websocket_disconnect' }); cleanupSSH(); }); @@ -53,7 +52,6 @@ wss.on('connection', (ws: WebSocket) => { break; case 'disconnect': - sshLogger.info('SSH disconnect requested', { operation: 'ssh_disconnect' }); cleanupSSH(); break; @@ -127,14 +125,14 @@ wss.on('connection', (ws: WebSocket) => { }, 60000); let resolvedCredentials = {password, key, keyPassword, keyType, authType}; - if (credentialId && id) { + if (credentialId && id && hostConfig.userId) { try { const credentials = await db .select() .from(sshCredentials) .where(and( eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, hostConfig.userId || '') + eq(sshCredentials.userId, hostConfig.userId) )); if (credentials.length > 0) { @@ -146,15 +144,18 @@ wss.on('connection', (ws: WebSocket) => { keyType: credential.keyType, authType: credential.authType }; + } else { + sshLogger.warn(`No credentials found for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, userId: hostConfig.userId }); } } catch (error) { sshLogger.warn(`Failed to resolve credentials for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, error: error instanceof Error ? error.message : 'Unknown error' }); } + } else if (credentialId && id) { + sshLogger.warn('Missing userId for credential resolution in terminal', { operation: 'ssh_credentials', hostId: id, credentialId, hasUserId: !!hostConfig.userId }); } sshConn.on('ready', () => { clearTimeout(connectionTimeout); - sshLogger.success('SSH connection established', { operation: 'ssh_connect', hostId: id, ip, port, username, authType: resolvedCredentials.authType }); sshConn!.shell({ @@ -175,7 +176,6 @@ wss.on('connection', (ws: WebSocket) => { }); stream.on('close', () => { - sshLogger.info('SSH stream closed', { operation: 'ssh_stream', hostId: id, ip, port, username }); ws.send(JSON.stringify({type: 'disconnected', message: 'Connection lost'})); }); @@ -219,7 +219,6 @@ wss.on('connection', (ws: WebSocket) => { sshConn.on('close', () => { clearTimeout(connectionTimeout); - cleanupSSH(connectionTimeout); }); diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index db4db51d..967656b1 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -382,7 +382,9 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P const tunnelName = tunnelConfig.name; const tunnelMarker = getTunnelMarker(tunnelName); - tunnelLogger.info('SSH tunnel connection attempt started', { operation: 'tunnel_connect', tunnelName, retryAttempt, sourceIP: tunnelConfig.sourceIP, sourcePort: tunnelConfig.sourceSSHPort }); + 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 }); @@ -394,14 +396,10 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P if (retryAttempt === 0) { retryExhaustedTunnels.delete(tunnelName); retryCounters.delete(tunnelName); - tunnelLogger.info('Reset retry state for tunnel', { operation: 'tunnel_connect', tunnelName }); - } else { - tunnelLogger.warn('Tunnel connection retry attempt', { operation: 'tunnel_connect', tunnelName, retryAttempt }); } const currentStatus = connectionStatus.get(tunnelName); if (!currentStatus || currentStatus.status !== CONNECTION_STATES.WAITING) { - tunnelLogger.info('Broadcasting tunnel connecting status', { operation: 'tunnel_connect', tunnelName, retryAttempt }); broadcastTunnelStatus(tunnelName, { connected: false, status: CONNECTION_STATES.CONNECTING, @@ -428,7 +426,6 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P }; if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) { - tunnelLogger.info('Resolving source credentials from database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId, userId: tunnelConfig.sourceUserId }); try { const credentials = await db .select() @@ -447,15 +444,12 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P keyType: credential.keyType, authMethod: credential.authType }; - tunnelLogger.success('Source credentials resolved successfully', { operation: 'tunnel_connect', tunnelName, credentialId: credential.id, authType: credential.authType }); } else { tunnelLogger.warn('No source credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId }); } } catch (error) { tunnelLogger.warn('Failed to resolve source credentials from database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId, error: error instanceof Error ? error.message : 'Unknown error' }); } - } else { - tunnelLogger.info('Using direct source credentials from tunnel config', { operation: 'tunnel_connect', tunnelName, authMethod: tunnelConfig.sourceAuthMethod }); } // Resolve endpoint credentials if tunnel config has endpointCredentialId @@ -486,10 +480,14 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P keyType: credential.keyType, authMethod: credential.authType }; + } else { + tunnelLogger.warn('No endpoint credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId }); } } catch (error) { tunnelLogger.warn(`Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : 'Unknown error'}`); } + } else if (tunnelConfig.endpointCredentialId) { + tunnelLogger.warn('Missing userId for endpoint credential resolution', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId, hasUserId: !!tunnelConfig.endpointUserId }); } const conn = new Client(); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 6bd03942..8b2bd222 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -12,12 +12,6 @@ import { systemLogger } from './utils/logger.js'; try { systemLogger.info("Initializing backend services...", { operation: 'startup' }); - systemLogger.info("Loading database service...", { operation: 'database_init' }); - systemLogger.info("Loading SSH terminal service...", { operation: 'terminal_init' }); - systemLogger.info("Loading SSH tunnel service...", { operation: 'tunnel_init' }); - systemLogger.info("Loading file manager service...", { operation: 'file_manager_init' }); - systemLogger.info("Loading server stats service...", { operation: 'stats_init' }); - systemLogger.success("All backend services initialized successfully", { operation: 'startup_complete', services: ['database', 'terminal', 'tunnel', 'file_manager', 'stats'] @@ -25,13 +19,11 @@ import { systemLogger } from './utils/logger.js'; process.on('SIGINT', () => { systemLogger.info("Received SIGINT signal, initiating graceful shutdown...", { operation: 'shutdown' }); - systemLogger.info("Shutting down all services...", { operation: 'shutdown' }); process.exit(0); }); process.on('SIGTERM', () => { systemLogger.info("Received SIGTERM signal, initiating graceful shutdown...", { operation: 'shutdown' }); - systemLogger.info("Shutting down all services...", { operation: 'shutdown' }); process.exit(0); }); diff --git a/src/lib/frontend-logger.ts b/src/lib/frontend-logger.ts index acc45b02..a6193301 100644 --- a/src/lib/frontend-logger.ts +++ b/src/lib/frontend-logger.ts @@ -1,6 +1,6 @@ /** * Frontend Logger - A comprehensive logging utility for the frontend - * Based on the backend logger patterns but adapted for browser environment + * Enhanced with better formatting, readability, and request/response grouping */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success'; @@ -45,7 +45,7 @@ class FrontendLogger { private formatMessage(level: LogLevel, message: string, context?: LogContext): string { const timestamp = this.getTimeStamp(); const levelTag = this.getLevelTag(level); - const serviceTag = this.serviceIcon; + const serviceTag = this.getServiceTag(); let contextStr = ''; if (context && this.isDevelopment) { @@ -78,6 +78,10 @@ class FrontendLogger { return `${symbols[level]} [${level.toUpperCase()}]`; } + private getServiceTag(): string { + return `${this.serviceIcon} [${this.serviceName}]`; + } + private shouldLog(level: LogLevel): boolean { if (level === 'debug' && !this.isDevelopment) { return false; @@ -181,64 +185,136 @@ class FrontendLogger { this.warn(`SECURITY: ${message}`, { ...context, operation: 'security' }); } - // Specialized logging methods for different scenarios + // Enhanced API request/response logging methods requestStart(method: string, url: string, context?: LogContext): void { - this.request(`Starting ${method.toUpperCase()} request`, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + + console.group(`🚀 ${method.toUpperCase()} ${shortUrl}`); + this.request(`→ Starting request to ${cleanUrl}`, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url) + url: cleanUrl }); } requestSuccess(method: string, url: string, status: number, responseTime: number, context?: LogContext): void { - this.response(`Request completed successfully`, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + const statusIcon = this.getStatusIcon(status); + const performanceIcon = this.getPerformanceIcon(responseTime); + + this.response(`← ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url), + url: cleanUrl, status, responseTime }); + console.groupEnd(); } requestError(method: string, url: string, status: number, errorMessage: string, responseTime?: number, context?: LogContext): void { - this.error(`Request failed`, undefined, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + const statusIcon = this.getStatusIcon(status); + + this.error(`← ${statusIcon} ${status} ${errorMessage}`, undefined, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url), + url: cleanUrl, status, errorMessage, responseTime }); + console.groupEnd(); } networkError(method: string, url: string, errorMessage: string, context?: LogContext): void { - this.error(`Network error occurred`, undefined, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + + this.error(`🌐 Network Error: ${errorMessage}`, undefined, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url), + url: cleanUrl, errorMessage, errorCode: 'NETWORK_ERROR' }); + console.groupEnd(); } authError(method: string, url: string, context?: LogContext): void { - this.security(`Authentication failed`, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + + this.security(`🔐 Authentication Required`, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url), + url: cleanUrl, errorCode: 'AUTH_REQUIRED' }); + console.groupEnd(); } retryAttempt(method: string, url: string, attempt: number, maxAttempts: number, context?: LogContext): void { - this.retry(`Retry attempt ${attempt}/${maxAttempts}`, { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + + this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, { ...context, method: method.toUpperCase(), - url: this.sanitizeUrl(url), + url: cleanUrl, retryCount: attempt }); } + // Enhanced logging for API operations + apiOperation(operation: string, details: string, context?: LogContext): void { + this.info(`🔧 ${operation}: ${details}`, { ...context, operation: 'api_operation' }); + } + + // Log request summary for better debugging + requestSummary(method: string, url: string, status: number, responseTime: number, context?: LogContext): void { + const cleanUrl = this.sanitizeUrl(url); + const shortUrl = this.getShortUrl(cleanUrl); + const statusIcon = this.getStatusIcon(status); + const performanceIcon = this.getPerformanceIcon(responseTime); + + console.log(`%c📊 ${method} ${shortUrl} ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`, + 'color: #666; font-style: italic; font-size: 0.9em;', + context + ); + } + + // New helper methods for better formatting + private getShortUrl(url: string): string { + try { + const urlObj = new URL(url); + const path = urlObj.pathname; + const query = urlObj.search; + return `${urlObj.hostname}${path}${query}`; + } catch { + return url.length > 50 ? url.substring(0, 47) + '...' : url; + } + } + + private getStatusIcon(status: number): string { + if (status >= 200 && status < 300) return '✅'; + if (status >= 300 && status < 400) return 'â†Šī¸'; + if (status >= 400 && status < 500) return 'âš ī¸'; + if (status >= 500) return '❌'; + return '❓'; + } + + private getPerformanceIcon(responseTime: number): string { + if (responseTime < 100) return '⚡'; + if (responseTime < 500) return '🚀'; + if (responseTime < 1000) return '🏃'; + if (responseTime < 3000) return 'đŸšļ'; + return '🐌'; + } + private sanitizeUrl(url: string): string { // Remove sensitive information from URLs for logging try { diff --git a/src/types/index.ts b/src/types/index.ts index 707e7591..2767bc8e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -297,7 +297,7 @@ export interface CredentialViewerProps { export interface CredentialSelectorProps { value?: number | null; - onChange: (value: number | null) => void; + onValueChange: (value: number | null) => void; } export interface HostManagerProps { @@ -384,10 +384,6 @@ export interface FolderStats { }>; } -export interface FolderManagerProps { - onFolderChanged?: () => void; -} - // ============================================================================ // BACKEND TYPES // ============================================================================ diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx index 87e9a11d..afdf6fa2 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx @@ -21,7 +21,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert" import { toast } from "sonner" import { createCredential, updateCredential, getCredentials, getCredentialDetails } from '@/ui/main-axios' import { useTranslation } from "react-i18next" -import type { Credential, CredentialEditorProps } from '../../../types/index.js' +import type { Credential, CredentialEditorProps, CredentialData } from '../../../types/index.js' export function CredentialEditor({ editingCredential, onFormSubmit }: CredentialEditorProps) { const { t } = useTranslation(); @@ -31,6 +31,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential const [fullCredentialDetails, setFullCredentialDetails] = useState(null); const [authTab, setAuthTab] = useState<'password' | 'key'>('password'); + const [keyInputMethod, setKeyInputMethod] = useState<'upload' | 'paste'>('upload'); useEffect(() => { const fetchData = async () => { @@ -84,9 +85,15 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential key: z.any().optional().nullable(), keyPassword: z.string().optional(), keyType: z.enum([ - 'rsa', - 'ecdsa', - 'ed25519' + 'auto', + 'ssh-rsa', + 'ssh-ed25519', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'ssh-dss', + 'ssh-rsa-sha2-256', + 'ssh-rsa-sha2-512', ]).optional(), }).superRefine((data, ctx) => { if (data.authType === 'password') { @@ -122,14 +129,13 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential password: "", key: null, keyPassword: "", - keyType: "rsa", + keyType: "auto", } }); useEffect(() => { if (editingCredential && fullCredentialDetails) { const defaultAuthType = fullCredentialDetails.authType; - setAuthTab(defaultAuthType); form.reset({ @@ -142,11 +148,10 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential password: fullCredentialDetails.password || "", key: null, keyPassword: fullCredentialDetails.keyPassword || "", - keyType: (fullCredentialDetails.keyType as any) || "rsa", + keyType: (fullCredentialDetails.keyType as any) || "auto", }); } else if (!editingCredential) { setAuthTab('password'); - form.reset({ name: "", description: "", @@ -157,52 +162,43 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential password: "", key: null, keyPassword: "", - keyType: "rsa", + keyType: "auto", }); } - }, [editingCredential, fullCredentialDetails, form]); + }, [editingCredential?.id, fullCredentialDetails]); - const onSubmit = async (data: any) => { + const onSubmit = async (data: FormData) => { try { - const formData = data as FormData; - - if (!formData.name || formData.name.trim() === '') { - formData.name = formData.username; + if (!data.name || data.name.trim() === '') { + data.name = data.username; } - const submitData: any = { - name: formData.name, - description: formData.description, - folder: formData.folder, - tags: formData.tags, - authType: formData.authType, - username: formData.username, - keyType: formData.keyType + const submitData: CredentialData = { + name: data.name, + description: data.description, + folder: data.folder, + tags: data.tags, + authType: data.authType, + username: data.username, + keyType: data.keyType }; - if (formData.password !== undefined) { - submitData.password = formData.password; - } - - if (formData.key !== undefined) { - if (formData.key instanceof File) { - const keyContent = await formData.key.text(); - submitData.key = keyContent; - } else { - submitData.key = formData.key; - } - } - - if (formData.keyPassword !== undefined) { - submitData.keyPassword = formData.keyPassword; + if (data.authType === 'password') { + submitData.password = data.password; + submitData.key = undefined; + submitData.keyPassword = undefined; + } else if (data.authType === 'key') { + submitData.key = data.key instanceof File ? await data.key.text() : data.key; + submitData.keyPassword = data.keyPassword; + submitData.password = undefined; } if (editingCredential) { await updateCredential(editingCredential.id, submitData); - toast.success(t('credentials.credentialUpdatedSuccessfully', { name: formData.name })); + toast.success(t('credentials.credentialUpdatedSuccessfully', { name: data.name })); } else { await createCredential(submitData); - toast.success(t('credentials.credentialAddedSuccessfully', { name: formData.name })); + toast.success(t('credentials.credentialAddedSuccessfully', { name: data.name })); } if (onFormSubmit) { @@ -256,9 +252,15 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential }, [folderDropdownOpen]); const keyTypeOptions = [ - { value: 'rsa', label: t('credentials.keyTypeRSA') }, - { value: 'ecdsa', label: t('credentials.keyTypeECDSA') }, - { value: 'ed25519', label: t('credentials.keyTypeEd25519') }, + { value: 'auto', label: t('hosts.autoDetect') }, + { value: 'ssh-rsa', label: t('hosts.rsa') }, + { value: 'ssh-ed25519', label: t('hosts.ed25519') }, + { value: 'ecdsa-sha2-nistp256', label: t('hosts.ecdsaNistP256') }, + { value: 'ecdsa-sha2-nistp384', label: t('hosts.ecdsaNistP384') }, + { value: 'ecdsa-sha2-nistp521', label: t('hosts.ecdsaNistP521') }, + { value: 'ssh-dss', label: t('hosts.dsa') }, + { value: 'ssh-rsa-sha2-256', label: t('hosts.rsaSha2256') }, + { value: 'ssh-rsa-sha2-512', label: t('hosts.rsaSha2512') }, ]; const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); @@ -436,13 +438,16 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential { - setAuthTab(value as 'password' | 'key'); - form.setValue('authType', value as 'password' | 'key'); + const newAuthType = value as 'password' | 'key'; + setAuthTab(newAuthType); + form.setValue('authType', newAuthType); + // Clear other auth fields when switching - if (value === 'password') { + if (newAuthType === 'password') { form.setValue('key', null); form.setValue('keyPassword', ''); - } else if (value === 'key') { + form.setValue('keyType', 'auto'); + } else if (newAuthType === 'key') { form.setValue('password', ''); } }} @@ -467,103 +472,206 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential /> -
- ( - - {t('credentials.sshPrivateKey')} - -
- { - const file = e.target.files?.[0]; - field.onChange(file || null); - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
-
-
- )} - /> - ( - - {t('credentials.keyPassword')} - - - - - )} - /> - ( - - {t('credentials.keyType')} - -
- - {keyTypeDropdownOpen && ( -
-
- {keyTypeOptions.map((opt) => ( - - ))} -
+ { + setKeyInputMethod(value as 'upload' | 'paste'); + // Clear the other field when switching + if (value === 'upload') { + form.setValue('key', null); + } else { + form.setValue('key', ''); + } + }} + className="w-full" + > + + {t('hosts.uploadFile')} + {t('hosts.pasteKey')} + + +
+ ( + + {t('credentials.sshPrivateKey')} + +
+ { + const file = e.target.files?.[0]; + field.onChange(file || null); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
- )} -
- - - )} - /> -
+ + + )} + /> + ( + + {t('credentials.keyPassword')} + + + + + )} + /> + ( + + {t('credentials.keyType')} + +
+ + {keyTypeDropdownOpen && ( +
+
+ {keyTypeOptions.map((opt) => ( + + ))} +
+
+ )} +
+
+
+ )} + /> +
+ + + ( + + {t('credentials.sshPrivateKey')} + +