import axios, { AxiosError, type AxiosInstance } from "axios"; import { toast } from "sonner"; import { getBasePath } from "@/lib/base-path"; import { isElectron } from "@/lib/electron"; import { clearTermixSessionStorage } from "@/shell/TabContext"; import type { SSHHost, SSHHostData, SSHFolder, TunnelConfig, TunnelStatus, TunnelConnection, C2STunnelPreset, FileManagerFile, FileManagerShortcut, DockerContainer, DockerStats, DockerLogOptions, DockerValidation, ProxyNode, } from "@/types/index"; // ============================================================================ // RBAC TYPE DEFINITIONS // ============================================================================ export interface Role { id: number; name: string; displayName: string; description: string | null; isSystem: boolean; permissions: string | null; createdAt: string; updatedAt: string; } export interface UserRole { userId: string; roleId: number; roleName: string; roleDisplayName: string; grantedBy: string; grantedByUsername: string; grantedAt: string; } export interface AccessRecord { id: number; targetType: "user" | "role"; userId: string | null; roleId: number | null; username: string | null; roleName: string | null; roleDisplayName: string | null; grantedBy: string; grantedByUsername: string; permissionLevel: "view"; expiresAt: string | null; createdAt: string; } import { apiLogger, authLogger, sshLogger, tunnelLogger, fileLogger, statsLogger, systemLogger, dashboardLogger, type LogContext, } from "@/lib/frontend-logger"; import { dbHealthMonitor } from "@/lib/db-health-monitor"; interface FileManagerOperation { name: string; path: string; isSSH: boolean; sshSessionId?: string; hostId: number; } export type ServerStatus = { status: "online" | "offline"; lastChecked: string; }; export type SSHHostWithStatus = SSHHost & { status: "online" | "offline" | "unknown"; }; type ApiConnectionLog = { type: "info" | "success" | "warning" | "error"; stage: string; message: string; details?: Record; }; type ConnectErrorResponse = { error?: string; message?: string; connectionLogs?: ApiConnectionLog[]; requires_totp?: boolean; requires_warpgate?: boolean; sessionId?: string; prompt?: string; url?: string; securityKey?: string; status?: string; reason?: 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; availableHuman?: string | null; } export type ServerMetrics = { cpu: CpuMetrics; memory: MemoryMetrics; disk: DiskMetrics; lastChecked: string; }; interface AuthResponse { success?: boolean; is_admin?: boolean; username?: string; userId?: string; is_oidc?: boolean; totp_enabled?: boolean; data_unlocked?: boolean; requires_totp?: boolean; temp_token?: string; rememberMe?: boolean; } interface UserInfo { totp_enabled: boolean; userId: string; username: string; is_admin: boolean; is_oidc: boolean; data_unlocked: boolean; password_hash?: string; } interface UserCount { count: number; } interface OIDCAuthorize { auth_url: string; } type ElectronApi = { isElectron?: boolean; getSetting?: (key: string) => Promise; setSetting?: (key: string, value: string) => Promise; }; type ElectronWindow = Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: ElectronApi; ReactNativeWebView?: unknown; }; // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ export { isElectron }; function getLoggerForService(serviceName: string) { if (serviceName.includes("SSH") || serviceName.includes("ssh")) { return sshLogger; } else if (serviceName.includes("TUNNEL") || serviceName.includes("tunnel")) { return tunnelLogger; } else if (serviceName.includes("FILE") || serviceName.includes("file")) { return fileLogger; } else if (serviceName.includes("STATS") || serviceName.includes("stats")) { return statsLogger; } else if (serviceName.includes("AUTH") || serviceName.includes("auth")) { return authLogger; } else if ( serviceName.includes("DASHBOARD") || serviceName.includes("dashboard") ) { return dashboardLogger; } else { return apiLogger; } } const electronSettingsCache = new Map(); if (isElectron()) { (async () => { try { const electronAPI = (window as ElectronWindow).electronAPI; if (electronAPI?.getSetting) { const settingsToLoad = ["rightClickCopyPaste"]; for (const key of settingsToLoad) { const value = await electronAPI.getSetting(key); if (value !== null && value !== undefined) { // Only populate if not already set to prevent overwriting new values during login if (!localStorage.getItem(key)) { electronSettingsCache.set(key, value); localStorage.setItem(key, value); console.log(`[Electron] Loaded setting ${key} from main process`); } else { // Even if we don't overwrite localStorage, update the cache electronSettingsCache.set(key, localStorage.getItem(key)!); } } } } } catch (error) { console.error("[Electron] Failed to load settings cache:", error); } })(); } export function setCookie( name: string, value: string, days = 7, ): void | Promise { if (isElectron()) { try { if (name === "jwt") { return; } const electronAPI = (window as ElectronWindow).electronAPI; if (electronAPI?.setSetting) { electronSettingsCache.set(name, value); localStorage.setItem(name, value); electronAPI.setSetting(name, value).catch((err: Error) => { console.error(`[Electron] Failed to persist setting ${name}:`, err); }); } console.log(`[Electron] Set setting: ${name}`); } catch (error) { console.error(`[Electron] Failed to set setting: ${name}`, error); } } else { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; } } export function getCookie(name: string): string | undefined { if (isElectron()) { try { if (name === "jwt") { return undefined; } if (electronSettingsCache.has(name)) { return electronSettingsCache.get(name); } const token = localStorage.getItem(name) || undefined; if (token) { electronSettingsCache.set(name, token); } console.log(`[Electron] Get setting: ${name} = ${token}`); return token; } catch (error) { console.error(`[Electron] Failed to get setting: ${name}`, error); return undefined; } } else { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); const encodedToken = parts.length === 2 ? parts.pop()?.split(";").shift() : undefined; const token = encodedToken ? decodeURIComponent(encodedToken) : undefined; return token; } } let userWasAuthenticated = false; let latestAuthSuccessAt = 0; function markUserAuthenticated(): void { userWasAuthenticated = true; latestAuthSuccessAt = typeof performance !== "undefined" ? performance.now() : Date.now(); } export function isCurrentAuthInvalidationError(error: unknown): boolean { const authError = error as { __staleAuthInvalidation?: boolean; }; if (authError.__staleAuthInvalidation) { return false; } const axiosError = error as AxiosError; const apiError = error as ApiError; const responseData = axiosError.response?.data as | Record | undefined; const errorCode = responseData?.code || apiError.code; const errorMessage = responseData?.error || apiError.message; const status = axiosError.response?.status || apiError.status; const isMissingAuthenticationToken = errorMessage === "Missing authentication token"; return ( status === 401 && (errorCode === "SESSION_EXPIRED" || errorCode === "SESSION_NOT_FOUND" || (errorCode === "AUTH_REQUIRED" && userWasAuthenticated) || errorMessage === "Invalid token" || (errorMessage === "Authentication required" && userWasAuthenticated) || (isMissingAuthenticationToken && userWasAuthenticated)) ); } function createApiInstance( baseURL: string, serviceName: string = "API", ): AxiosInstance { const instance = axios.create({ baseURL, headers: { "Content-Type": "application/json" }, timeout: 30000, withCredentials: true, }); instance.interceptors.request.use((config: AxiosRequestConfig) => { const startTime = performance.now(); const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const configWithMetadata = config as AxiosRequestConfigExtended; configWithMetadata.startTime = startTime; configWithMetadata.requestId = requestId; const method = config.method?.toUpperCase() || "UNKNOWN"; const url = config.url || "UNKNOWN"; const fullUrl = `${config.baseURL}${url}`; const context: LogContext = { requestId, method, url: fullUrl, operation: "request_start", }; const logger = getLoggerForService(serviceName); const isDevMode = process.env.NODE_ENV === "development"; if (isDevMode) { logger.requestStart(method, fullUrl, context); } if (isElectron()) { if (config.headers.set) { config.headers.set("X-Electron-App", "true"); } else { config.headers["X-Electron-App"] = "true"; } const jwt = localStorage.getItem("jwt"); if (jwt) { if (config.headers.set) { config.headers.set("Authorization", `Bearer ${jwt}`); } else { config.headers["Authorization"] = `Bearer ${jwt}`; } } } if ( typeof window !== "undefined" && (window as ElectronWindow).ReactNativeWebView ) { let platform = "Unknown"; if (typeof navigator !== "undefined" && navigator.userAgent) { if (navigator.userAgent.includes("Android")) { platform = "Android"; } else if ( navigator.userAgent.includes("iPhone") || navigator.userAgent.includes("iPad") || navigator.userAgent.includes("iOS") ) { platform = "iOS"; } } if (config.headers.set) { config.headers.set("User-Agent", `Termix-Mobile/${platform}`); } else { config.headers["User-Agent"] = `Termix-Mobile/${platform}`; } } return config; }); instance.interceptors.response.use( (response: AxiosResponse) => { const endTime = performance.now(); const responseConfig = response.config as AxiosRequestConfigExtended; const startTime = responseConfig.startTime; const requestId = responseConfig.requestId; const responseTime = Math.round(endTime - (startTime || endTime)); const method = response.config.method?.toUpperCase() || "UNKNOWN"; const url = response.config.url || "UNKNOWN"; const fullUrl = `${response.config.baseURL}${url}`; const context: LogContext = { requestId, method, url: fullUrl, status: response.status, statusText: response.statusText, responseTime, operation: "request_success", }; const logger = getLoggerForService(serviceName); if (process.env.NODE_ENV === "development") { logger.requestSuccess( method, fullUrl, response.status, responseTime, context, ); } if (responseTime > 3000) { logger.warn(`🐌 Slow request: ${responseTime}ms`, context); } dbHealthMonitor.reportDatabaseSuccess(); return response; }, (error: AxiosErrorExtended) => { const endTime = performance.now(); const startTime = error.config?.startTime; const requestId = error.config?.requestId; const responseTime = startTime ? Math.round(endTime - startTime) : undefined; const method = error.config?.method?.toUpperCase() || "UNKNOWN"; const url = error.config?.url || "UNKNOWN"; const fullUrl = error.config ? `${error.config.baseURL}${url}` : url; const status = error.response?.status; const message = (error.response?.data as { error?: string })?.error || (error as Error).message || "Unknown error"; const errorCode = (error.response?.data as { code?: string })?.code || error.code; const context: LogContext = { requestId, method, url: fullUrl, status, responseTime, errorCode, errorMessage: message, operation: "request_error", }; const logger = getLoggerForService(serviceName); // A caller can mark a request as a silent retry (see progressive /status // retry) so we don't spam error logs / health events on each attempt. const isSilentRetry = !!error.config?.__silentRetry; if (process.env.NODE_ENV === "development" && !isSilentRetry) { if (status === 401) { logger.authError(method, fullUrl, context); } else if (status === 0 || !status) { logger.networkError(method, fullUrl, message, context); } else { logger.requestError( method, fullUrl, status || 0, message, responseTime, context, ); } } if (status === 401) { const errorCode = (error.response?.data as Record) ?.code; const errorMessage = (error.response?.data as Record) ?.error; const isSessionExpired = errorCode === "SESSION_EXPIRED"; const isSessionNotFound = errorCode === "SESSION_NOT_FOUND"; const isMissingAuthenticationToken = errorMessage === "Missing authentication token"; const isInvalidToken = errorCode === "AUTH_REQUIRED" || errorMessage === "Invalid token" || errorMessage === "Authentication required" || (isMissingAuthenticationToken && userWasAuthenticated); if (isSessionExpired || isSessionNotFound || isInvalidToken) { const requestStartedAt = typeof error.config?.startTime === "number" ? error.config.startTime : 0; const isStaleAuthInvalidation = latestAuthSuccessAt > 0 && requestStartedAt > 0 && requestStartedAt < latestAuthSuccessAt; if (isStaleAuthInvalidation) { ( error as { __staleAuthInvalidation?: boolean } ).__staleAuthInvalidation = true; return Promise.reject(error); } const wasAuthenticated = userWasAuthenticated; if (isElectron()) { const electronAPI = ( window as unknown as { electronAPI?: { clearSessionCookies?: () => Promise }; } ).electronAPI; electronAPI?.clearSessionCookies?.().catch(() => {}); } if (typeof window !== "undefined") { document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } if (isSessionExpired && typeof window !== "undefined") { console.warn("Session expired - please log in again"); toast.warning("Session expired. Please log in again."); } dbHealthMonitor.reportSessionExpired(); userWasAuthenticated = false; } } else if (!isSilentRetry) { const wasAuthenticated = userWasAuthenticated; dbHealthMonitor.reportDatabaseError(error, wasAuthenticated); } return Promise.reject(error); }, ); return instance; } // ============================================================================ // API INSTANCES // ============================================================================ function isDev(): boolean { if (isElectron()) { return false; } return ( process.env.NODE_ENV === "development" && (window.location.port === "3000" || window.location.port === "5173" || window.location.port === "" || window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") ); } const apiHost = import.meta.env.VITE_API_HOST || "localhost"; let configuredServerUrl: string | null = null; let embeddedMode = false; export interface ServerConfig { serverUrl: string; lastUpdated: string; } interface AxiosRequestConfigExtended extends AxiosRequestConfig { startTime?: number; requestId?: string; __silentRetry?: boolean; } interface AxiosErrorExtended extends AxiosError { config?: AxiosRequestConfigExtended; } export async function getServerConfig(): Promise { if (!isElectron()) return null; try { const result = await ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).electronAPI?.invoke("get-server-config"); return result; } catch (error) { console.error("Failed to get server config:", error); return null; } } export async function saveServerConfig(config: ServerConfig): Promise { if (!isElectron()) return false; try { const result = await ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).electronAPI?.invoke("save-server-config", config); if (result?.success) { configuredServerUrl = config.serverUrl; ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).configuredServerUrl = configuredServerUrl; updateApiInstances(); return true; } return false; } catch (error) { console.error("Failed to save server config:", error); return false; } } export function getConfiguredServerUrl(): string | null { return configuredServerUrl; } interface AxiosRequestConfigExtended extends AxiosRequestConfig { startTime?: number; requestId?: string; __silentRetry?: boolean; } interface AxiosErrorExtended extends AxiosError { config?: AxiosRequestConfigExtended; } export async function testServerConnection( serverUrl: string, ): Promise<{ success: boolean; error?: string }> { if (!isElectron()) return { success: false, error: "Not in Electron environment" }; try { const result = await ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).electronAPI?.invoke("test-server-connection", serverUrl); return result; } catch (error) { console.error("Failed to test server connection:", error); return { success: false, error: "Connection test failed" }; } } export async function checkElectronUpdate(): Promise<{ success: boolean; status?: "up_to_date" | "requires_update" | "beta"; localVersion?: string; remoteVersion?: string; latest_release?: { tag_name: string; name: string; published_at: string; html_url: string; body: string; }; cached?: boolean; cache_age?: number; error?: string; }> { if (!isElectron()) return { success: false, error: "Not in Electron environment" }; try { const result = await ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).electronAPI?.invoke("check-electron-update"); return result; } catch (error) { console.error("Failed to check Electron update:", error); return { success: false, error: "Update check failed" }; } } export async function getEmbeddedServerStatus(): Promise<{ running: boolean; embedded: boolean; dataDir: string | null; } | null> { if (!isElectron()) return null; try { const result = await ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: { invoke: (channel: string, ...args: unknown[]) => Promise; }; } ).electronAPI?.invoke("get-embedded-server-status"); return result as { running: boolean; embedded: boolean; dataDir: string | null; } | null; } catch { return null; } } export function isEmbeddedMode(): boolean { return embeddedMode; } export function setEmbeddedMode(value: boolean): void { embeddedMode = value; if (value) { configuredServerUrl = null; initializeApiInstances(); } } function getApiUrl(path: string, defaultPort: number): string { const devMode = isDev(); const electronMode = isElectron(); if (electronMode) { if (embeddedMode && !configuredServerUrl) { return `http://localhost:${defaultPort}${path}`; } if (configuredServerUrl) { const baseUrl = configuredServerUrl.replace(/\/$/, ""); const url = `${baseUrl}${path}`; return url; } console.warn("Electron mode but no server configured!"); return "http://no-server-configured"; } else if (devMode) { const protocol = window.location.protocol === "https:" ? "https" : "http"; const sslPort = protocol === "https" ? 8443 : defaultPort; const url = `${protocol}://${apiHost}:${sslPort}${path}`; return url; } else { return getBasePath() + path; } } function initializeApiInstances() { // Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST"); sshHostApi = hostApi; // Tunnel Management API (port 30003) tunnelApi = createApiInstance(getApiUrl("/ssh", 30003), "TUNNEL"); // File Manager Operations API (port 30004) fileManagerApi = createApiInstance( getApiUrl("/ssh/file_manager", 30004), "FILE_MANAGER", ); // Server Statistics API (port 30005) statsApi = createApiInstance(getApiUrl("", 30005), "STATS"); // Authentication API (port 30001) authApi = createApiInstance(getApiUrl("", 30001), "AUTH"); // Dashboard API (port 30006) dashboardApi = createApiInstance(getApiUrl("", 30006), "DASHBOARD"); // RBAC API (port 30001) rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC"); // Docker Management API (port 30007) dockerApi = createApiInstance(getApiUrl("/docker", 30007), "DOCKER"); } // Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet export let hostApi: AxiosInstance; // Backward compatibility export let sshHostApi: AxiosInstance; // Tunnel Management API (port 30003) export let tunnelApi: AxiosInstance; // File Manager Operations API (port 30004) export let fileManagerApi: AxiosInstance; // Server Statistics API (port 30005) export let statsApi: AxiosInstance; // Authentication API (port 30001) export let authApi: AxiosInstance; // Dashboard API (port 30006) export let dashboardApi: AxiosInstance; // RBAC API (port 30001) export let rbacApi: AxiosInstance; // Docker Management API (port 30007) export let dockerApi: AxiosInstance; // Pre-initialize with default values to avoid undefined errors during early mounting initializeApiInstances(); function initializeApp() { if (isElectron()) { Promise.all([getServerConfig(), getEmbeddedServerStatus()]) .then(([config, status]) => { if (status?.embedded && status?.running) { embeddedMode = true; } if (config?.serverUrl) { configuredServerUrl = config.serverUrl; ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).configuredServerUrl = configuredServerUrl; } else if (embeddedMode) { // Embedded backend running, no remote server needed } else { console.warn("No server URL in config"); } initializeApiInstances(); }) .catch((error) => { console.error( "Failed to load server config, initializing with default:", error, ); initializeApiInstances(); }); } else { initializeApiInstances(); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeApp); } else { initializeApp(); } function updateApiInstances() { systemLogger.info("Updating API instances with new server configuration", { operation: "api_instance_update", configuredServerUrl, }); initializeApiInstances(); ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).configuredServerUrl = configuredServerUrl; systemLogger.success("All API instances updated successfully", { operation: "api_instance_update_complete", configuredServerUrl, }); } // ============================================================================ // 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 { const context: LogContext = { operation: "error_handling", errorOperation: operation, }; if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.response?.data?.error || error.message; const code = error.response?.data?.code || error.response?.data?.error; const url = error.config?.url; const method = error.config?.method?.toUpperCase(); const errorContext: LogContext = { ...context, method, url, status, errorCode: code, errorMessage: message, }; if (status === 401) { authLogger.warn( `Auth failed: ${method} ${url} - ${message}`, errorContext, ); const isLoginEndpoint = url?.includes("/users/login"); const errorMessage = isLoginEndpoint ? message : "Authentication required. Please log in again."; throw new ApiError(errorMessage, 401, code || "AUTH_REQUIRED"); } else if (status === 403) { authLogger.warn(`Access denied: ${method} ${url}`, errorContext); const apiError = new ApiError( code === "TOTP_REQUIRED" ? message : "Access denied. You do not have permission to perform this action.", 403, code || "ACCESS_DENIED", ); (apiError as ApiError & { response?: unknown }).response = error.response; throw apiError; } else if (status === 404) { apiLogger.warn(`Not found: ${method} ${url}`, errorContext); throw new ApiError( "Resource not found. The requested item may have been deleted.", 404, "NOT_FOUND", ); } else if (status === 409) { apiLogger.warn(`Conflict: ${method} ${url}`, errorContext); throw new ApiError( "Conflict. The resource already exists or is in use.", 409, "CONFLICT", ); } else if (status === 422) { apiLogger.warn( `Validation error: ${method} ${url} - ${message}`, errorContext, ); throw new ApiError( "Validation error. Please check your input and try again.", 422, "VALIDATION_ERROR", ); } else if (status && status >= 500) { apiLogger.error( `Server error: ${method} ${url} - ${message}`, error, errorContext, ); throw new ApiError( "Server error occurred. Please try again later.", status, "SERVER_ERROR", ); } else if (status === 0) { if (url.includes("no-server-configured")) { apiLogger.error( `No server configured: ${method} ${url}`, error, errorContext, ); throw new ApiError( "No server configured. Please configure a Termix server first.", 0, "NO_SERVER_CONFIGURED", ); } apiLogger.error( `Network error: ${method} ${url} - ${message}`, error, errorContext, ); throw new ApiError( "Network error. Please check your connection and try again.", 0, "NETWORK_ERROR", ); } else { apiLogger.error( `Request failed: ${method} ${url} - ${message}`, error, errorContext, ); throw new ApiError(message || `Failed to ${operation}`, status, code); } } if (error instanceof ApiError) { throw error; } const errorMessage = error instanceof Error ? error.message : "Unknown error"; apiLogger.error( `Unexpected error during ${operation}: ${errorMessage}`, error, context, ); throw new ApiError( `Unexpected error during ${operation}: ${errorMessage}`, undefined, "UNKNOWN_ERROR", ); } // ============================================================================ // SSH HOST MANAGEMENT // ============================================================================ export async function getSSHHosts(): Promise { try { const hostsResponse = await sshHostApi.get("/db/host"); const hosts: SSHHost[] = Array.isArray(hostsResponse.data) ? hostsResponse.data : []; let statuses: Record = {}; try { statuses = (await getAllServerStatuses()) || {}; } catch { // Status fetch failure should not prevent host list from loading } return hosts.map((host) => ({ ...host, status: statuses[host.id]?.status || "unknown", })); } catch (error) { throw handleApiError(error, "fetch SSH hosts"); } } export async function createSSHHost(hostData: SSHHostData): Promise { try { const submitData = { connectionType: hostData.connectionType || "ssh", name: hostData.name || "", ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || "", tags: hostData.tags || [], pin: Boolean(hostData.pin), authType: hostData.authType, password: hostData.connectionType !== "ssh" ? hostData.password || null : hostData.authType === "password" ? hostData.password : null, key: hostData.authType === "key" ? hostData.key : null, keyPassword: hostData.authType === "key" ? hostData.keyPassword : null, keyType: hostData.authType === "key" ? hostData.keyType : null, credentialId: hostData.authType === "credential" ? hostData.credentialId : null, overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername), enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), enableDocker: Boolean(hostData.enableDocker), showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar), showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar), showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar), showDockerInSidebar: Boolean(hostData.showDockerInSidebar), showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], jumpHosts: hostData.jumpHosts || [], quickActions: hostData.quickActions || [], sudoPassword: hostData.sudoPassword || null, statsConfig: hostData.statsConfig || null, dockerConfig: hostData.dockerConfig || null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), domain: hostData.domain || null, security: hostData.security || null, ignoreCert: Boolean(hostData.ignoreCert), guacamoleConfig: hostData.guacamoleConfig || null, notes: hostData.notes || "", useSocks5: Boolean(hostData.useSocks5), socks5Host: hostData.socks5Host || null, socks5Port: hostData.socks5Port || null, socks5Username: hostData.socks5Username || null, socks5Password: hostData.socks5Password || null, socks5ProxyChain: hostData.socks5ProxyChain || null, macAddress: hostData.macAddress || null, portKnockSequence: hostData.portKnockSequence || null, enableSsh: hostData.enableSsh !== false, enableRdp: Boolean(hostData.enableRdp), enableVnc: Boolean(hostData.enableVnc), enableTelnet: Boolean(hostData.enableTelnet), sshPort: hostData.sshPort || hostData.port || 22, rdpPort: hostData.rdpPort || 3389, vncPort: hostData.vncPort || 5900, telnetPort: hostData.telnetPort || 23, rdpUser: hostData.rdpUser || null, rdpPassword: hostData.rdpPassword || null, rdpDomain: hostData.rdpDomain || null, rdpSecurity: hostData.rdpSecurity || null, rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert), vncPassword: hostData.vncPassword || null, vncUser: hostData.vncUser || null, telnetUser: hostData.telnetUser || null, telnetPassword: hostData.telnetPassword || null, }; 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) { throw handleApiError(error, "create SSH host"); } } export async function updateSSHHost( hostId: number, hostData: SSHHostData, ): Promise { try { const submitData = { connectionType: hostData.connectionType || "ssh", name: hostData.name || "", ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || "", tags: hostData.tags || [], pin: Boolean(hostData.pin), authType: hostData.authType, password: hostData.connectionType !== "ssh" ? hostData.password || null : hostData.authType === "password" ? hostData.password : null, key: hostData.authType === "key" ? hostData.key : null, keyPassword: hostData.authType === "key" ? hostData.keyPassword : null, keyType: hostData.authType === "key" ? hostData.keyType : null, credentialId: hostData.authType === "credential" ? hostData.credentialId : null, overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername), enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), enableDocker: Boolean(hostData.enableDocker), showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar), showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar), showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar), showDockerInSidebar: Boolean(hostData.showDockerInSidebar), showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], jumpHosts: hostData.jumpHosts || [], quickActions: hostData.quickActions || [], sudoPassword: hostData.sudoPassword || null, statsConfig: hostData.statsConfig || null, dockerConfig: hostData.dockerConfig || null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), domain: hostData.domain || null, security: hostData.security || null, ignoreCert: Boolean(hostData.ignoreCert), guacamoleConfig: hostData.guacamoleConfig || null, notes: hostData.notes || "", useSocks5: Boolean(hostData.useSocks5), socks5Host: hostData.socks5Host || null, socks5Port: hostData.socks5Port || null, socks5Username: hostData.socks5Username || null, socks5Password: hostData.socks5Password || null, socks5ProxyChain: hostData.socks5ProxyChain || null, macAddress: hostData.macAddress || null, portKnockSequence: hostData.portKnockSequence || null, enableSsh: hostData.enableSsh !== false, enableRdp: Boolean(hostData.enableRdp), enableVnc: Boolean(hostData.enableVnc), enableTelnet: Boolean(hostData.enableTelnet), sshPort: hostData.sshPort || hostData.port || 22, rdpPort: hostData.rdpPort || 3389, vncPort: hostData.vncPort || 5900, telnetPort: hostData.telnetPort || 23, rdpUser: hostData.rdpUser || null, rdpPassword: hostData.rdpPassword || null, rdpDomain: hostData.rdpDomain || null, rdpSecurity: hostData.rdpSecurity || null, rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert), vncPassword: hostData.vncPassword || null, vncUser: hostData.vncUser || null, telnetUser: hostData.telnetUser || null, telnetPassword: hostData.telnetPassword || null, }; 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) { throw handleApiError(error, "update SSH host"); } } export async function wakeOnLan(hostId: number): Promise<{ success: boolean }> { try { const response = await sshHostApi.post(`/db/host/${hostId}/wake`); return response.data; } catch (error) { throw handleApiError(error, "wake on LAN"); } } export async function bulkImportSSHHosts( hosts: SSHHostData[], overwrite = false, ): Promise<{ message: string; success: number; updated: number; skipped: number; failed: number; errors: string[]; }> { try { const response = await sshHostApi.post("/bulk-import", { hosts, overwrite, }); return response.data; } catch (error) { handleApiError(error, "bulk import SSH hosts"); } } export async function bulkUpdateSSHHosts( hostIds: number[], updates: Record, ): Promise<{ updated: number; failed: number; errors: string[] }> { try { const response = await sshHostApi.patch("/bulk-update", { hostIds, updates, }); return response.data; } catch (error) { handleApiError(error, "bulk update 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"); } } export async function exportSSHHostWithCredentials( hostId: number, ): Promise { try { const response = await sshHostApi.get(`/db/host/${hostId}/export`); return response.data; } catch (error) { handleApiError(error, "export SSH host with credentials"); } } export async function exportAllSSHHosts(): Promise<{ hosts: SSHHost[]; }> { try { const response = await sshHostApi.get("/db/hosts/export"); return response.data; } catch (error) { handleApiError(error, "export all SSH hosts"); } } // ============================================================================ // SSH AUTOSTART MANAGEMENT // ============================================================================ export async function enableAutoStart( sshConfigId: number, ): Promise> { try { const response = await sshHostApi.post("/autostart/enable", { sshConfigId, }); return response.data; } catch (error) { handleApiError(error, "enable autostart"); } } export async function disableAutoStart( sshConfigId: number, ): Promise> { try { const response = await sshHostApi.delete("/autostart/disable", { data: { sshConfigId }, }); return response.data; } catch (error) { handleApiError(error, "disable autostart"); } } export async function getAutoStartStatus(): Promise<{ autostart_configs: Array<{ sshConfigId: number; host: string; port: number; username: string; authType: string; }>; total_count: number; }> { try { const response = await sshHostApi.get("/autostart/status"); return response.data; } catch (error) { handleApiError(error, "fetch autostart status"); } } // ============================================================================ // PROXY CONNECTIVITY TEST // ============================================================================ export async function testProxyConnection(options: { singleProxy?: { host: string; port: number; type?: 4 | 5 | "http"; username?: string; password?: string; }; proxyChain?: ProxyNode[]; testTarget?: { host: string; port: number }; }): Promise<{ success: boolean; latencyMs?: number; error?: string }> { try { const response = await sshHostApi.post("/db/proxy/test", options); return response.data; } catch (error) { if (error instanceof AxiosError && error.response?.data?.error) { return { success: false, error: error.response.data.error }; } handleApiError(error, "test proxy connection"); } } // ============================================================================ // TUNNEL MANAGEMENT // ============================================================================ export async function getTunnelStatuses(): Promise< Record > { try { const response = await tunnelApi.get("/tunnel/status"); return response.data || {}; } catch (error) { handleApiError(error, "fetch tunnel statuses"); } } export function subscribeTunnelStatuses( onStatuses: (statuses: Record) => void, onError?: () => void, ): () => void { const baseURL = (tunnelApi.defaults.baseURL || "").replace(/\/$/, ""); const source = new EventSource(`${baseURL}/tunnel/status/stream`, { withCredentials: true, }); source.addEventListener("statuses", (event) => { try { onStatuses(JSON.parse(event.data) as Record); } catch { onError?.(); } }); source.onerror = () => { onError?.(); }; return () => source.close(); } 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"); } } export async function getC2STunnelPresets(): Promise { try { const response = await authApi.get("/c2s-tunnel-presets"); return response.data || []; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { return []; } handleApiError(error, "fetch client tunnel presets"); } } export async function createC2STunnelPreset(data: { name: string; config: TunnelConnection[]; platform?: string; computerName?: string; }): Promise { try { const response = await authApi.post("/c2s-tunnel-presets", data); return response.data; } catch (error) { handleApiError(error, "create client tunnel preset"); } } export async function updateC2STunnelPreset( id: number, data: Partial<{ name: string; config: TunnelConnection[]; platform: string; computerName: string; }>, ): Promise { try { const response = await authApi.put(`/c2s-tunnel-presets/${id}`, data); return response.data; } catch (error) { handleApiError(error, "update client tunnel preset"); } } export async function deleteC2STunnelPreset( id: number, ): Promise> { try { const response = await authApi.delete(`/c2s-tunnel-presets/${id}`); return response.data; } catch (error) { handleApiError(error, "delete client tunnel preset"); } } // ============================================================================ // 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 { 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 { 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 { 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: { hostId?: number; ip: string; port: number; username: string; password?: string; sshKey?: string; keyPassword?: string; authType?: string; credentialId?: number; userId?: string; forceKeyboardInteractive?: boolean; useSocks5?: boolean; socks5Host?: string; socks5Port?: number; socks5Username?: string; socks5Password?: string; socks5ProxyChain?: unknown; jumpHosts?: Array<{ hostId: number }>; }, ): Promise> { try { const response = await fileManagerApi.post("/ssh/connect", { sessionId, ...config, }); return response.data; } catch (error: unknown) { if ( axios.isAxiosError(error) && error.response?.data?.connectionLogs ) { const data = error.response.data; const errorWithLogs = new Error( data.error || data.message || error.message, ); Object.assign(errorWithLogs, { connectionLogs: data.connectionLogs, }); if (data.requires_totp) { Object.assign(errorWithLogs, { requires_totp: true, sessionId: data.sessionId, prompt: data.prompt, }); } if (data.requires_warpgate) { Object.assign(errorWithLogs, { requires_warpgate: true, sessionId: data.sessionId, url: data.url, securityKey: data.securityKey, }); } if (data.status === "auth_required") { Object.assign(errorWithLogs, { status: "auth_required", reason: data.reason, }); } throw errorWithLogs; } 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 verifySSHTOTP( sessionId: string, totpCode: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/connect-totp", { sessionId, totpCode, }); return response.data; } catch (error) { handleApiError(error, "verify SSH TOTP"); } } export async function verifySSHWarpgate( sessionId: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/connect-warpgate", { sessionId, }); return response.data; } catch (error) { handleApiError(error, "verify SSH Warpgate"); } } /** * @openapi * /ssh/quick-connect: * post: * summary: Create a temporary SSH connection without saving to database * description: Returns a temporary host configuration for immediate use * tags: * - SSH * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - ip * - port * - username * - authType * properties: * ip: * type: string * description: SSH server IP or hostname * port: * type: number * description: SSH server port * username: * type: string * description: SSH username * authType: * type: string * enum: [password, key, credential] * description: Authentication method * password: * type: string * description: Password (required if authType is password) * key: * type: string * description: SSH private key (required if authType is key) * keyPassword: * type: string * description: SSH key password (optional) * keyType: * type: string * description: SSH key type * credentialId: * type: number * description: Credential ID (required if authType is credential) * overrideCredentialUsername: * type: boolean * description: Use provided username instead of credential username * responses: * 200: * description: Temporary host configuration created successfully * content: * application/json: * schema: * type: object * description: SSHHost object * 400: * description: Invalid request data * 401: * description: Unauthorized * 500: * description: Server error */ export async function quickConnect( data: Record, ): Promise { try { const response = await authApi.post("/host/quick-connect", data); return response.data; } catch (error) { throw handleApiError(error, "quick connect"); } } 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 keepSSHAlive( sessionId: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/keepalive", { sessionId, }); return response.data; } catch (error) { handleApiError(error, "SSH keepalive"); } } export async function listSSHFiles( sessionId: string, path: string, ): Promise<{ files: unknown[]; path: string }> { try { const response = await fileManagerApi.get("/ssh/listFiles", { params: { sessionId, path }, }); return response.data || { files: [], path }; } catch (error) { handleApiError(error, "list SSH files"); return { files: [], path }; } } export async function identifySSHSymlink( sessionId: string, path: string, ): Promise<{ path: string; target: string; type: "directory" | "file" }> { try { const response = await fileManagerApi.get("/ssh/identifySymlink", { params: { sessionId, path }, }); return response.data; } catch (error) { handleApiError(error, "identify SSH symlink"); } } export async function resolveSSHPath( sessionId: string, path: string, ): Promise { try { const response = await fileManagerApi.get("/ssh/resolvePath", { params: { sessionId, path }, }); return response.data?.resolvedPath || path; } catch { return path; } } export async function readSSHFile( sessionId: string, path: string, ): Promise<{ content: string; path: string; encoding?: "base64" | "utf8"; }> { try { const response = await fileManagerApi.get("/ssh/readFile", { params: { sessionId, path }, }); return response.data; } catch (error: unknown) { if (error.response?.status === 404) { const customError = new Error("File not found"); ( customError as Error & { response?: unknown; isFileNotFound?: boolean } ).response = error.response; ( customError as Error & { response?: unknown; isFileNotFound?: boolean } ).isFileNotFound = error.response.data?.fileNotFound || true; throw customError; } handleApiError(error, "read SSH file"); } } export async function writeSSHFile( sessionId: string, path: string, content: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/writeFile", { sessionId, path, content, hostId, userId, }); 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, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/uploadFile", { sessionId, path, fileName, content, hostId, userId, }); return response.data; } catch (error) { handleApiError(error, "upload SSH file"); } } export async function downloadSSHFile( sessionId: string, filePath: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/downloadFile", { sessionId, path: filePath, hostId, userId, }); return response.data; } catch (error) { handleApiError(error, "download SSH file"); } } export async function downloadSSHFileStream( sessionId: string, filePath: string, ): Promise { const response = await fileManagerApi.post( "/ssh/downloadFileStream", { sessionId, path: filePath }, { responseType: "blob" }, ); const blob = response.data as Blob; const fileName = filePath.split("/").pop() || "download"; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } export async function createSSHFile( sessionId: string, path: string, fileName: string, content: string = "", hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/createFile", { sessionId, path, fileName, content, hostId, userId, }); return response.data; } catch (error) { handleApiError(error, "create SSH file"); } } export async function createSSHFolder( sessionId: string, path: string, folderName: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/createFolder", { sessionId, path, folderName, hostId, userId, }); return response.data; } catch (error) { handleApiError(error, "create SSH folder"); } } export async function deleteSSHItem( sessionId: string, path: string, isDirectory: boolean, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.delete("/ssh/deleteItem", { data: { sessionId, path, isDirectory, hostId, userId, }, }); return response.data; } catch (error) { handleApiError(error, "delete SSH item"); } } export async function setSudoPassword( sessionId: string, password: string, ): Promise { try { await fileManagerApi.post("/sudo-password", { sessionId, password, }); } catch (error) { handleApiError(error, "set sudo password"); } } export async function copySSHItem( sessionId: string, sourcePath: string, targetDir: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.post( "/ssh/copyItem", { sessionId, sourcePath, targetDir, hostId, userId, }, { timeout: 60000, }, ); return response.data; } catch (error) { handleApiError(error, "copy SSH item"); throw error; } } export async function renameSSHItem( sessionId: string, oldPath: string, newName: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.put("/ssh/renameItem", { sessionId, oldPath, newName, hostId, userId, }); return response.data; } catch (error) { handleApiError(error, "rename SSH item"); throw error; } } export async function moveSSHItem( sessionId: string, oldPath: string, newPath: string, hostId?: number, userId?: string, ): Promise> { try { const response = await fileManagerApi.put( "/ssh/moveItem", { sessionId, oldPath, newPath, hostId, userId, }, { timeout: 60000, }, ); return response.data; } catch (error) { handleApiError(error, "move SSH item"); throw error; } } export async function changeSSHPermissions( sessionId: string, path: string, permissions: string, hostId?: number, userId?: string, ): Promise<{ success: boolean; message: string }> { try { fileLogger.info("Changing SSH file permissions", { operation: "change_permissions", sessionId, path, permissions, hostId, userId, }); const response = await fileManagerApi.post("/ssh/changePermissions", { sessionId, path, permissions, hostId, userId, }); fileLogger.success("SSH file permissions changed successfully", { operation: "change_permissions", sessionId, path, permissions, }); return response.data; } catch (error) { fileLogger.error("Failed to change SSH file permissions", error, { operation: "change_permissions", sessionId, path, permissions, }); handleApiError(error, "change SSH permissions"); throw error; } } export async function extractSSHArchive( sessionId: string, archivePath: string, extractPath?: string, hostId?: number, userId?: string, ): Promise<{ success: boolean; message: string; extractPath: string }> { try { fileLogger.info("Extracting archive", { operation: "extract_archive", sessionId, archivePath, extractPath, hostId, userId, }); const response = await fileManagerApi.post("/ssh/extractArchive", { sessionId, archivePath, extractPath, hostId, userId, }); fileLogger.success("Archive extracted successfully", { operation: "extract_archive", sessionId, archivePath, extractPath: response.data.extractPath, }); return response.data; } catch (error) { fileLogger.error("Failed to extract archive", error, { operation: "extract_archive", sessionId, archivePath, extractPath, }); handleApiError(error, "extract archive"); throw error; } } export async function compressSSHFiles( sessionId: string, paths: string[], archiveName: string, format?: string, hostId?: number, userId?: string, ): Promise<{ success: boolean; message: string; archivePath: string }> { try { fileLogger.info("Compressing files", { operation: "compress_files", sessionId, paths, archiveName, format, hostId, userId, }); const response = await fileManagerApi.post("/ssh/compressFiles", { sessionId, paths, archiveName, format: format || "zip", hostId, userId, }); fileLogger.success("Files compressed successfully", { operation: "compress_files", sessionId, paths, archivePath: response.data.archivePath, }); return response.data; } catch (error) { fileLogger.error("Failed to compress files", error, { operation: "compress_files", sessionId, paths, archiveName, format, }); handleApiError(error, "compress files"); throw error; } } // ============================================================================ // FILE MANAGER DATA // ============================================================================ export async function getRecentFiles( hostId: number, ): Promise> { try { const response = await authApi.get("/host/file_manager/recent", { params: { hostId }, }); return response.data; } catch (error) { handleApiError(error, "get recent files"); throw error; } } export async function addRecentFile( hostId: number, path: string, name?: string, ): Promise> { try { const response = await authApi.post("/host/file_manager/recent", { hostId, path, name, }); return response.data; } catch (error) { handleApiError(error, "add recent file"); throw error; } } export async function removeRecentFile( hostId: number, path: string, ): Promise> { try { const response = await authApi.delete("/host/file_manager/recent", { data: { hostId, path }, }); return response.data; } catch (error) { handleApiError(error, "remove recent file"); throw error; } } export async function getPinnedFiles( hostId: number, ): Promise> { try { const response = await authApi.get("/host/file_manager/pinned", { params: { hostId }, }); return response.data; } catch (error) { handleApiError(error, "get pinned files"); throw error; } } export async function addPinnedFile( hostId: number, path: string, name?: string, ): Promise> { try { const response = await authApi.post("/host/file_manager/pinned", { hostId, path, name, }); return response.data; } catch (error) { handleApiError(error, "add pinned file"); throw error; } } export async function removePinnedFile( hostId: number, path: string, ): Promise> { try { const response = await authApi.delete("/host/file_manager/pinned", { data: { hostId, path }, }); return response.data; } catch (error) { handleApiError(error, "remove pinned file"); throw error; } } export async function getFolderShortcuts( hostId: number, ): Promise> { try { const response = await authApi.get("/host/file_manager/shortcuts", { params: { hostId }, }); return response.data; } catch (error) { handleApiError(error, "get folder shortcuts"); throw error; } } export async function addFolderShortcut( hostId: number, path: string, name?: string, ): Promise> { try { const response = await authApi.post("/host/file_manager/shortcuts", { hostId, path, name, }); return response.data; } catch (error) { handleApiError(error, "add folder shortcut"); throw error; } } export async function removeFolderShortcut( hostId: number, path: string, ): Promise> { try { const response = await authApi.delete("/host/file_manager/shortcuts", { data: { hostId, path }, }); return response.data; } catch (error) { handleApiError(error, "remove folder shortcut"); throw error; } } // ============================================================================ // SERVER STATISTICS // ============================================================================ /** * Progressive retry schedule for the background /status poll. * * Each entry describes one attempt's per-request timeout and the pause to * observe before the next attempt. The pause on the last entry is `null`: * after that final failure we surface the network error, which flows * through the response interceptor + dbHealthMonitor (which decides * between the degraded toast and the full-outage overlay based on whether * any WebSocket is still alive). * * Sequence: try(2s) -> wait 3s -> try(5s) -> wait 5s -> try(8s) -> fail. * Worst-case wall-clock = 23s, which fits inside the 30s ServerStatusContext * poll cadence, so the next tick acts as the next retry without overlap. */ const STATUS_RETRY_SCHEDULE: ReadonlyArray<{ timeoutMs: number; pauseAfterMs: number | null; }> = [ { timeoutMs: 2000, pauseAfterMs: 3000 }, { timeoutMs: 5000, pauseAfterMs: 5000 }, { timeoutMs: 8000, pauseAfterMs: null }, ]; function isTransientStatusError(error: unknown): boolean { if (!axios.isAxiosError(error)) return false; if (error.response) { // Definitive server response (even 5xx) is not something more retries // will fix in a useful timeframe; bail out and report it normally. return false; } const code = error.code; if (!code) { // No code + no response means classic network error (offline / DNS / TCP) return true; } return ( code === "ECONNABORTED" || code === "ETIMEDOUT" || code === "ERR_NETWORK" || code === "ECONNREFUSED" || code === "ECONNRESET" ); } export async function getAllServerStatuses(): Promise< Record > { let lastError: unknown = null; for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) { const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i]; const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1; try { const response = await statsApi.get("/status", { timeout: timeoutMs, // Silence per-attempt interceptor logging & health-monitor side // effects on all attempts except the final one, so background // blips don't look like real outages. __silentRetry: !isFinalAttempt, } as AxiosRequestConfig & { __silentRetry?: boolean }); return response.data || {}; } catch (error) { lastError = error; if (!isTransientStatusError(error)) { break; } if (pauseAfterMs === null) { break; } await new Promise((resolve) => setTimeout(resolve, pauseAfterMs)); } } handleApiError(lastError, "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"); throw error; } } export async function getServerMetricsById( id: number, ): Promise { try { const response = await statsApi.get(`/metrics/${id}`, { // Treat 404 as an expected "no metrics yet / disabled" signal rather // than an error so we don't spam warn logs on the client. validateStatus: (status) => status === 200 || status === 404, }); if (response.status === 404) { return null; } return response.data; } catch (error) { // If a 404 still slips through (e.g. intercepted before reaching here), // swallow it quietly; everything else still flows through handleApiError. if (axios.isAxiosError(error) && error.response?.status === 404) { return null; } handleApiError(error, "fetch server metrics"); throw error; } } export async function startMetricsPolling(hostId: number): Promise<{ success: boolean; requires_totp?: boolean; sessionId?: string; prompt?: string; viewerSessionId?: string; connectionLogs?: ApiConnectionLog[]; }> { try { const response = await statsApi.post(`/metrics/start/${hostId}`); return response.data; } catch (error: unknown) { if ( axios.isAxiosError(error) && error.response?.data?.connectionLogs ) { const data = error.response.data; const errorWithLogs = new Error( data.error || data.message || error.message, ); Object.assign(errorWithLogs, { connectionLogs: data.connectionLogs, }); throw errorWithLogs; } handleApiError(error, "start metrics polling"); throw error; } } export async function stopMetricsPolling( hostId: number, viewerSessionId?: string, ): Promise { try { await statsApi.post(`/metrics/stop/${hostId}`, { viewerSessionId }); } catch (error) { handleApiError(error, "stop metrics polling"); throw error; } } export async function sendMetricsHeartbeat( viewerSessionId: string, ): Promise { try { await statsApi.post("/metrics/heartbeat", { viewerSessionId }); } catch (error) { handleApiError(error, "send metrics heartbeat"); throw error; } } export async function registerMetricsViewer(hostId: number): Promise<{ success: boolean; viewerSessionId?: string; skipped?: boolean; reason?: string; }> { try { const response = await statsApi.post("/metrics/register-viewer", { hostId, }); return response.data; } catch (error) { handleApiError(error, "register metrics viewer"); throw error; } } export async function unregisterMetricsViewer( hostId: number, viewerSessionId: string, ): Promise { try { await statsApi.post("/metrics/unregister-viewer", { hostId, viewerSessionId, }); } catch (error) { handleApiError(error, "unregister metrics viewer"); throw error; } } export async function submitMetricsTOTP( sessionId: string, totpCode: string, ): Promise<{ success: boolean; viewerSessionId?: string; }> { try { const response = await statsApi.post("/metrics/connect-totp", { sessionId, totpCode, }); return response.data; } catch (error) { handleApiError(error, "submit metrics TOTP"); throw error; } } export async function refreshServerPolling(): Promise { try { await statsApi.post("/refresh"); } catch (error) { console.warn("Failed to refresh server polling:", error); } } export async function notifyHostCreatedOrUpdated( hostId: number, ): Promise { try { await statsApi.post("/host-updated", { hostId }); } catch (error) { console.warn("Failed to notify stats server of host update:", error); } } // ============================================================================ // GLOBAL MONITORING SETTINGS // ============================================================================ export async function getGlobalMonitoringSettings(): Promise<{ statusCheckInterval: number; metricsInterval: number; }> { try { const response = await statsApi.get("/global-settings"); return response.data; } catch (error) { handleApiError(error, "fetch global monitoring settings"); } } export async function updateGlobalMonitoringSettings(settings: { statusCheckInterval?: number; metricsInterval?: number; }): Promise { try { await statsApi.post("/global-settings", settings); } catch (error) { handleApiError(error, "update global monitoring settings"); } } // ============================================================================ // LOG LEVEL SETTINGS // ============================================================================ export async function getLogLevel(): Promise<{ level: string }> { try { const response = await authApi.get("/users/log-level"); return response.data; } catch (error) { handleApiError(error, "fetch log level"); } } export async function updateLogLevel(level: string): Promise { try { await authApi.patch("/users/log-level", { level }); } catch (error) { handleApiError(error, "update log level"); } } // ============================================================================ // SESSION TIMEOUT SETTINGS // ============================================================================ export async function getSessionTimeout(): Promise<{ timeoutHours: number }> { try { const response = await authApi.get("/users/session-timeout"); return response.data; } catch (error) { handleApiError(error, "fetch session timeout"); } } export async function updateSessionTimeout( timeoutHours: number, ): Promise { try { await authApi.patch("/users/session-timeout", { timeoutHours }); } catch (error) { handleApiError(error, "update session timeout"); } } // ============================================================================ // GUACAMOLE SETTINGS // ============================================================================ export async function getGuacamoleSettings(): Promise<{ enabled: boolean; url: string; }> { try { const response = await authApi.get("/users/guacamole-settings"); return response.data; } catch (error) { handleApiError(error, "fetch guacamole settings"); } } export async function updateGuacamoleSettings(settings: { enabled?: boolean; url?: string; }): Promise { try { await authApi.patch("/users/guacamole-settings", settings); } catch (error) { handleApiError(error, "update guacamole settings"); } } // ============================================================================ // 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, rememberMe: boolean = false, ): Promise { try { const response = await authApi.post("/users/login", { username, password, rememberMe, }); const isInIframe = typeof window !== "undefined" && window.self !== window.top; if (isInIframe && isElectron() && response.data.success) { try { window.parent.postMessage( { type: "AUTH_SUCCESS", source: "login_api", platform: "desktop", timestamp: Date.now(), }, window.location.origin, ); } catch (e) { console.error("[main-axios] Error posting message to parent:", e); } } if (response.data.success && !response.data.requires_totp) { markUserAuthenticated(); } return { success: response.data.success, is_admin: response.data.is_admin, username: response.data.username, requires_totp: response.data.requires_totp, temp_token: response.data.temp_token, rememberMe: response.data.rememberMe, is_oidc: response.data.is_oidc, totp_enabled: response.data.totp_enabled, data_unlocked: response.data.data_unlocked, }; } catch (error) { throw handleApiError(error, "login user"); } } export async function logoutUser(): Promise<{ success: boolean; message: string; }> { try { const response = await authApi.post("/users/logout"); clearTermixSessionStorage(); if (isElectron()) { const electronAPI = ( window as unknown as { electronAPI?: { clearSessionCookies?: () => Promise }; } ).electronAPI; electronAPI?.clearSessionCookies?.().catch(() => {}); } else { const isSecure = window.location.protocol === "https:"; const cookieString = isSecure ? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Lax" : "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax"; document.cookie = cookieString; } return response.data; } catch (error) { clearTermixSessionStorage(); if (isElectron()) { const electronAPI = ( window as unknown as { electronAPI?: { clearSessionCookies?: () => Promise }; } ).electronAPI; electronAPI?.clearSessionCookies?.().catch(() => {}); } else { const isSecure = window.location.protocol === "https:"; const cookieString = isSecure ? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Lax" : "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax"; document.cookie = cookieString; } handleApiError(error, "logout user"); } } export async function getUserInfo(): Promise { try { const response = await authApi.get("/users/me"); markUserAuthenticated(); return response.data; } catch (error) { handleApiError(error, "fetch user info"); } } export async function getCurrentToken(): Promise { try { const response = await authApi.get("/users/me/token"); return response.data?.token ?? null; } catch { return null; } } export async function unlockUserData( password: string, ): Promise<{ success: boolean; message: string }> { try { const response = await authApi.post("/users/unlock-data", { password }); return response.data; } catch (error) { handleApiError(error, "unlock user data"); } } 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 getPasswordLoginAllowed(): Promise<{ allowed: boolean }> { try { const response = await authApi.get("/users/password-login-allowed"); return response.data; } catch (error) { handleApiError(error, "check password login status"); } } export async function getOIDCConfig(): Promise> { try { const response = await authApi.get("/users/oidc-config"); return response.data; } catch (error: unknown) { console.warn( "Failed to fetch OIDC config:", error.response?.data?.error || error.message, ); return null; } } export async function getAdminOIDCConfig(): Promise> { try { const response = await authApi.get("/users/oidc-config/admin"); return response.data; } catch (error) { handleApiError(error, "fetch admin OIDC config"); } } export async function getSetupRequired(): Promise<{ setup_required: boolean }> { try { const response = await authApi.get("/users/setup-required"); return response.data; } catch (error) { handleApiError(error, "check setup status"); } } 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 changePassword(oldPassword: string, newPassword: string) { try { const response = await authApi.post("/users/change-password", { oldPassword, newPassword, }); return response.data; } catch (error) { handleApiError(error, "change password"); } } export async function getOIDCAuthorizeUrl( rememberMe = false, desktopCallbackPort?: number, ): Promise { try { const response = await authApi.get("/users/oidc/authorize", { params: { rememberMe, desktopCallbackPort }, }); 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 getSessions(): Promise<{ sessions: { id: string; userId: string; username?: string; deviceType: string; deviceInfo: string; createdAt: string; expiresAt: string; lastActiveAt: string; isRevoked?: boolean; isCurrentSession?: boolean; }[]; }> { try { const response = await authApi.get("/users/sessions"); return response.data; } catch (error) { handleApiError(error, "fetch sessions"); } } export async function revokeSession( sessionId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await authApi.delete(`/users/sessions/${sessionId}`); return response.data; } catch (error) { handleApiError(error, "revoke session"); } } export async function revokeAllUserSessions( userId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await authApi.post("/users/sessions/revoke-all", { targetUserId: userId, exceptCurrent: false, }); return response.data; } catch (error) { handleApiError(error, "revoke all user sessions"); } } export interface ApiKey { id: string; name: string; userId: string; username: string | null; tokenPrefix: string; createdAt: string; expiresAt: string | null; lastUsedAt: string | null; isActive: boolean; } export interface CreatedApiKey extends ApiKey { token: string; } export async function createApiKey( name: string, userId: string, expiresAt?: string, ): Promise { try { const response = await authApi.post("/users/api-keys", { name, userId, expiresAt: expiresAt ?? null, }); return response.data; } catch (error) { handleApiError(error, "create API key"); } } export async function getApiKeys(): Promise<{ apiKeys: ApiKey[] }> { try { const response = await authApi.get("/users/api-keys"); return response.data; } catch (error) { handleApiError(error, "fetch API keys"); } } export async function deleteApiKey( keyId: string, ): Promise<{ success: boolean }> { try { const response = await authApi.delete(`/users/api-keys/${keyId}`); return response.data; } catch (error) { handleApiError(error, "delete API key"); } } export async function makeUserAdmin( userId: string, ): Promise> { try { const response = await authApi.post("/users/make-admin", { userId }); return response.data; } catch (error) { handleApiError(error, "make user admin"); } } export async function removeAdminStatus( userId: string, ): Promise> { try { const response = await authApi.post("/users/remove-admin", { userId }); 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 updatePasswordLoginAllowed( allowed: boolean, ): Promise<{ allowed: boolean }> { try { const response = await authApi.patch("/users/password-login-allowed", { allowed, }); return response.data; } catch (error) { handleApiError(error, "update password login allowed"); } } export async function getPasswordResetAllowed(): Promise { try { const response = await authApi.get("/users/password-reset-allowed"); return response.data.allowed; } catch (error) { handleApiError(error, "get password reset allowed"); } } export async function updatePasswordResetAllowed( allowed: boolean, ): Promise<{ allowed: boolean }> { try { const response = await authApi.patch("/users/password-reset-allowed", { allowed, }); return response.data; } catch (error) { handleApiError(error, "update password reset allowed"); } } export async function updateOIDCConfig( config: Record, ): Promise> { try { const response = await authApi.post("/users/oidc-config", config); return response.data; } catch (error) { handleApiError(error, "update OIDC config"); } } export async function disableOIDCConfig(): Promise> { try { const response = await authApi.delete("/users/oidc-config"); return response.data; } catch (error) { handleApiError(error, "disable 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, rememberMe: boolean = false, ): Promise { try { const response = await authApi.post("/users/totp/verify-login", { temp_token, totp_code, rememberMe, }); const isInIframe = typeof window !== "undefined" && window.self !== window.top; if (isInIframe && isElectron() && response.data.success) { try { window.parent.postMessage( { type: "AUTH_SUCCESS", source: "totp_verify", platform: "desktop", timestamp: Date.now(), }, window.location.origin, ); } catch (e) { console.error("[main-axios] Error posting message to parent:", e); } } if (response.data.success) { markUserAuthenticated(); } 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(): Promise<{ alerts: Array>; }> { try { const response = await authApi.get(`/alerts`); return response.data; } catch (error) { handleApiError(error, "fetch user alerts"); throw error; } } export async function dismissAlert( alertId: string, ): Promise> { try { const response = await authApi.post("/alerts/dismiss", { alertId }); return response.data; } catch (error) { handleApiError(error, "dismiss alert"); throw error; } } // ============================================================================ // 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( checkRemote = true, ): Promise> { try { const response = await authApi.get( `/version${checkRemote ? "" : "?checkRemote=false"}`, ); return response.data; } catch (error) { handleApiError(error, "fetch version info"); } } // ============================================================================ // DATABASE HEALTH // ============================================================================ export async function getDatabaseHealth(): Promise> { try { const response = await authApi.get("/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) { throw handleApiError(error, "fetch credentials"); } } export async function getCredentialDetails( credentialId: number, ): Promise> { try { const response = await authApi.get(`/credentials/${credentialId}`); return response.data; } catch (error) { throw handleApiError(error, "fetch credential details"); } } export async function createCredential( credentialData: Record, ): Promise> { try { const response = await authApi.post("/credentials", credentialData); return response.data; } catch (error) { throw handleApiError(error, "create credential"); } } export async function updateCredential( credentialId: number, credentialData: Record, ): Promise> { try { const response = await authApi.put( `/credentials/${credentialId}`, credentialData, ); return response.data; } catch (error) { throw handleApiError(error, "update credential"); } } export async function deleteCredential( credentialId: number, ): Promise> { try { const response = await authApi.delete(`/credentials/${credentialId}`); return response.data; } catch (error) { throw 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 getSSHHostWithCredentials( hostId: number, ): Promise> { try { const response = await sshHostApi.get( `/db/host/${hostId}/with-credentials`, ); return response.data; } catch (error) { handleApiError(error, "fetch SSH host with credentials"); } } export async function getHostPassword( hostId: number, field: "password" | "sudoPassword" = "password", ): Promise { try { const response = await sshHostApi.get( `/db/host/${hostId}/password?field=${field}`, ); return response.data?.value || null; } catch { return null; } } export async function applyCredentialToHost( hostId: number, credentialId: number, ): Promise> { try { const response = await sshHostApi.post( `/db/host/${hostId}/apply-credential`, { credentialId }, ); return response.data; } catch (error) { throw handleApiError(error, "apply credential to host"); } } export async function removeCredentialFromHost( hostId: number, ): Promise> { try { const response = await sshHostApi.delete(`/db/host/${hostId}/credential`); return response.data; } catch (error) { throw handleApiError(error, "remove credential from host"); } } export async function migrateHostToCredential( hostId: number, credentialName: string, ): Promise> { try { const response = await sshHostApi.post( `/db/host/${hostId}/migrate-to-credential`, { credentialName }, ); return response.data; } catch (error) { throw handleApiError(error, "migrate host to credential"); } } // ============================================================================ // SSH FOLDER MANAGEMENT // ============================================================================ export async function getFoldersWithStats(): Promise> { try { const response = await authApi.get("/host/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("/host/folders/rename", { oldName, newName, }); return response.data; } catch (error) { handleApiError(error, "rename folder"); } } export async function getSSHFolders(): Promise { try { sshLogger.info("Fetching SSH folders", { operation: "fetch_ssh_folders", }); const response = await authApi.get("/host/folders"); sshLogger.success("SSH folders fetched successfully", { operation: "fetch_ssh_folders", count: response.data.length, }); return response.data; } catch (error) { sshLogger.error("Failed to fetch SSH folders", error, { operation: "fetch_ssh_folders", }); handleApiError(error, "fetch SSH folders"); throw error; } } export async function updateFolderMetadata( name: string, color?: string, icon?: string, ): Promise { try { sshLogger.info("Updating folder metadata", { operation: "update_folder_metadata", name, color, icon, }); await authApi.put("/host/folders/metadata", { name, color, icon, }); sshLogger.success("Folder metadata updated successfully", { operation: "update_folder_metadata", name, }); } catch (error) { sshLogger.error("Failed to update folder metadata", error, { operation: "update_folder_metadata", name, }); handleApiError(error, "update folder metadata"); throw error; } } export async function deleteAllHostsInFolder( folderName: string, ): Promise<{ deletedCount: number }> { try { sshLogger.info("Deleting all hosts in folder", { operation: "delete_folder_hosts", folderName, }); const response = await authApi.delete( `/host/folders/${encodeURIComponent(folderName)}/hosts`, ); sshLogger.success("All hosts in folder deleted successfully", { operation: "delete_folder_hosts", folderName, deletedCount: response.data.deletedCount, }); return response.data; } catch (error) { sshLogger.error("Failed to delete hosts in folder", error, { operation: "delete_folder_hosts", folderName, }); handleApiError(error, "delete hosts in folder"); throw error; } } export async function renameCredentialFolder( oldName: string, newName: string, ): Promise> { try { const response = await authApi.put("/credentials/folders/rename", { oldName, newName, }); return response.data; } catch (error) { throw handleApiError(error, "rename credential folder"); } } export async function detectKeyType( privateKey: string, keyPassword?: string, ): Promise> { try { const response = await authApi.post("/credentials/detect-key-type", { privateKey, keyPassword, }); return response.data; } catch (error) { throw handleApiError(error, "detect key type"); } } export async function detectPublicKeyType( publicKey: string, ): Promise> { try { const response = await authApi.post("/credentials/detect-public-key-type", { publicKey, }); return response.data; } catch (error) { throw handleApiError(error, "detect public key type"); } } export async function validateKeyPair( privateKey: string, publicKey: string, keyPassword?: string, ): Promise> { try { const response = await authApi.post("/credentials/validate-key-pair", { privateKey, publicKey, keyPassword, }); return response.data; } catch (error) { throw handleApiError(error, "validate key pair"); } } export async function generatePublicKeyFromPrivate( privateKey: string, keyPassword?: string, ): Promise> { try { const response = await authApi.post("/credentials/generate-public-key", { privateKey, keyPassword, }); return response.data; } catch (error) { throw handleApiError(error, "generate public key from private key"); } } export async function generateKeyPair( keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256", keySize?: number, passphrase?: string, ): Promise> { try { const response = await authApi.post("/credentials/generate-key-pair", { keyType, keySize, passphrase, }); return response.data; } catch (error) { throw handleApiError(error, "generate SSH key pair"); } } export async function deployCredentialToHost( credentialId: number, targetHostId: number, ): Promise> { try { const response = await authApi.post( `/credentials/${credentialId}/deploy-to-host`, { targetHostId }, ); return response.data; } catch (error) { throw handleApiError(error, "deploy credential to host"); } } // ============================================================================ // SNIPPETS API // ============================================================================ export async function getSnippets(): Promise> { try { const response = await authApi.get("/snippets"); return response.data; } catch (error) { throw handleApiError(error, "fetch snippets"); } } export async function createSnippet( snippetData: Record, ): Promise> { try { const response = await authApi.post("/snippets", snippetData); return response.data; } catch (error) { throw handleApiError(error, "create snippet"); } } export async function updateSnippet( snippetId: number, snippetData: Record, ): Promise> { try { const response = await authApi.put(`/snippets/${snippetId}`, snippetData); return response.data; } catch (error) { throw handleApiError(error, "update snippet"); } } export async function deleteSnippet( snippetId: number, ): Promise> { try { const response = await authApi.delete(`/snippets/${snippetId}`); return response.data; } catch (error) { throw handleApiError(error, "delete snippet"); } } export async function executeSnippet( snippetId: number, hostId: number, ): Promise<{ success: boolean; output: string; error?: string }> { try { const response = await authApi.post("/snippets/execute", { snippetId, hostId, }); return response.data; } catch (error) { throw handleApiError(error, "execute snippet"); } } // ============================================================================ // MISCELLANEOUS API CALLS // ============================================================================ export interface NetworkTopologyNode { data: { id: string; label?: string; ip?: string; status?: string; tags?: string[]; parent?: string; color?: string; }; position?: { x: number; y: number }; } export interface NetworkTopologyEdge { data: { id?: string; source: string; target: string; }; } export interface NetworkTopologyData { nodes: NetworkTopologyNode[]; edges: NetworkTopologyEdge[]; } export async function getNetworkTopology(): Promise { try { const response = await authApi.get("/network-topology/"); return response.data; } catch (error) { throw handleApiError(error, "fetch network topology"); } } export async function saveNetworkTopology( topology: NetworkTopologyData, ): Promise<{ success: boolean }> { try { const response = await authApi.post("/network-topology/", { topology }); return response.data; } catch (error) { throw handleApiError(error, "save network topology"); } } export async function getSnippetFolders(): Promise> { try { const response = await authApi.get("/snippets/folders"); return response.data; } catch (error) { throw handleApiError(error, "fetch snippet folders"); } } export async function createSnippetFolder(folderData: { name: string; color?: string; icon?: string; }): Promise> { try { const response = await authApi.post("/snippets/folders", folderData); return response.data; } catch (error) { throw handleApiError(error, "create snippet folder"); } } export async function updateSnippetFolderMetadata( folderName: string, metadata: { color?: string; icon?: string }, ): Promise> { try { const response = await authApi.put( `/snippets/folders/${encodeURIComponent(folderName)}/metadata`, metadata, ); return response.data; } catch (error) { throw handleApiError(error, "update snippet folder metadata"); } } export async function renameSnippetFolder( oldName: string, newName: string, ): Promise<{ success: boolean; oldName: string; newName: string }> { try { const response = await authApi.put("/snippets/folders/rename", { oldName, newName, }); return response.data; } catch (error) { throw handleApiError(error, "rename snippet folder"); } } export async function deleteSnippetFolder( folderName: string, ): Promise<{ success: boolean }> { try { const response = await authApi.delete( `/snippets/folders/${encodeURIComponent(folderName)}`, ); return response.data; } catch (error) { throw handleApiError(error, "delete snippet folder"); } } export async function reorderSnippets( updates: Array<{ id: number; order: number; folder?: string }>, ): Promise<{ success: boolean }> { try { const response = await authApi.post("/snippets/reorder", { snippets: updates, }); return response.data; } catch (error) { throw handleApiError(error, "reorder snippets"); } } // ============================================================================ // DASHBOARD API // ============================================================================ export interface UptimeInfo { uptimeMs: number; uptimeSeconds: number; formatted: string; } export interface RecentActivityItem { id: number; userId: string; type: | "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker" | "telnet" | "vnc" | "rdp"; hostId: number; hostName: string; timestamp: string; } export async function getUptime(): Promise { try { const response = await dashboardApi.get("/uptime"); return response.data; } catch (error) { throw handleApiError(error, "fetch uptime"); } } export async function getRecentActivity( limit?: number, ): Promise { try { const response = await dashboardApi.get("/activity/recent", { params: { limit }, }); return response.data; } catch (error) { throw handleApiError(error, "fetch recent activity"); } } export async function logActivity( type: | "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker" | "rdp" | "vnc" | "telnet", hostId: number, hostName: string, ): Promise<{ message: string; id: number | string }> { try { const response = await dashboardApi.post("/activity/log", { type, hostId, hostName, }); return response.data; } catch (error) { throw handleApiError(error, "log activity"); } } export async function resetRecentActivity(): Promise<{ message: string }> { try { const response = await dashboardApi.delete("/activity/reset"); return response.data; } catch (error) { throw handleApiError(error, "reset recent activity"); } } // ============================================================================ // COMMAND HISTORY API // ============================================================================ export async function saveCommandToHistory( hostId: number, command: string, ): Promise<{ id: number; command: string; executedAt: string }> { try { const response = await authApi.post("/terminal/command_history", { hostId, command, }); return response.data; } catch (error) { throw handleApiError(error, "save command to history"); } } export async function getCommandHistory( hostId: number, limit: number = 100, ): Promise { try { const response = await authApi.get(`/terminal/command_history/${hostId}`, { params: { limit }, }); return response.data; } catch (error) { throw handleApiError(error, "fetch command history"); } } export async function deleteCommandFromHistory( hostId: number, command: string, ): Promise<{ success: boolean }> { try { const response = await authApi.post("/terminal/command_history/delete", { hostId, command, }); return response.data; } catch (error) { throw handleApiError(error, "delete command from history"); } } export async function clearCommandHistory( hostId: number, ): Promise<{ success: boolean }> { try { const response = await authApi.delete( `/terminal/command_history/${hostId}`, ); return response.data; } catch (error) { throw handleApiError(error, "clear command history"); } } // ============================================================================ // OIDC ACCOUNT LINKING // ============================================================================ export async function linkOIDCToPasswordAccount( oidcUserId: string, targetUsername: string, ): Promise<{ success: boolean; message: string }> { try { const response = await authApi.post("/users/link-oidc-to-password", { oidcUserId, targetUsername, }); return response.data; } catch (error) { throw handleApiError(error, "link OIDC account to password account"); } } export async function unlinkOIDCFromPasswordAccount( userId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await authApi.post("/users/unlink-oidc-from-password", { userId, }); return response.data; } catch (error) { throw handleApiError(error, "unlink OIDC from password account"); } } export interface GuacamoleTokenRequest { protocol: "rdp" | "vnc" | "telnet"; hostname: string; port?: number; username?: string; password?: string; domain?: string; security?: string; ignoreCert?: boolean; guacamoleConfig?: { colorDepth?: number; width?: number; height?: number; dpi?: number; resizeMethod?: string; forceLossless?: boolean; disableAudio?: boolean; enableAudioInput?: boolean; enableWallpaper?: boolean; enableTheming?: boolean; enableFontSmoothing?: boolean; enableFullWindowDrag?: boolean; enableDesktopComposition?: boolean; enableMenuAnimations?: boolean; disableBitmapCaching?: boolean; disableOffscreenCaching?: boolean; disableGlyphCaching?: boolean; disableGfx?: boolean; enablePrinting?: boolean; printerName?: string; enableDrive?: boolean; driveName?: string; drivePath?: string; createDrivePath?: boolean; disableDownload?: boolean; disableUpload?: boolean; enableTouch?: boolean; clientName?: string; console?: boolean; initialProgram?: string; serverLayout?: string; timezone?: string; gatewayHostname?: string; gatewayPort?: number; gatewayUsername?: string; gatewayPassword?: string; gatewayDomain?: string; remoteApp?: string; remoteAppDir?: string; remoteAppArgs?: string; normalizeClipboard?: string; disableCopy?: boolean; disablePaste?: boolean; cursor?: string; swapRedBlue?: boolean; readOnly?: boolean; recordingPath?: string; recordingName?: string; createRecordingPath?: boolean; recordingExcludeOutput?: boolean; recordingExcludeMouse?: boolean; recordingIncludeKeys?: boolean; wolSendPacket?: boolean; wolMacAddr?: string; wolBroadcastAddr?: string; wolUdpPort?: number; wolWaitTime?: number; }; } export interface GuacamoleTokenResponse { token: string; } type GuacamoleConfigSource = { guacamoleConfig?: string | Record | null; }; export function getGuacamoleDpi( source?: GuacamoleConfigSource, ): number | undefined { const config = source?.guacamoleConfig; if (!config) return undefined; let dpi: unknown; if (typeof config === "string") { try { dpi = JSON.parse(config).dpi; } catch { return undefined; } } else { dpi = config.dpi; } const parsedDpi = typeof dpi === "string" ? Number(dpi) : dpi; if ( typeof parsedDpi !== "number" || !Number.isFinite(parsedDpi) || parsedDpi <= 0 ) { return undefined; } return Math.trunc(parsedDpi); } function toGuacamoleParams( config: GuacamoleTokenRequest["guacamoleConfig"], ): Record { if (!config) return {}; const params: Record = {}; const mappings: Record = { colorDepth: "color-depth", resizeMethod: "resize-method", forceLossless: "force-lossless", disableAudio: "disable-audio", enableAudioInput: "enable-audio-input", enableWallpaper: "enable-wallpaper", enableTheming: "enable-theming", enableFontSmoothing: "enable-font-smoothing", enableFullWindowDrag: "enable-full-window-drag", enableDesktopComposition: "enable-desktop-composition", enableMenuAnimations: "enable-menu-animations", disableBitmapCaching: "disable-bitmap-caching", disableOffscreenCaching: "disable-offscreen-caching", disableGlyphCaching: "disable-glyph-caching", disableGfx: "disable-gfx", enablePrinting: "enable-printing", printerName: "printer-name", enableDrive: "enable-drive", driveName: "drive-name", drivePath: "drive-path", createDrivePath: "create-drive-path", disableDownload: "disable-download", disableUpload: "disable-upload", enableTouch: "enable-touch", clientName: "client-name", initialProgram: "initial-program", serverLayout: "server-layout", gatewayHostname: "gateway-hostname", gatewayPort: "gateway-port", gatewayUsername: "gateway-username", gatewayPassword: "gateway-password", gatewayDomain: "gateway-domain", remoteApp: "remote-app", remoteAppDir: "remote-app-dir", remoteAppArgs: "remote-app-args", normalizeClipboard: "normalize-clipboard", disableCopy: "disable-copy", disablePaste: "disable-paste", swapRedBlue: "swap-red-blue", readOnly: "read-only", recordingPath: "recording-path", recordingName: "recording-name", createRecordingPath: "create-recording-path", recordingExcludeOutput: "recording-exclude-output", recordingExcludeMouse: "recording-exclude-mouse", recordingIncludeKeys: "recording-include-keys", wolSendPacket: "wol-send-packet", wolMacAddr: "wol-mac-addr", wolBroadcastAddr: "wol-broadcast-addr", wolUdpPort: "wol-udp-port", wolWaitTime: "wol-wait-time", }; for (const [key, value] of Object.entries(config)) { if (value !== undefined && value !== null && value !== "") { const paramName = mappings[key] || key; if (typeof value === "boolean") { params[paramName] = value ? "true" : "false"; } else { params[paramName] = value; } } } return params; } export async function getGuacamoleToken( request: GuacamoleTokenRequest, ): Promise { try { const guacParams = toGuacamoleParams(request.guacamoleConfig); const response = await authApi.post("/guacamole/token", { type: request.protocol, hostname: request.hostname, port: request.port, username: request.username, password: request.password, domain: request.domain, security: request.security, "ignore-cert": request.ignoreCert, ...guacParams, }); return response.data; } catch (error) { throw handleApiError(error, "get guacamole token"); } } export async function getGuacamoleTokenFromHost( hostId: number, ): Promise { try { const response = await authApi.post(`/guacamole/connect-host/${hostId}`); return response.data; } catch (error) { throw handleApiError(error, "get guacamole token from host"); } } export async function getGuacdStatus(): Promise<{ guacd: { status: string }; }> { const response = await authApi.get("/guacamole/status"); return response.data; } // ============================================================================ // RBAC MANAGEMENT // ============================================================================ export async function getRoles(): Promise<{ roles: Role[] }> { try { const response = await rbacApi.get("/rbac/roles"); return response.data; } catch (error) { throw handleApiError(error, "fetch roles"); } } export async function createRole(roleData: { name: string; displayName: string; description?: string | null; }): Promise<{ role: Role }> { try { const response = await rbacApi.post("/rbac/roles", roleData); return response.data; } catch (error) { throw handleApiError(error, "create role"); } } export async function updateRole( roleId: number, roleData: { displayName?: string; description?: string | null; }, ): Promise<{ role: Role }> { try { const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData); return response.data; } catch (error) { throw handleApiError(error, "update role"); } } export async function deleteRole( roleId: number, ): Promise<{ success: boolean }> { try { const response = await rbacApi.delete(`/rbac/roles/${roleId}`); return response.data; } catch (error) { throw handleApiError(error, "delete role"); } } export async function getUserRoles( userId: string, ): Promise<{ roles: UserRole[] }> { try { const response = await rbacApi.get(`/rbac/users/${userId}/roles`); return response.data; } catch (error) { throw handleApiError(error, "fetch user roles"); } } export async function assignRoleToUser( userId: string, roleId: number, ): Promise<{ success: boolean }> { try { const response = await rbacApi.post(`/rbac/users/${userId}/roles`, { roleId, }); return response.data; } catch (error) { throw handleApiError(error, "assign role to user"); } } export async function removeRoleFromUser( userId: string, roleId: number, ): Promise<{ success: boolean }> { try { const response = await rbacApi.delete( `/rbac/users/${userId}/roles/${roleId}`, ); return response.data; } catch (error) { throw handleApiError(error, "remove role from user"); } } export async function shareHost( hostId: number, shareData: { targetType: "user" | "role"; targetUserId?: string; targetRoleId?: number; permissionLevel: "view"; durationHours?: number; }, ): Promise<{ success: boolean }> { try { const response = await rbacApi.post( `/rbac/host/${hostId}/share`, shareData, ); return response.data; } catch (error) { throw handleApiError(error, "share host"); } } export async function getHostAccess( hostId: number, ): Promise<{ accessList: AccessRecord[] }> { try { const response = await rbacApi.get(`/rbac/host/${hostId}/access`); return response.data; } catch (error) { throw handleApiError(error, "fetch host access"); } } export async function revokeHostAccess( hostId: number, accessId: number, ): Promise<{ success: boolean }> { try { const response = await rbacApi.delete( `/rbac/host/${hostId}/access/${accessId}`, ); return response.data; } catch (error) { throw handleApiError(error, "revoke host access"); } } // ============================================================================ // SNIPPET SHARING // ============================================================================ export async function shareSnippet( snippetId: number, shareData: { targetType: "user" | "role"; targetUserId?: string; targetRoleId?: number; durationHours?: number; }, ): Promise<{ success: boolean }> { try { const response = await rbacApi.post( `/rbac/snippet/${snippetId}/share`, shareData, ); return response.data; } catch (error) { throw handleApiError(error, "share snippet"); } } export async function getSnippetAccess( snippetId: number, ): Promise<{ accessList: AccessRecord[] }> { try { const response = await rbacApi.get(`/rbac/snippet/${snippetId}/access`); return response.data; } catch (error) { throw handleApiError(error, "fetch snippet access"); } } export async function revokeSnippetAccess( snippetId: number, accessId: number, ): Promise<{ success: boolean }> { try { const response = await rbacApi.delete( `/rbac/snippet/${snippetId}/access/${accessId}`, ); return response.data; } catch (error) { throw handleApiError(error, "revoke snippet access"); } } export async function getSharedSnippets(): Promise<{ sharedSnippets: Array<{ id: number; name: string; content: string; description: string | null; folder: string | null; ownerUsername: string; permissionLevel: string; expiresAt: string | null; }>; }> { try { const response = await rbacApi.get("/rbac/shared-snippets"); return response.data; } catch (error) { handleApiError(error, "fetch shared snippets"); } } // ============================================================================ // DOCKER MANAGEMENT API // ============================================================================ export async function connectDockerSession( sessionId: string, hostId: number, config?: { userProvidedPassword?: string; userProvidedSshKey?: string; userProvidedKeyPassword?: string; forceKeyboardInteractive?: boolean; useSocks5?: boolean; socks5Host?: string; socks5Port?: number; socks5Username?: string; socks5Password?: string; socks5ProxyChain?: unknown; }, ): Promise<{ success?: boolean; message?: string; requires_totp?: boolean; prompt?: string; isPassword?: boolean; status?: string; reason?: string; connectionLogs?: ApiConnectionLog[]; requires_warpgate?: boolean; url?: string; securityKey?: string; }> { try { const response = await dockerApi.post("/ssh/connect", { sessionId, hostId, ...config, }); return response.data; } catch (error: unknown) { if ( axios.isAxiosError(error) && error.response?.data?.status === "auth_required" ) { return error.response.data; } if ( axios.isAxiosError(error) && error.response?.data?.requires_totp ) { return error.response.data; } if ( axios.isAxiosError(error) && error.response?.data?.requires_warpgate ) { return error.response.data; } if ( axios.isAxiosError(error) && error.response?.data?.connectionLogs ) { const data = error.response.data; const errorWithLogs = new Error( data.error || data.message || error.message, ); Object.assign(errorWithLogs, { connectionLogs: data.connectionLogs, }); throw errorWithLogs; } throw handleApiError(error, "connect to Docker SSH session"); } } export async function verifyDockerTOTP( sessionId: string, totpCode: string, ): Promise<{ status: string; message: string }> { try { const response = await dockerApi.post("/ssh/connect-totp", { sessionId, totpCode, }); return response.data; } catch (error) { throw handleApiError(error, "verify Docker TOTP"); } } export async function verifyDockerWarpgate( sessionId: string, ): Promise<{ status: string; message: string }> { try { const response = await dockerApi.post("/ssh/connect-warpgate", { sessionId, }); return response.data; } catch (error) { throw handleApiError(error, "verify Docker Warpgate"); } } export async function disconnectDockerSession( sessionId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post("/ssh/disconnect", { sessionId, }); return response.data; } catch (error) { throw handleApiError(error, "disconnect from Docker SSH session"); } } export async function keepaliveDockerSession( sessionId: string, ): Promise<{ success: boolean }> { try { const response = await dockerApi.post("/ssh/keepalive", { sessionId, }); return response.data; } catch (error) { throw handleApiError(error, "keepalive Docker SSH session"); } } export async function getDockerSessionStatus( sessionId: string, ): Promise<{ success: boolean; connected: boolean }> { try { const response = await dockerApi.get("/ssh/status", { params: { sessionId }, }); return response.data; } catch (error) { throw handleApiError(error, "get Docker session status"); } } export async function validateDockerAvailability( sessionId: string, ): Promise { try { const response = await dockerApi.get(`/validate/${sessionId}`); return response.data; } catch (error) { throw handleApiError(error, "validate Docker availability"); } } export async function listDockerContainers( sessionId: string, all: boolean = true, ): Promise { try { const response = await dockerApi.get(`/containers/${sessionId}`, { params: { all }, }); return response.data; } catch (error) { throw handleApiError(error, "list Docker containers"); } } export async function getDockerContainerDetails( sessionId: string, containerId: string, ): Promise { try { const response = await dockerApi.get( `/containers/${sessionId}/${containerId}`, ); return response.data; } catch (error) { throw handleApiError(error, "get Docker container details"); } } export async function startDockerContainer( sessionId: string, containerId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post( `/containers/${sessionId}/${containerId}/start`, ); return response.data; } catch (error) { throw handleApiError(error, "start Docker container"); } } export async function stopDockerContainer( sessionId: string, containerId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post( `/containers/${sessionId}/${containerId}/stop`, ); return response.data; } catch (error) { throw handleApiError(error, "stop Docker container"); } } export async function restartDockerContainer( sessionId: string, containerId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post( `/containers/${sessionId}/${containerId}/restart`, ); return response.data; } catch (error) { throw handleApiError(error, "restart Docker container"); } } export async function pauseDockerContainer( sessionId: string, containerId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post( `/containers/${sessionId}/${containerId}/pause`, ); return response.data; } catch (error) { throw handleApiError(error, "pause Docker container"); } } export async function unpauseDockerContainer( sessionId: string, containerId: string, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.post( `/containers/${sessionId}/${containerId}/unpause`, ); return response.data; } catch (error) { throw handleApiError(error, "unpause Docker container"); } } export async function removeDockerContainer( sessionId: string, containerId: string, force: boolean = false, ): Promise<{ success: boolean; message: string }> { try { const response = await dockerApi.delete( `/containers/${sessionId}/${containerId}/remove`, { params: { force }, }, ); return response.data; } catch (error) { throw handleApiError(error, "remove Docker container"); } } export async function getContainerLogs( sessionId: string, containerId: string, options?: DockerLogOptions, ): Promise<{ logs: string }> { try { const response = await dockerApi.get( `/containers/${sessionId}/${containerId}/logs`, { params: options, }, ); return response.data; } catch (error) { throw handleApiError(error, "get container logs"); } } export async function downloadContainerLogs( sessionId: string, containerId: string, options?: DockerLogOptions, ): Promise { try { const response = await dockerApi.get( `/containers/${sessionId}/${containerId}/logs`, { params: { ...options, download: true }, responseType: "blob", }, ); return response.data; } catch (error) { throw handleApiError(error, "download container logs"); } } export async function getContainerStats( sessionId: string, containerId: string, ): Promise { try { const response = await dockerApi.get( `/containers/${sessionId}/${containerId}/stats`, ); return response.data; } catch (error) { throw handleApiError(error, "get container stats"); } } export interface DashboardLayout { cards: Array<{ id: string; enabled: boolean; order: number; panel?: "main" | "side"; height?: number | null; }>; mainWidthPct?: number; } export async function getDashboardPreferences(): Promise { const response = await dashboardApi.get("/dashboard/preferences"); return response.data; } export async function saveDashboardPreferences( layout: DashboardLayout, ): Promise<{ success: boolean }> { const response = await dashboardApi.post("/dashboard/preferences", layout); return response.data; }