import axios, { AxiosError, type AxiosInstance } from "axios"; import type { SSHHost, SSHHostData, TunnelConfig, TunnelStatus, FileManagerFile, FileManagerShortcut, } from "../types/index.js"; import { apiLogger, authLogger, sshLogger, tunnelLogger, fileLogger, statsLogger, systemLogger, dashboardLogger, type LogContext, } from "../lib/frontend-logger.js"; interface FileManagerOperation { name: string; path: string; isSSH: boolean; sshSessionId?: string; hostId: number; } export type ServerStatus = { status: "online" | "offline"; lastChecked: string; }; interface CpuMetrics { percent: number | null; cores: number | null; load: [number, number, number] | null; } interface MemoryMetrics { percent: number | null; usedGiB: number | null; totalGiB: number | null; } interface DiskMetrics { percent: number | null; usedHuman: string | null; totalHuman: string | null; availableHuman?: string | null; } export type ServerMetrics = { cpu: CpuMetrics; memory: MemoryMetrics; disk: DiskMetrics; lastChecked: string; }; interface AuthResponse { token: string; success?: boolean; is_admin?: boolean; username?: string; userId?: string; is_oidc?: boolean; totp_enabled?: boolean; data_unlocked?: boolean; } interface UserInfo { totp_enabled: boolean; userId: string; username: string; is_admin: boolean; is_oidc: boolean; data_unlocked: boolean; } interface UserCount { count: number; } interface OIDCAuthorize { auth_url: string; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ export function isElectron(): boolean { const hasISElectron = ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).IS_ELECTRON === true; const hasElectronAPI = !!( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).electronAPI; const result = hasISElectron || hasElectronAPI; return result; } 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; } } export function setCookie(name: string, value: string, days = 7): void { if (isElectron()) { localStorage.setItem(name, value); } 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()) { const token = localStorage.getItem(name) || undefined; return token; } 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; } } 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) => { const startTime = performance.now(); const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; (config as Record).startTime = startTime; (config as Record).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); if (process.env.NODE_ENV === "development") { logger.requestStart(method, fullUrl, context); } if (isElectron()) { config.headers["X-Electron-App"] = "true"; const token = localStorage.getItem("jwt"); if (token) { config.headers["Authorization"] = `Bearer ${token}`; } } if (typeof window !== "undefined" && (window as any).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"; } } config.headers["User-Agent"] = `Termix-Mobile/${platform}`; } return config; }); instance.interceptors.response.use( (response) => { const endTime = performance.now(); const startTime = (response.config as Record).startTime; const requestId = (response.config as Record).requestId; const responseTime = Math.round(endTime - startTime); 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); } return response; }, (error: AxiosError) => { const endTime = performance.now(); const startTime = (error.config as Record | undefined) ?.startTime; const requestId = (error.config as Record | undefined) ?.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 Record)?.error || (error as Error).message || "Unknown error"; const errorCode = (error.response?.data as Record)?.code || error.code; const context: LogContext = { requestId, method, url: fullUrl, status, responseTime, errorCode, errorMessage: message, operation: "request_error", }; const logger = getLoggerForService(serviceName); if (process.env.NODE_ENV === "development") { 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 isInvalidToken = errorCode === "AUTH_REQUIRED" || errorMessage === "Invalid token" || errorMessage === "Authentication required"; if (isElectron()) { localStorage.removeItem("jwt"); } else { localStorage.removeItem("jwt"); } if ( (isSessionExpired || isInvalidToken) && typeof window !== "undefined" ) { console.warn( "Session expired or invalid token - please log in again", ); document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; import("sonner").then(({ toast }) => { toast.warning("Session expired. Please log in again."); }); const currentPath = window.location.pathname; const isOnAuthPage = currentPath === "/" || currentPath === "/login" || currentPath === "/auth"; if (!isOnAuthPage) { setTimeout(() => window.location.reload(), 1000); } } } 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; export interface ServerConfig { serverUrl: string; lastUpdated: string; } 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 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"; 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" }; } } function getApiUrl(path: string, defaultPort: number): string { const devMode = isDev(); const electronMode = isElectron(); if (electronMode) { 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 path; } } function initializeApiInstances() { // SSH Host Management API (port 30001) sshHostApi = createApiInstance(getApiUrl("/ssh", 30001), "SSH_HOST"); // 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"); // Homepage API (port 30006) homepageApi = createApiInstance(getApiUrl("", 30006), "HOMEPAGE"); } // SSH Host Management API (port 30001) 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; // Homepage API (port 30006) export let homepageApi: AxiosInstance; function initializeApp() { if (isElectron()) { getServerConfig() .then((config) => { if (config?.serverUrl) { configuredServerUrl = config.serverUrl; ( window as Window & typeof globalThis & { IS_ELECTRON?: boolean; electronAPI?: unknown; configuredServerUrl?: string; } ).configuredServerUrl = configuredServerUrl; } 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, "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 response = await sshHostApi.get("/db/host"); return response.data; } catch (error) { handleApiError(error, "fetch SSH hosts"); } } export async function createSSHHost(hostData: SSHHostData): Promise { try { const submitData = { name: hostData.name || "", ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || "", tags: hostData.tags || [], pin: Boolean(hostData.pin), authType: hostData.authType, password: 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, enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], statsConfig: hostData.statsConfig ? typeof hostData.statsConfig === "string" ? hostData.statsConfig : JSON.stringify(hostData.statsConfig) : null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), }; if (!submitData.enableTunnel) { submitData.tunnelConnections = []; } if (!submitData.enableFileManager) { submitData.defaultPath = ""; } if (hostData.authType === "key" && hostData.key instanceof File) { const formData = new FormData(); formData.append("key", hostData.key); const dataWithoutFile = { ...submitData }; delete dataWithoutFile.key; formData.append("data", JSON.stringify(dataWithoutFile)); const response = await sshHostApi.post("/db/host", formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; } else { const response = await sshHostApi.post("/db/host", submitData); return response.data; } } catch (error) { handleApiError(error, "create SSH host"); } } export async function updateSSHHost( hostId: number, hostData: SSHHostData, ): Promise { try { const submitData = { name: hostData.name || "", ip: hostData.ip, port: parseInt(hostData.port.toString()) || 22, username: hostData.username, folder: hostData.folder || "", tags: hostData.tags || [], pin: Boolean(hostData.pin), authType: hostData.authType, password: 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, enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], statsConfig: hostData.statsConfig ? typeof hostData.statsConfig === "string" ? hostData.statsConfig : JSON.stringify(hostData.statsConfig) : null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), }; if (!submitData.enableTunnel) { submitData.tunnelConnections = []; } if (!submitData.enableFileManager) { submitData.defaultPath = ""; } if (hostData.authType === "key" && hostData.key instanceof File) { const formData = new FormData(); formData.append("key", hostData.key); const dataWithoutFile = { ...submitData }; delete dataWithoutFile.key; formData.append("data", JSON.stringify(dataWithoutFile)); const response = await sshHostApi.put(`/db/host/${hostId}`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; } else { const response = await sshHostApi.put(`/db/host/${hostId}`, submitData); return response.data; } } catch (error) { handleApiError(error, "update SSH host"); } } export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{ message: string; success: number; failed: number; errors: string[]; }> { try { const response = await sshHostApi.post("/bulk-import", { hosts }); return response.data; } catch (error) { handleApiError(error, "bulk import SSH hosts"); } } export async function deleteSSHHost( hostId: number, ): Promise> { try { const response = await sshHostApi.delete(`/db/host/${hostId}`); return response.data; } catch (error) { handleApiError(error, "delete SSH host"); } } export async function getSSHHostById(hostId: number): Promise { try { const response = await sshHostApi.get(`/db/host/${hostId}`); return response.data; } catch (error) { handleApiError(error, "fetch SSH host"); } } 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"); } } // ============================================================================ // 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"); } } // ============================================================================ // 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 async function getTunnelStatusByName( tunnelName: string, ): Promise { const statuses = await getTunnelStatuses(); return statuses[tunnelName]; } export async function connectTunnel( tunnelConfig: TunnelConfig, ): Promise> { try { const response = await tunnelApi.post("/tunnel/connect", tunnelConfig); return response.data; } catch (error) { handleApiError(error, "connect tunnel"); } } export async function disconnectTunnel( tunnelName: string, ): Promise> { try { const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName }); return response.data; } catch (error) { handleApiError(error, "disconnect tunnel"); } } export async function cancelTunnel( tunnelName: string, ): Promise> { try { const response = await tunnelApi.post("/tunnel/cancel", { tunnelName }); return response.data; } catch (error) { handleApiError(error, "cancel tunnel"); } } // ============================================================================ // FILE MANAGER METADATA (Recent, Pinned, Shortcuts) // ============================================================================ export async function getFileManagerRecent( hostId: number, ): Promise { try { const response = await sshHostApi.get( `/file_manager/recent?hostId=${hostId}`, ); return response.data || []; } catch { 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; }, ): Promise> { try { const response = await fileManagerApi.post("/ssh/connect", { sessionId, ...config, }); return response.data; } catch (error) { handleApiError(error, "connect SSH"); } } export async function disconnectSSH( sessionId: string, ): Promise> { try { const response = await fileManagerApi.post("/ssh/disconnect", { sessionId, }); return response.data; } catch (error) { handleApiError(error, "disconnect SSH"); } } export async function 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 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 readSSHFile( sessionId: string, path: string, ): Promise<{ content: string; path: string }> { try { const response = await fileManagerApi.get("/ssh/readFile", { params: { sessionId, path }, }); return response.data; } catch (error: 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 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 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; } } // ============================================================================ // FILE MANAGER DATA // ============================================================================ // Recent Files export async function getRecentFiles( hostId: number, ): Promise> { try { const response = await authApi.get("/ssh/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("/ssh/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("/ssh/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("/ssh/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("/ssh/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("/ssh/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("/ssh/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("/ssh/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("/ssh/file_manager/shortcuts", { data: { hostId, path }, }); return response.data; } catch (error) { handleApiError(error, "remove folder shortcut"); throw error; } } // ============================================================================ // SERVER STATISTICS // ============================================================================ export async function getAllServerStatuses(): Promise< Record > { try { const response = await statsApi.get("/status"); return response.data || {}; } catch (error) { handleApiError(error, "fetch server statuses"); } } export async function getServerStatusById(id: number): Promise { try { const response = await statsApi.get(`/status/${id}`); return response.data; } catch (error) { handleApiError(error, "fetch server status"); } } export async function getServerMetricsById(id: number): Promise { try { const response = await statsApi.get(`/metrics/${id}`); return response.data; } catch (error) { handleApiError(error, "fetch server metrics"); } } export async function refreshServerPolling(): Promise { try { await statsApi.post("/refresh"); } catch (error) { // Silently fail - this is a background operation console.warn("Failed to refresh server polling:", error); } } // ============================================================================ // AUTHENTICATION // ============================================================================ export async function registerUser( username: string, password: string, ): Promise> { try { const response = await authApi.post("/users/create", { username, password, }); return response.data; } catch (error) { handleApiError(error, "register user"); } } export async function loginUser( username: string, password: string, ): Promise { try { const response = await authApi.post("/users/login", { username, password }); const hasToken = response.data.token; if (isElectron() && hasToken) { localStorage.setItem("jwt", response.data.token); } const isInIframe = typeof window !== "undefined" && window.self !== window.top; if (isInIframe && hasToken) { localStorage.setItem("jwt", response.data.token); try { window.parent.postMessage( { type: "AUTH_SUCCESS", token: response.data.token, source: "login_api", platform: "desktop", timestamp: Date.now(), }, "*", ); } catch (e) { console.error("[main-axios] Error posting message to parent:", e); } } return { token: response.data.token || "cookie-based", 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, }; } catch (error) { handleApiError(error, "login user"); } } export async function logoutUser(): Promise<{ success: boolean; message: string; }> { try { const response = await authApi.post("/users/logout"); return response.data; } catch (error) { handleApiError(error, "logout user"); } } export async function getUserInfo(): Promise { try { const response = await authApi.get("/users/me"); return response.data; } catch (error) { handleApiError(error, "fetch user info"); } } export async function 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(): Promise { try { const response = await authApi.get("/users/oidc/authorize"); return response.data; } catch (error) { handleApiError(error, "get OIDC authorize URL"); } } // ============================================================================ // USER MANAGEMENT // ============================================================================ export async function getUserList(): Promise<{ users: UserInfo[] }> { try { const response = await authApi.get("/users/list"); return response.data; } catch (error) { handleApiError(error, "fetch user list"); } } export async function makeUserAdmin( username: string, ): Promise> { try { const response = await authApi.post("/users/make-admin", { username }); return response.data; } catch (error) { handleApiError(error, "make user admin"); } } export async function removeAdminStatus( username: string, ): Promise> { try { const response = await authApi.post("/users/remove-admin", { username }); return response.data; } catch (error) { handleApiError(error, "remove admin status"); } } export async function deleteUser( username: string, ): Promise> { try { const response = await authApi.delete("/users/delete-user", { data: { username }, }); return response.data; } catch (error) { handleApiError(error, "delete user"); } } export async function deleteAccount( password: string, ): Promise> { try { const response = await authApi.delete("/users/delete-account", { data: { password }, }); return response.data; } catch (error) { handleApiError(error, "delete account"); } } export async function updateRegistrationAllowed( allowed: boolean, ): Promise> { try { const response = await authApi.patch("/users/registration-allowed", { allowed, }); return response.data; } catch (error) { handleApiError(error, "update registration allowed"); } } export async function 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 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, ): Promise { try { const response = await authApi.post("/users/totp/verify-login", { temp_token, totp_code, }); const hasToken = response.data.token; if (isElectron() && hasToken) { localStorage.setItem("jwt", response.data.token); } const isInIframe = typeof window !== "undefined" && window.self !== window.top; if (isInIframe && hasToken) { localStorage.setItem("jwt", response.data.token); try { window.parent.postMessage( { type: "AUTH_SUCCESS", token: response.data.token, source: "totp_verify", platform: "desktop", timestamp: Date.now(), }, "*", ); } catch (e) { console.error("[main-axios] Error posting message to parent:", e); } } 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"); } } 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"); } } // ============================================================================ // UPDATES & RELEASES // ============================================================================ export async function getReleasesRSS( perPage: number = 100, ): Promise> { try { const response = await authApi.get(`/releases/rss?per_page=${perPage}`); return response.data; } catch (error) { handleApiError(error, "fetch releases RSS"); } } export async function getVersionInfo(): Promise> { try { const response = await authApi.get("/version"); return response.data; } catch (error) { handleApiError(error, "fetch version info"); } } // ============================================================================ // DATABASE HEALTH // ============================================================================ export async function getDatabaseHealth(): Promise> { try { const response = await authApi.get("/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 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("/ssh/db/folders/with-stats"); return response.data; } catch (error) { handleApiError(error, "fetch folders with statistics"); } } export async function renameFolder( oldName: string, newName: string, ): Promise> { try { const response = await authApi.put("/ssh/folders/rename", { oldName, newName, }); return response.data; } catch (error) { handleApiError(error, "rename folder"); } } 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"); } } // ============================================================================ // HOMEPAGE API // ============================================================================ export interface UptimeInfo { uptimeMs: number; uptimeSeconds: number; formatted: string; } export interface RecentActivityItem { id: number; userId: string; type: "terminal" | "file_manager"; hostId: number; hostName: string; timestamp: string; } export async function getUptime(): Promise { try { const response = await homepageApi.get("/uptime"); return response.data; } catch (error) { throw handleApiError(error, "fetch uptime"); } } export async function getRecentActivity( limit?: number, ): Promise { try { const response = await homepageApi.get("/activity/recent", { params: { limit }, }); return response.data; } catch (error) { throw handleApiError(error, "fetch recent activity"); } } export async function logActivity( type: "terminal" | "file_manager", hostId: number, hostName: string, ): Promise<{ message: string; id: number | string }> { try { const response = await homepageApi.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 homepageApi.delete("/activity/reset"); return response.data; } catch (error) { throw handleApiError(error, "reset recent activity"); } }