import axios, { AxiosError, type AxiosInstance } from 'axios'; // ============================================================================ // TYPES & INTERFACES // ============================================================================ interface SSHHostData { name?: string; ip: string; port: number; username: string; folder?: string; tags?: string[]; pin?: boolean; authType: 'password' | 'key' | 'credential'; password?: string; key?: File | null; keyPassword?: string; keyType?: string; credentialId?: number | null; enableTerminal?: boolean; enableTunnel?: boolean; enableFileManager?: boolean; defaultPath?: string; tunnelConnections?: any[]; } interface SSHHost { id: number; name: string; ip: string; port: number; username: string; folder: string; tags: string[]; pin: boolean; authType: string; password?: string; key?: string; keyPassword?: string; keyType?: string; credentialId?: number; enableTerminal: boolean; enableTunnel: boolean; enableFileManager: boolean; defaultPath: string; tunnelConnections: any[]; createdAt: string; updatedAt: string; } interface TunnelConfig { name: string; hostName: string; sourceIP: string; sourceSSHPort: number; sourceUsername: string; sourcePassword?: string; sourceAuthMethod: string; sourceSSHKey?: string; sourceKeyPassword?: string; sourceKeyType?: string; endpointIP: string; endpointSSHPort: number; endpointUsername: string; endpointPassword?: string; endpointAuthMethod: string; endpointSSHKey?: string; endpointKeyPassword?: string; endpointKeyType?: string; sourcePort: number; endpointPort: number; maxRetries: number; retryInterval: number; autoStart: boolean; isPinned: boolean; } interface TunnelStatus { status: string; reason?: string; errorType?: string; retryCount?: number; maxRetries?: number; nextRetryIn?: number; retryExhausted?: boolean; } interface FileManagerFile { name: string; path: string; type?: 'file' | 'directory'; isSSH?: boolean; sshSessionId?: string; } interface FileManagerShortcut { name: string; path: string; } interface FileManagerOperation { name: string; path: string; isSSH: boolean; sshSessionId?: string; hostId: number; } export type ServerStatus = { status: 'online' | 'offline'; lastChecked: string; }; interface CpuMetrics { percent: number | null; cores: number | null; load: [number, number, number] | null; } interface MemoryMetrics { percent: number | null; usedGiB: number | null; totalGiB: number | null; } interface DiskMetrics { percent: number | null; usedHuman: string | null; totalHuman: string | null; } export type ServerMetrics = { cpu: CpuMetrics; memory: MemoryMetrics; disk: DiskMetrics; lastChecked: string; }; interface AuthResponse { token: string; } interface UserInfo { totp_enabled: boolean; id: string; username: string; is_admin: boolean; is_oidc: boolean; } interface UserCount { count: number; } interface OIDCAuthorize { auth_url: string; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ export function setCookie(name: string, value: string, days = 7): void { const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; if (isElectron) { localStorage.setItem(name, value); } else { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; } } function getCookie(name: string): string | undefined { const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; if (isElectron) { const token = localStorage.getItem(name) || undefined; return token; } else { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); const token = parts.length === 2 ? parts.pop()?.split(';').shift() : undefined; return token; } } function createApiInstance(baseURL: string): AxiosInstance { const instance = axios.create({ baseURL, headers: { 'Content-Type': 'application/json' }, timeout: 30000, }); instance.interceptors.request.use((config) => { const token = getCookie('jwt'); if (token) { config.headers.Authorization = `Bearer ${token}`; } else { console.log('No token found, Authorization header not set'); } return config; }); instance.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; } return Promise.reject(error); } ); return instance; } // ============================================================================ // API INSTANCES // ============================================================================ const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; const isDev = process.env.NODE_ENV === 'development' && (window.location.port === '3000' || window.location.port === '5173' || window.location.port === ''); let apiHost = import.meta.env.VITE_API_HOST || 'localhost'; let apiPort = 8081; if (isElectron) { apiPort = 8081; } function getApiUrl(path: string, defaultPort: number): string { if (isElectron) { return `http://127.0.0.1:${defaultPort}${path}`; } else if (isDev) { return `http://${apiHost}:${defaultPort}${path}`; } else { return path; } } // Multi-port backend architecture (original design) // SSH Host Management API (port 8081) export let sshHostApi = createApiInstance( getApiUrl('/ssh', 8081) ); // Tunnel Management API (port 8083) export let tunnelApi = createApiInstance( getApiUrl('/ssh', 8083) ); // File Manager Operations API (port 8084) - SSH file operations export let fileManagerApi = createApiInstance( getApiUrl('/ssh/file_manager', 8084) ); // Server Statistics API (port 8085) export let statsApi = createApiInstance( getApiUrl('', 8085) ); // Authentication API (port 8081) - includes users, alerts, version, releases export let authApi = createApiInstance( getApiUrl('', 8081) ); // Function to update API instances with new port (for Electron) function updateApiPorts(port: number) { apiPort = port; sshHostApi = createApiInstance(`http://127.0.0.1:${port}/ssh`); tunnelApi = createApiInstance(`http://127.0.0.1:${port}/ssh`); fileManagerApi = createApiInstance(`http://127.0.0.1:${port}/ssh/file_manager`); statsApi = createApiInstance(`http://127.0.0.1:${port}`); authApi = createApiInstance(`http://127.0.0.1:${port}`); } // ============================================================================ // ERROR HANDLING // ============================================================================ class ApiError extends Error { constructor( message: string, public status?: number, public code?: string ) { super(message); this.name = 'ApiError'; } } function handleApiError(error: unknown, operation: string): never { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.error || error.message; if (status === 401) { throw new ApiError('Authentication required', 401); } else if (status === 403) { throw new ApiError('Access denied', 403); } else if (status === 404) { throw new ApiError('Resource not found', 404); } else if (status && status >= 500) { throw new ApiError('Server error occurred', status); } else { throw new ApiError(message || `Failed to ${operation}`, status); } } if (error instanceof ApiError) { throw error; } throw new ApiError(`Unexpected error during ${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`); } // ============================================================================ // SSH HOST MANAGEMENT // ============================================================================ export async function getSSHHosts(): Promise { try { const response = await sshHostApi.get('/db/host'); return response.data; } catch (error) { handleApiError(error, 'fetch SSH hosts'); } } export async function createSSHHost(hostData: SSHHostData): Promise { try { const submitData = { name: hostData.name || '', ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || '', tags: hostData.tags || [], pin: hostData.pin || false, authMethod: hostData.authType, authType: hostData.authType, password: hostData.authType === 'password' ? hostData.password : '', key: hostData.authType === 'key' ? hostData.key : null, keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '', keyType: hostData.authType === 'key' ? hostData.keyType : '', credentialId: hostData.authType === 'credential' ? hostData.credentialId : null, enableTerminal: hostData.enableTerminal !== false, enableTunnel: hostData.enableTunnel !== false, enableFileManager: hostData.enableFileManager !== false, defaultPath: hostData.defaultPath || '/', tunnelConnections: hostData.tunnelConnections || [], }; if (!submitData.enableTunnel) { submitData.tunnelConnections = []; } if (!submitData.enableFileManager) { submitData.defaultPath = ''; } if (hostData.authType === 'key' && hostData.key instanceof File) { const formData = new FormData(); formData.append('key', hostData.key); const dataWithoutFile = { ...submitData }; delete dataWithoutFile.key; formData.append('data', JSON.stringify(dataWithoutFile)); const response = await sshHostApi.post('/db/host', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return response.data; } else { const response = await sshHostApi.post('/db/host', submitData); return response.data; } } catch (error) { handleApiError(error, 'create SSH host'); } } export async function updateSSHHost(hostId: number, hostData: SSHHostData): Promise { try { const submitData = { name: hostData.name || '', ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || '', tags: hostData.tags || [], pin: hostData.pin || false, authMethod: hostData.authType, authType: hostData.authType, password: hostData.authType === 'password' ? hostData.password : '', key: hostData.authType === 'key' ? hostData.key : null, keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '', keyType: hostData.authType === 'key' ? hostData.keyType : '', credentialId: hostData.authType === 'credential' ? hostData.credentialId : null, enableTerminal: hostData.enableTerminal !== false, enableTunnel: hostData.enableTunnel !== false, enableFileManager: hostData.enableFileManager !== false, defaultPath: hostData.defaultPath || '/', tunnelConnections: hostData.tunnelConnections || [], }; if (!submitData.enableTunnel) { submitData.tunnelConnections = []; } if (!submitData.enableFileManager) { submitData.defaultPath = ''; } if (hostData.authType === 'key' && hostData.key instanceof File) { const formData = new FormData(); formData.append('key', hostData.key); const dataWithoutFile = { ...submitData }; delete dataWithoutFile.key; formData.append('data', JSON.stringify(dataWithoutFile)); const response = await sshHostApi.put(`/db/host/${hostId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return response.data; } else { const response = await sshHostApi.put(`/db/host/${hostId}`, submitData); return response.data; } } catch (error) { handleApiError(error, 'update SSH host'); } } export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{ message: string; success: number; failed: number; errors: string[]; }> { try { const response = await sshHostApi.post('/bulk-import', { hosts }); return response.data; } catch (error) { handleApiError(error, 'bulk import SSH hosts'); } } export async function deleteSSHHost(hostId: number): Promise { try { const response = await sshHostApi.delete(`/db/host/${hostId}`); return response.data; } catch (error) { handleApiError(error, 'delete SSH host'); } } export async function getSSHHostById(hostId: number): Promise { try { const response = await sshHostApi.get(`/db/host/${hostId}`); return response.data; } catch (error) { handleApiError(error, 'fetch SSH host'); } } // ============================================================================ // TUNNEL MANAGEMENT // ============================================================================ export async function getTunnelStatuses(): Promise> { try { const response = await tunnelApi.get('/tunnel/status'); return response.data || {}; } catch (error) { handleApiError(error, 'fetch tunnel statuses'); } } export async function getTunnelStatusByName(tunnelName: string): Promise { const statuses = await getTunnelStatuses(); return statuses[tunnelName]; } export async function connectTunnel(tunnelConfig: TunnelConfig): Promise { try { const response = await tunnelApi.post('/tunnel/connect', tunnelConfig); return response.data; } catch (error) { handleApiError(error, 'connect tunnel'); } } export async function disconnectTunnel(tunnelName: string): Promise { try { const response = await tunnelApi.post('/tunnel/disconnect', { tunnelName }); return response.data; } catch (error) { handleApiError(error, 'disconnect tunnel'); } } export async function cancelTunnel(tunnelName: string): Promise { try { const response = await tunnelApi.post('/tunnel/cancel', { tunnelName }); return response.data; } catch (error) { handleApiError(error, 'cancel tunnel'); } } // ============================================================================ // FILE MANAGER METADATA (Recent, Pinned, Shortcuts) // ============================================================================ export async function getFileManagerRecent(hostId: number): Promise { try { const response = await sshHostApi.get(`/file_manager/recent?hostId=${hostId}`); return response.data || []; } catch (error) { return []; } } export async function addFileManagerRecent(file: FileManagerOperation): Promise { try { const response = await sshHostApi.post('/file_manager/recent', file); return response.data; } catch (error) { handleApiError(error, 'add recent file'); } } export async function removeFileManagerRecent(file: FileManagerOperation): Promise { try { const response = await sshHostApi.delete('/file_manager/recent', { data: file }); return response.data; } catch (error) { handleApiError(error, 'remove recent file'); } } export async function getFileManagerPinned(hostId: number): Promise { try { const response = await sshHostApi.get(`/file_manager/pinned?hostId=${hostId}`); return response.data || []; } catch (error) { return []; } } export async function addFileManagerPinned(file: FileManagerOperation): Promise { try { const response = await sshHostApi.post('/file_manager/pinned', file); return response.data; } catch (error) { handleApiError(error, 'add pinned file'); } } export async function removeFileManagerPinned(file: FileManagerOperation): Promise { try { const response = await sshHostApi.delete('/file_manager/pinned', { data: file }); return response.data; } catch (error) { handleApiError(error, 'remove pinned file'); } } export async function getFileManagerShortcuts(hostId: number): Promise { try { const response = await sshHostApi.get(`/file_manager/shortcuts?hostId=${hostId}`); return response.data || []; } catch (error) { return []; } } export async function addFileManagerShortcut(shortcut: FileManagerOperation): Promise { try { const response = await sshHostApi.post('/file_manager/shortcuts', shortcut); return response.data; } catch (error) { handleApiError(error, 'add shortcut'); } } export async function removeFileManagerShortcut(shortcut: FileManagerOperation): Promise { try { const response = await sshHostApi.delete('/file_manager/shortcuts', { data: shortcut }); return response.data; } catch (error) { handleApiError(error, 'remove shortcut'); } } // ============================================================================ // SSH FILE OPERATIONS // ============================================================================ export async function connectSSH(sessionId: string, config: { ip: string; port: number; username: string; password?: string; sshKey?: string; keyPassword?: string; }): Promise { try { const response = await fileManagerApi.post('/ssh/connect', { sessionId, ...config }); return response.data; } catch (error) { handleApiError(error, 'connect SSH'); } } export async function disconnectSSH(sessionId: string): Promise { try { const response = await fileManagerApi.post('/ssh/disconnect', { sessionId }); return response.data; } catch (error) { handleApiError(error, 'disconnect SSH'); } } export async function getSSHStatus(sessionId: string): Promise<{ connected: boolean }> { try { const response = await fileManagerApi.get('/ssh/status', { params: { sessionId } }); return response.data; } catch (error) { handleApiError(error, 'get SSH status'); } } export async function listSSHFiles(sessionId: string, path: string): Promise { try { const response = await fileManagerApi.get('/ssh/listFiles', { params: { sessionId, path } }); return response.data || []; } catch (error) { handleApiError(error, 'list SSH files'); } } export async function readSSHFile(sessionId: string, path: string): Promise<{ content: string; path: string }> { try { const response = await fileManagerApi.get('/ssh/readFile', { params: { sessionId, path } }); return response.data; } catch (error) { handleApiError(error, 'read SSH file'); } } export async function writeSSHFile(sessionId: string, path: string, content: string): Promise { try { const response = await fileManagerApi.post('/ssh/writeFile', { sessionId, path, content }); if (response.data && (response.data.message === 'File written successfully' || response.status === 200)) { return response.data; } else { throw new Error('File write operation did not return success status'); } } catch (error) { handleApiError(error, 'write SSH file'); } } export async function uploadSSHFile(sessionId: string, path: string, fileName: string, content: string): Promise { try { const response = await fileManagerApi.post('/ssh/uploadFile', { sessionId, path, fileName, content }); return response.data; } catch (error) { handleApiError(error, 'upload SSH file'); } } export async function createSSHFile(sessionId: string, path: string, fileName: string, content: string = ''): Promise { try { const response = await fileManagerApi.post('/ssh/createFile', { sessionId, path, fileName, content }); return response.data; } catch (error) { handleApiError(error, 'create SSH file'); } } export async function createSSHFolder(sessionId: string, path: string, folderName: string): Promise { try { const response = await fileManagerApi.post('/ssh/createFolder', { sessionId, path, folderName }); return response.data; } catch (error) { handleApiError(error, 'create SSH folder'); } } export async function deleteSSHItem(sessionId: string, path: string, isDirectory: boolean): Promise { try { const response = await fileManagerApi.delete('/ssh/deleteItem', { data: { sessionId, path, isDirectory } }); return response.data; } catch (error) { handleApiError(error, 'delete SSH item'); } } export async function renameSSHItem(sessionId: string, oldPath: string, newName: string): Promise { try { const response = await fileManagerApi.put('/ssh/renameItem', { sessionId, oldPath, newName }); return response.data; } catch (error) { handleApiError(error, 'rename SSH item'); } } // ============================================================================ // SERVER STATISTICS // ============================================================================ export async function getAllServerStatuses(): Promise> { try { const response = await statsApi.get('/status'); return response.data || {}; } catch (error) { handleApiError(error, 'fetch server statuses'); } } export async function getServerStatusById(id: number): Promise { try { const response = await statsApi.get(`/status/${id}`); return response.data; } catch (error) { handleApiError(error, 'fetch server status'); } } export async function getServerMetricsById(id: number): Promise { try { const response = await statsApi.get(`/metrics/${id}`); return response.data; } catch (error) { handleApiError(error, 'fetch server metrics'); } } // ============================================================================ // AUTHENTICATION // ============================================================================ export async function registerUser(username: string, password: string): Promise { try { const response = await authApi.post('/users/create', { username, password }); return response.data; } catch (error) { handleApiError(error, 'register user'); } } export async function loginUser(username: string, password: string): Promise { try { const response = await authApi.post('/users/login', { username, password }); return response.data; } catch (error) { handleApiError(error, 'login user'); } } export async function getUserInfo(): Promise { try { const response = await authApi.get('/users/me'); return response.data; } catch (error) { handleApiError(error, 'fetch user info'); } } export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> { try { const response = await authApi.get('/users/registration-allowed'); return response.data; } catch (error) { handleApiError(error, 'check registration status'); } } export async function getOIDCConfig(): Promise { try { const response = await authApi.get('/users/oidc-config'); return response.data; } catch (error: any) { console.warn('Failed to fetch OIDC config:', error.response?.data?.error || error.message); return null; } } export async function getUserCount(): Promise { try { const response = await authApi.get('/users/count'); return response.data; } catch (error) { handleApiError(error, 'fetch user count'); } } export async function initiatePasswordReset(username: string): Promise { try { const response = await authApi.post('/users/initiate-reset', { username }); return response.data; } catch (error) { handleApiError(error, 'initiate password reset'); } } export async function verifyPasswordResetCode(username: string, resetCode: string): Promise { try { const response = await authApi.post('/users/verify-reset-code', { username, resetCode }); return response.data; } catch (error) { handleApiError(error, 'verify reset code'); } } export async function completePasswordReset(username: string, tempToken: string, newPassword: string): Promise { try { const response = await authApi.post('/users/complete-reset', { username, tempToken, newPassword }); return response.data; } catch (error) { handleApiError(error, 'complete password reset'); } } export async function getOIDCAuthorizeUrl(): Promise { try { const response = await authApi.get('/users/oidc/authorize'); return response.data; } catch (error) { handleApiError(error, 'get OIDC authorize URL'); } } // ============================================================================ // USER MANAGEMENT // ============================================================================ export async function getUserList(): Promise<{ users: UserInfo[] }> { try { const response = await authApi.get('/users/list'); return response.data; } catch (error) { handleApiError(error, 'fetch user list'); } } export async function makeUserAdmin(username: string): Promise { try { const response = await authApi.post('/users/make-admin', { username }); return response.data; } catch (error) { handleApiError(error, 'make user admin'); } } export async function removeAdminStatus(username: string): Promise { try { const response = await authApi.post('/users/remove-admin', { username }); return response.data; } catch (error) { handleApiError(error, 'remove admin status'); } } export async function deleteUser(username: string): Promise { try { const response = await authApi.delete('/users/delete-user', { data: { username } }); return response.data; } catch (error) { handleApiError(error, 'delete user'); } } export async function deleteAccount(password: string): Promise { try { const response = await authApi.delete('/users/delete-account', { data: { password } }); return response.data; } catch (error) { handleApiError(error, 'delete account'); } } export async function updateRegistrationAllowed(allowed: boolean): Promise { try { const response = await authApi.patch('/users/registration-allowed', { allowed }); return response.data; } catch (error) { handleApiError(error, 'update registration allowed'); } } export async function updateOIDCConfig(config: any): Promise { try { const response = await authApi.post('/users/oidc-config', config); return response.data; } catch (error) { handleApiError(error, 'update OIDC config'); } } // ============================================================================ // ALERTS // ============================================================================ export async function setupTOTP(): Promise<{ secret: string; qr_code: string }> { try { const response = await authApi.post('/users/totp/setup'); return response.data; } catch (error) { handleApiError(error as AxiosError, 'setup TOTP'); throw error; } } export async function enableTOTP(totp_code: string): Promise<{ message: string; backup_codes: string[] }> { try { const response = await authApi.post('/users/totp/enable', { totp_code }); return response.data; } catch (error) { handleApiError(error as AxiosError, 'enable TOTP'); throw error; } } export async function disableTOTP(password?: string, totp_code?: string): Promise<{ message: string }> { try { const response = await authApi.post('/users/totp/disable', { password, totp_code }); return response.data; } catch (error) { handleApiError(error as AxiosError, 'disable TOTP'); throw error; } } export async function verifyTOTPLogin(temp_token: string, totp_code: string): Promise { try { const response = await authApi.post('/users/totp/verify-login', { temp_token, totp_code }); return response.data; } catch (error) { handleApiError(error as AxiosError, 'verify TOTP login'); throw error; } } export async function generateBackupCodes(password?: string, totp_code?: string): Promise<{ backup_codes: string[] }> { try { const response = await authApi.post('/users/totp/backup-codes', { password, totp_code }); return response.data; } catch (error) { handleApiError(error as AxiosError, 'generate backup codes'); throw error; } } export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> { try { const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : ''); const response = await apiInstance.get(`/alerts/user/${userId}`); return response.data; } catch (error) { handleApiError(error, 'fetch user alerts'); } } export async function dismissAlert(userId: string, alertId: string): Promise { try { // Use the general API instance since alerts endpoint is at root level const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : ''); const response = await apiInstance.post('/alerts/dismiss', { userId, alertId }); return response.data; } catch (error) { handleApiError(error, 'dismiss alert'); } } // ============================================================================ // UPDATES & RELEASES // ============================================================================ export async function getReleasesRSS(perPage: number = 100): Promise { try { const response = await authApi.get(`/releases/rss?per_page=${perPage}`); return response.data; } catch (error) { handleApiError(error, 'fetch releases RSS'); } } export async function getVersionInfo(): Promise { try { const response = await authApi.get('/version'); return response.data; } catch (error) { handleApiError(error, 'fetch version info'); } } // ============================================================================ // DATABASE HEALTH // ============================================================================ export async function getDatabaseHealth(): Promise { try { const response = await authApi.get('/users/db-health'); return response.data; } catch (error) { handleApiError(error, 'check database health'); } } // ============================================================================ // SSH CREDENTIALS MANAGEMENT // ============================================================================ export async function getCredentials(): Promise { try { const response = await authApi.get('/credentials'); return response.data; } catch (error) { handleApiError(error, 'fetch credentials'); } } export async function getCredentialDetails(credentialId: number): Promise { try { const response = await authApi.get(`/credentials/${credentialId}`); return response.data; } catch (error) { handleApiError(error, 'fetch credential details'); } } export async function createCredential(credentialData: any): Promise { try { const response = await authApi.post('/credentials', credentialData); return response.data; } catch (error) { handleApiError(error, 'create credential'); } } export async function updateCredential(credentialId: number, credentialData: any): Promise { try { const response = await authApi.put(`/credentials/${credentialId}`, credentialData); return response.data; } catch (error) { handleApiError(error, 'update credential'); } } export async function deleteCredential(credentialId: number): Promise { try { const response = await authApi.delete(`/credentials/${credentialId}`); return response.data; } catch (error) { handleApiError(error, 'delete credential'); } } export async function getCredentialHosts(credentialId: number): Promise { try { const response = await authApi.get(`/credentials/${credentialId}/hosts`); return response.data; } catch (error) { handleApiError(error, 'fetch credential hosts'); } } export async function getCredentialFolders(): Promise { try { const response = await authApi.get('/credentials/folders'); return response.data; } catch (error) { handleApiError(error, 'fetch credential folders'); } } export async function applyCredentialToHost(credentialId: number, hostId: number): Promise { try { const response = await authApi.post(`/credentials/${credentialId}/apply-to-host/${hostId}`); return response.data; } catch (error) { handleApiError(error, 'apply credential to host'); } } // ============================================================================ // SSH FOLDER MANAGEMENT // ============================================================================ export async function getFoldersWithStats(): Promise { try { const response = await authApi.get('/ssh/db/folders/with-stats'); return response.data; } catch (error) { handleApiError(error, 'fetch folders with statistics'); } } export async function renameFolder(oldName: string, newName: string): Promise { try { const response = await authApi.put('/ssh/db/folders/rename', { oldName, newName }); return response.data; } catch (error) { handleApiError(error, 'rename folder'); } }