Files
Termix/src/ui/main-axios.ts
T

1984 lines
52 KiB
TypeScript
Raw Normal View History

2025-09-12 14:42:00 -05:00
import axios, { AxiosError, type AxiosInstance } from "axios";
import { toast } from "sonner";
+4
2026-02-12 22:28:13 -06:00
import { getBasePath } from "@/lib/base-path";
2026-05-06 15:12:07 -05:00
import { isElectron } from "@/lib/electron";
2026-05-11 01:23:11 -05:00
import { clearTermixSessionStorage } from "@/shell/TabContext";
2026-06-04 15:16:53 -04:00
import type { SSHHost } from "@/types/index";
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// 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;
+4
2026-02-12 22:28:13 -06:00
permissionLevel: "view";
+1
2025-12-31 22:20:12 -06:00
expiresAt: string | null;
createdAt: string;
}
2025-09-12 14:42:00 -05:00
import {
apiLogger,
authLogger,
sshLogger,
tunnelLogger,
fileLogger,
statsLogger,
systemLogger,
+7
2025-11-05 10:36:16 -06:00
dashboardLogger,
2025-09-12 14:42:00 -05:00
type LogContext,
2026-05-11 01:23:11 -05:00
} from "@/lib/frontend-logger";
import { dbHealthMonitor } from "@/lib/db-health-monitor";
2025-08-07 02:20:27 -05:00
export type ServerStatus = {
2025-09-12 14:42:00 -05:00
status: "online" | "offline";
lastChecked: string;
};
2026-01-24 19:49:42 -06:00
export type SSHHostWithStatus = SSHHost & {
status: "online" | "offline" | "unknown";
};
2025-08-24 01:27:41 -05:00
interface CpuMetrics {
2025-09-12 14:42:00 -05:00
percent: number | null;
cores: number | null;
load: [number, number, number] | null;
2025-08-24 01:27:41 -05:00
}
interface MemoryMetrics {
2025-09-12 14:42:00 -05:00
percent: number | null;
usedGiB: number | null;
totalGiB: number | null;
2025-08-24 01:27:41 -05:00
}
interface DiskMetrics {
2025-09-12 14:42:00 -05:00
percent: number | null;
usedHuman: string | null;
totalHuman: string | null;
2025-10-03 00:02:10 -05:00
availableHuman?: string | null;
2025-08-24 01:27:41 -05:00
}
export type ServerMetrics = {
2025-09-12 14:42:00 -05:00
cpu: CpuMetrics;
memory: MemoryMetrics;
disk: DiskMetrics;
lastChecked: string;
};
2026-06-04 15:16:53 -04:00
export interface AuthResponse {
2025-10-01 15:40:10 -05:00
success?: boolean;
is_admin?: boolean;
username?: string;
userId?: string;
is_oidc?: boolean;
totp_enabled?: boolean;
data_unlocked?: boolean;
2026-01-24 19:49:42 -06:00
requires_totp?: boolean;
temp_token?: string;
+5
2026-03-08 18:02:14 -05:00
rememberMe?: boolean;
2026-06-04 15:16:53 -04:00
token?: string;
2025-08-21 22:47:38 -05:00
}
2025-08-07 02:20:27 -05:00
2026-06-04 15:16:53 -04:00
export interface UserInfo {
2025-09-12 14:42:00 -05:00
totp_enabled: boolean;
2025-10-01 15:40:10 -05:00
userId: string;
2025-09-12 14:42:00 -05:00
username: string;
is_admin: boolean;
is_oidc: boolean;
2025-10-01 15:40:10 -05:00
data_unlocked: boolean;
2025-11-17 09:46:05 -06:00
password_hash?: string;
2025-08-21 22:47:38 -05:00
}
2025-08-07 02:20:27 -05:00
2025-08-21 22:47:38 -05:00
interface UserCount {
2025-09-12 14:42:00 -05:00
count: number;
2025-08-21 22:47:38 -05:00
}
interface OIDCAuthorize {
2025-09-12 14:42:00 -05:00
auth_url: string;
2025-08-21 22:47:38 -05:00
}
2026-05-06 15:12:07 -05:00
type ElectronApi = {
isElectron?: boolean;
getSetting?: (key: string) => Promise<string | null | undefined>;
setSetting?: (key: string, value: string) => Promise<void>;
};
type ElectronWindow = Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: ElectronApi;
ReactNativeWebView?: unknown;
};
2025-08-24 01:27:41 -05:00
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
2026-05-06 15:12:07 -05:00
export { isElectron };
2025-09-12 14:42:00 -05:00
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;
+7
2025-11-05 10:36:16 -06:00
} else if (
serviceName.includes("DASHBOARD") ||
serviceName.includes("dashboard")
) {
return dashboardLogger;
2025-09-12 14:42:00 -05:00
} else {
return apiLogger;
}
}
2025-11-17 09:46:05 -06:00
const electronSettingsCache = new Map<string, string>();
if (isElectron()) {
(async () => {
try {
2026-05-06 15:12:07 -05:00
const electronAPI = (window as ElectronWindow).electronAPI;
2025-11-17 09:46:05 -06:00
if (electronAPI?.getSetting) {
2026-05-06 15:12:07 -05:00
const settingsToLoad = ["rightClickCopyPaste"];
2025-11-17 09:46:05 -06:00
for (const key of settingsToLoad) {
const value = await electronAPI.getSetting(key);
if (value !== null && value !== undefined) {
+9
2026-04-22 16:55:23 -05:00
// 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)!);
}
2025-11-17 09:46:05 -06:00
}
}
}
} catch (error) {
console.error("[Electron] Failed to load settings cache:", error);
}
})();
}
2026-05-06 15:12:07 -05:00
export function setCookie(
name: string,
value: string,
days = 7,
): void | Promise<void> {
2025-09-12 14:42:00 -05:00
if (isElectron()) {
2025-11-17 09:46:05 -06:00
try {
2026-05-06 15:12:07 -05:00
if (name === "jwt") {
return;
}
2025-11-17 09:46:05 -06:00
2026-05-06 15:12:07 -05:00
const electronAPI = (window as ElectronWindow).electronAPI;
2025-11-17 09:46:05 -06:00
if (electronAPI?.setSetting) {
2026-05-06 15:12:07 -05:00
electronSettingsCache.set(name, value);
localStorage.setItem(name, value);
2025-11-17 09:46:05 -06:00
electronAPI.setSetting(name, value).catch((err: Error) => {
console.error(`[Electron] Failed to persist setting ${name}:`, err);
});
}
2026-05-06 15:12:07 -05:00
console.log(`[Electron] Set setting: ${name}`);
2025-11-17 09:46:05 -06:00
} catch (error) {
console.error(`[Electron] Failed to set setting: ${name}`, error);
}
2025-09-12 14:42:00 -05:00
} else {
2025-08-21 22:47:38 -05:00
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
2025-09-12 14:42:00 -05:00
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export function getCookie(name: string): string | undefined {
if (isElectron()) {
2025-11-17 09:46:05 -06:00
try {
2026-05-06 15:12:07 -05:00
if (name === "jwt") {
return undefined;
}
2025-11-17 09:46:05 -06:00
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;
}
2025-09-12 14:42:00 -05:00
} else {
2025-08-07 02:20:27 -05:00
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
2025-10-01 15:40:10 -05:00
const encodedToken =
2025-09-12 14:42:00 -05:00
parts.length === 2 ? parts.pop()?.split(";").shift() : undefined;
2025-10-01 15:40:10 -05:00
const token = encodedToken ? decodeURIComponent(encodedToken) : undefined;
2025-09-12 14:42:00 -05:00
return token;
}
2025-08-07 02:20:27 -05:00
}
2026-01-24 19:49:42 -06:00
let userWasAuthenticated = false;
2026-05-06 15:12:07 -05:00
let latestAuthSuccessAt = 0;
2026-06-04 15:16:53 -04:00
export function markUserAuthenticated(): void {
2026-05-06 15:12:07 -05:00
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<string, unknown>
| 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))
);
}
2026-01-24 19:49:42 -06:00
2025-09-12 14:42:00 -05:00
function createApiInstance(
baseURL: string,
serviceName: string = "API",
): AxiosInstance {
const instance = axios.create({
baseURL,
headers: { "Content-Type": "application/json" },
timeout: 30000,
2025-10-01 15:40:10 -05:00
withCredentials: true,
2025-09-12 14:42:00 -05:00
});
2025-08-07 02:20:27 -05:00
2026-01-24 19:49:42 -06:00
instance.interceptors.request.use((config: AxiosRequestConfig) => {
2025-09-12 14:42:00 -05:00
const startTime = performance.now();
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2026-05-06 15:12:07 -05:00
const configWithMetadata = config as AxiosRequestConfigExtended;
configWithMetadata.startTime = startTime;
configWithMetadata.requestId = requestId;
2025-09-12 14:42:00 -05:00
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);
+9
2026-04-22 16:55:23 -05:00
const isDevMode = process.env.NODE_ENV === "development";
if (isDevMode) {
2025-09-12 14:42:00 -05:00
logger.requestStart(method, fullUrl, context);
}
if (isElectron()) {
+9
2026-04-22 16:55:23 -05:00
if (config.headers.set) {
config.headers.set("X-Electron-App", "true");
} else {
config.headers["X-Electron-App"] = "true";
}
2026-05-12 21:55:14 -05:00
const jwt = localStorage.getItem("jwt");
if (jwt) {
if (config.headers.set) {
config.headers.set("Authorization", `Bearer ${jwt}`);
} else {
config.headers["Authorization"] = `Bearer ${jwt}`;
}
}
2025-09-12 14:42:00 -05:00
}
2026-05-06 15:12:07 -05:00
if (
typeof window !== "undefined" &&
(window as ElectronWindow).ReactNativeWebView
) {
+7
2025-11-05 10:36:16 -06:00
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";
}
}
+9
2026-04-22 16:55:23 -05:00
if (config.headers.set) {
config.headers.set("User-Agent", `Termix-Mobile/${platform}`);
} else {
config.headers["User-Agent"] = `Termix-Mobile/${platform}`;
}
+7
2025-11-05 10:36:16 -06:00
}
2025-09-12 14:42:00 -05:00
return config;
});
instance.interceptors.response.use(
2026-01-24 19:49:42 -06:00
(response: AxiosResponse) => {
2025-09-12 14:42:00 -05:00
const endTime = performance.now();
2026-05-06 15:12:07 -05:00
const responseConfig = response.config as AxiosRequestConfigExtended;
const startTime = responseConfig.startTime;
const requestId = responseConfig.requestId;
2026-01-24 19:49:42 -06:00
const responseTime = Math.round(endTime - (startTime || endTime));
2025-09-12 14:42:00 -05:00
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);
}
2026-01-24 19:49:42 -06:00
dbHealthMonitor.reportDatabaseSuccess();
2025-09-12 14:42:00 -05:00
return response;
},
2026-01-24 19:49:42 -06:00
(error: AxiosErrorExtended) => {
2025-09-12 14:42:00 -05:00
const endTime = performance.now();
2026-01-24 19:49:42 -06:00
const startTime = error.config?.startTime;
const requestId = error.config?.requestId;
2025-09-12 14:42:00 -05:00
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 =
2026-01-24 19:49:42 -06:00
(error.response?.data as { error?: string })?.error ||
2025-09-12 14:42:00 -05:00
(error as Error).message ||
"Unknown error";
+7
2025-11-05 10:36:16 -06:00
const errorCode =
2026-01-24 19:49:42 -06:00
(error.response?.data as { code?: string })?.code || error.code;
2025-09-12 14:42:00 -05:00
const context: LogContext = {
requestId,
method,
url: fullUrl,
status,
responseTime,
errorCode,
errorMessage: message,
operation: "request_error",
};
const logger = getLoggerForService(serviceName);
+9
2026-04-22 16:55:23 -05:00
// 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.
2026-05-06 15:12:07 -05:00
const isSilentRetry = !!error.config?.__silentRetry;
2025-09-12 14:42:00 -05:00
+9
2026-04-22 16:55:23 -05:00
if (process.env.NODE_ENV === "development" && !isSilentRetry) {
2025-09-12 14:42:00 -05:00
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,
);
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2025-09-12 14:42:00 -05:00
if (status === 401) {
+7
2025-11-05 10:36:16 -06:00
const errorCode = (error.response?.data as Record<string, unknown>)
?.code;
const errorMessage = (error.response?.data as Record<string, unknown>)
?.error;
2025-10-01 15:40:10 -05:00
const isSessionExpired = errorCode === "SESSION_EXPIRED";
2025-11-17 09:46:05 -06:00
const isSessionNotFound = errorCode === "SESSION_NOT_FOUND";
2026-05-06 15:12:07 -05:00
const isMissingAuthenticationToken =
errorMessage === "Missing authentication token";
+7
2025-11-05 10:36:16 -06:00
const isInvalidToken =
errorCode === "AUTH_REQUIRED" ||
errorMessage === "Invalid token" ||
2026-01-24 19:49:42 -06:00
errorMessage === "Authentication required" ||
2026-05-06 15:12:07 -05:00
(isMissingAuthenticationToken && userWasAuthenticated);
2025-10-01 15:40:10 -05:00
2026-05-06 15:12:07 -05:00
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);
+9
2026-04-22 16:55:23 -05:00
}
2025-11-17 09:46:05 -06:00
if (isElectron()) {
+5
2026-03-08 18:02:14 -05:00
const electronAPI = (
window as unknown as {
electronAPI?: { clearSessionCookies?: () => Promise<void> };
}
).electronAPI;
electronAPI?.clearSessionCookies?.().catch(() => {});
2025-11-17 09:46:05 -06:00
}
+7
2025-11-05 10:36:16 -06:00
2025-11-17 09:46:05 -06:00
if (typeof window !== "undefined") {
document.cookie =
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
2025-10-01 15:40:10 -05:00
2025-11-17 09:46:05 -06:00
if (isSessionExpired && typeof window !== "undefined") {
console.warn("Session expired - please log in again");
toast.warning("Session expired. Please log in again.");
2025-11-17 09:46:05 -06:00
}
2026-01-24 19:49:42 -06:00
2026-05-06 15:12:07 -05:00
dbHealthMonitor.reportSessionExpired();
2026-01-24 19:49:42 -06:00
userWasAuthenticated = false;
2025-10-01 15:40:10 -05:00
}
+9
2026-04-22 16:55:23 -05:00
} else if (!isSilentRetry) {
2026-05-06 15:12:07 -05:00
const wasAuthenticated = userWasAuthenticated;
2026-01-24 19:49:42 -06:00
dbHealthMonitor.reportDatabaseError(error, wasAuthenticated);
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2025-09-12 14:42:00 -05:00
return Promise.reject(error);
},
);
return instance;
2025-08-24 01:27:41 -05:00
}
// ============================================================================
// API INSTANCES
// ============================================================================
2025-09-12 14:42:00 -05:00
function isDev(): boolean {
2025-10-01 15:40:10 -05:00
if (isElectron()) {
return false;
}
2025-09-12 14:42:00 -05:00
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")
);
}
+7
2025-11-05 10:36:16 -06:00
const apiHost = import.meta.env.VITE_API_HOST || "localhost";
2025-09-12 14:42:00 -05:00
let configuredServerUrl: string | null = null;
+5
2026-03-08 18:02:14 -05:00
let embeddedMode = false;
2025-09-12 14:42:00 -05:00
export interface ServerConfig {
serverUrl: string;
lastUpdated: string;
2026-06-04 15:16:53 -04:00
allowInvalidCertificate?: boolean;
2025-09-12 14:42:00 -05:00
}
2026-01-24 19:49:42 -06:00
interface AxiosRequestConfigExtended extends AxiosRequestConfig {
startTime?: number;
requestId?: string;
2026-05-06 15:12:07 -05:00
__silentRetry?: boolean;
2026-01-24 19:49:42 -06:00
}
interface AxiosErrorExtended extends AxiosError {
config?: AxiosRequestConfigExtended;
}
2025-09-12 14:42:00 -05:00
export async function getServerConfig(): Promise<ServerConfig | null> {
if (!isElectron()) return null;
try {
+7
2025-11-05 10:36:16 -06:00
const result = await (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("get-server-config");
2025-09-12 14:42:00 -05:00
return result;
} catch (error) {
console.error("Failed to get server config:", error);
return null;
}
}
export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
if (!isElectron()) return false;
try {
+7
2025-11-05 10:36:16 -06:00
const result = await (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("save-server-config", config);
2025-09-12 14:42:00 -05:00
if (result?.success) {
configuredServerUrl = config.serverUrl;
+7
2025-11-05 10:36:16 -06:00
(
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
2025-09-12 14:42:00 -05:00
updateApiInstances();
return true;
}
return false;
} catch (error) {
console.error("Failed to save server config:", error);
return false;
}
}
2026-01-24 19:49:42 -06:00
export function getConfiguredServerUrl(): string | null {
return configuredServerUrl;
}
2025-09-12 14:42:00 -05:00
export async function testServerConnection(
serverUrl: string,
): Promise<{ success: boolean; error?: string }> {
if (!isElectron())
return { success: false, error: "Not in Electron environment" };
try {
+7
2025-11-05 10:36:16 -06:00
const result = await (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("test-server-connection", serverUrl);
2025-09-12 14:42:00 -05:00
return result;
} catch (error) {
console.error("Failed to test server connection:", error);
return { success: false, error: "Connection test failed" };
}
}
2025-10-01 15:40:10 -05:00
export async function checkElectronUpdate(): Promise<{
success: boolean;
2026-05-06 15:12:07 -05:00
status?: "up_to_date" | "requires_update" | "beta";
2025-10-01 15:40:10 -05:00
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 {
+7
2025-11-05 10:36:16 -06:00
const result = await (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("check-electron-update");
2025-10-01 15:40:10 -05:00
return result;
} catch (error) {
console.error("Failed to check Electron update:", error);
return { success: false, error: "Update check failed" };
}
2025-09-12 14:42:00 -05:00
}
+5
2026-03-08 18:02:14 -05:00
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<unknown>;
};
}
).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();
}
}
2025-09-12 14:42:00 -05:00
function getApiUrl(path: string, defaultPort: number): string {
+7
2025-11-05 10:36:16 -06:00
const devMode = isDev();
const electronMode = isElectron();
if (electronMode) {
+5
2026-03-08 18:02:14 -05:00
if (embeddedMode && !configuredServerUrl) {
return `http://localhost:${defaultPort}${path}`;
}
2025-09-12 14:42:00 -05:00
if (configuredServerUrl) {
const baseUrl = configuredServerUrl.replace(/\/$/, "");
+7
2025-11-05 10:36:16 -06:00
const url = `${baseUrl}${path}`;
return url;
2025-09-12 14:42:00 -05:00
}
+7
2025-11-05 10:36:16 -06:00
console.warn("Electron mode but no server configured!");
2025-09-12 14:42:00 -05:00
return "http://no-server-configured";
+7
2025-11-05 10:36:16 -06:00
} 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;
2025-09-12 14:42:00 -05:00
} else {
+4
2026-02-12 22:28:13 -06:00
return getBasePath() + path;
2025-09-12 14:42:00 -05:00
}
}
function initializeApiInstances() {
2026-03-14 20:05:05 -05:00
// Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet
hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
sshHostApi = hostApi;
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
// Tunnel Management API (port 30003)
tunnelApi = createApiInstance(getApiUrl("/ssh", 30003), "TUNNEL");
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
// File Manager Operations API (port 30004)
2025-09-12 14:42:00 -05:00
fileManagerApi = createApiInstance(
2025-10-01 15:40:10 -05:00
getApiUrl("/ssh/file_manager", 30004),
2025-09-12 14:42:00 -05:00
"FILE_MANAGER",
);
2025-10-01 15:40:10 -05:00
// Server Statistics API (port 30005)
statsApi = createApiInstance(getApiUrl("", 30005), "STATS");
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
// Authentication API (port 30001)
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
+7
2025-11-05 10:36:16 -06:00
2026-01-24 19:49:42 -06:00
// Dashboard API (port 30006)
dashboardApi = createApiInstance(getApiUrl("", 30006), "DASHBOARD");
+1
2025-12-31 22:20:12 -06:00
// RBAC API (port 30001)
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
// Docker Management API (port 30007)
dockerApi = createApiInstance(getApiUrl("/docker", 30007), "DOCKER");
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2026-03-14 20:05:05 -05:00
// Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet
export let hostApi: AxiosInstance;
// Backward compatibility
2025-09-12 14:42:00 -05:00
export let sshHostApi: AxiosInstance;
2025-08-24 01:27:41 -05:00
2025-10-01 15:40:10 -05:00
// Tunnel Management API (port 30003)
2025-09-12 14:42:00 -05:00
export let tunnelApi: AxiosInstance;
2025-08-24 01:27:41 -05:00
2025-10-01 15:40:10 -05:00
// File Manager Operations API (port 30004)
2025-09-12 14:42:00 -05:00
export let fileManagerApi: AxiosInstance;
2025-08-24 01:27:41 -05:00
2025-10-01 15:40:10 -05:00
// Server Statistics API (port 30005)
2025-09-12 14:42:00 -05:00
export let statsApi: AxiosInstance;
2025-08-24 01:27:41 -05:00
2025-10-01 15:40:10 -05:00
// Authentication API (port 30001)
2025-09-12 14:42:00 -05:00
export let authApi: AxiosInstance;
2026-01-24 19:49:42 -06:00
// Dashboard API (port 30006)
export let dashboardApi: AxiosInstance;
+7
2025-11-05 10:36:16 -06:00
+1
2025-12-31 22:20:12 -06:00
// RBAC API (port 30001)
export let rbacApi: AxiosInstance;
// Docker Management API (port 30007)
export let dockerApi: AxiosInstance;
+9
2026-04-22 16:55:23 -05:00
// Pre-initialize with default values to avoid undefined errors during early mounting
initializeApiInstances();
2026-05-28 22:29:20 -05:00
let _resolveAppReady!: () => void;
export const appReadyPromise: Promise<void> = new Promise((resolve) => {
_resolveAppReady = resolve;
});
+7
2025-11-05 10:36:16 -06:00
function initializeApp() {
if (isElectron()) {
+5
2026-03-08 18:02:14 -05:00
Promise.all([getServerConfig(), getEmbeddedServerStatus()])
.then(([config, status]) => {
2026-05-28 22:29:20 -05:00
if (status?.embedded && status?.running && !config?.serverUrl) {
+5
2026-03-08 18:02:14 -05:00
embeddedMode = true;
}
+7
2025-11-05 10:36:16 -06:00
if (config?.serverUrl) {
configuredServerUrl = config.serverUrl;
(
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
+5
2026-03-08 18:02:14 -05:00
} else if (embeddedMode) {
// Embedded backend running, no remote server needed
+7
2025-11-05 10:36:16 -06:00
} else {
console.warn("No server URL in config");
}
initializeApiInstances();
})
.catch((error) => {
console.error(
"Failed to load server config, initializing with default:",
error,
);
initializeApiInstances();
2026-05-28 22:29:20 -05:00
})
.finally(() => {
_resolveAppReady();
+7
2025-11-05 10:36:16 -06:00
});
} else {
initializeApiInstances();
2026-05-28 22:29:20 -05:00
_resolveAppReady();
+7
2025-11-05 10:36:16 -06:00
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeApp);
2025-10-01 15:40:10 -05:00
} else {
+7
2025-11-05 10:36:16 -06:00
initializeApp();
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
function updateApiInstances() {
systemLogger.info("Updating API instances with new server configuration", {
operation: "api_instance_update",
configuredServerUrl,
});
initializeApiInstances();
+7
2025-11-05 10:36:16 -06:00
(
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
2025-09-12 14:42:00 -05:00
systemLogger.success("All API instances updated successfully", {
operation: "api_instance_update_complete",
configuredServerUrl,
});
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// ERROR HANDLING
// ============================================================================
class ApiError extends Error {
2025-09-12 14:42:00 -05:00
constructor(
message: string,
public status?: number,
public code?: string,
) {
super(message);
this.name = "ApiError";
}
2025-08-24 01:27:41 -05:00
}
2026-06-04 15:16:53 -04:00
export function handleApiError(error: unknown, operation: string): never {
2025-09-12 14:42:00 -05:00
const context: LogContext = {
operation: "error_handling",
errorOperation: operation,
};
if (axios.isAxiosError(error)) {
const status = error.response?.status;
+7
2025-11-05 10:36:16 -06:00
const message =
error.response?.data?.message ||
error.response?.data?.error ||
error.message;
const code = error.response?.data?.code || error.response?.data?.error;
2025-09-12 14:42:00 -05:00
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,
);
2025-10-01 15:40:10 -05:00
const isLoginEndpoint = url?.includes("/users/login");
const errorMessage = isLoginEndpoint
? message
: "Authentication required. Please log in again.";
2026-05-06 15:12:07 -05:00
throw new ApiError(errorMessage, 401, code || "AUTH_REQUIRED");
2025-09-12 14:42:00 -05:00
} else if (status === 403) {
authLogger.warn(`Access denied: ${method} ${url}`, errorContext);
+7
2025-11-05 10:36:16 -06:00
const apiError = new ApiError(
code === "TOTP_REQUIRED"
? message
: "Access denied. You do not have permission to perform this action.",
2025-09-12 14:42:00 -05:00
403,
+7
2025-11-05 10:36:16 -06:00
code || "ACCESS_DENIED",
2025-09-12 14:42:00 -05:00
);
+7
2025-11-05 10:36:16 -06:00
(apiError as ApiError & { response?: unknown }).response = error.response;
throw apiError;
2025-09-12 14:42:00 -05:00
} 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);
2025-08-24 01:27:41 -05:00
}
2025-09-12 14:42:00 -05:00
}
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",
);
2025-08-24 01:27:41 -05:00
}
// ============================================================================
2026-06-04 15:16:53 -04:00
// ============================================================================
// HOST-TO-HOST TRANSFER
2025-08-24 01:27:41 -05:00
// ============================================================================
2025-08-07 02:20:27 -05:00
2026-06-04 15:16:53 -04:00
export type TransferMethodPreference = "auto" | "tar" | "item_sftp";
2026-01-24 19:49:42 -06:00
2026-06-04 15:16:53 -04:00
export interface TransferScanSummary {
fileCount: number;
totalBytes: number;
largestFileBytes: number;
incompressibleRatio: number;
2025-08-07 02:20:27 -05:00
}
2026-06-04 15:16:53 -04:00
export interface TransferMethodPreview {
methodPreference: TransferMethodPreference;
resolvedMethod: "tar" | "item_sftp";
reasonKey: string;
sourcePlatform: "unix" | "windows";
destPlatform: "unix" | "windows";
sourceHasTar: boolean;
destHasTar: boolean;
summary: TransferScanSummary;
2025-08-07 02:20:27 -05:00
}
2026-06-04 15:16:53 -04:00
export async function getTransferMethodPreview(
sourceSessionId: string,
sourcePaths: string[],
destSessionId: string,
destPath: string,
methodPreference?: TransferMethodPreference,
): Promise<TransferMethodPreview> {
2025-09-12 14:42:00 -05:00
try {
2026-06-04 15:16:53 -04:00
const response = await fileManagerApi.post("/ssh/transferMethodPreview", {
sourceSessionId,
sourcePaths,
destSessionId,
destPath,
methodPreference: methodPreference ?? "auto",
+5
2026-03-08 18:02:14 -05:00
});
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "preview transfer method");
throw error;
2025-09-12 14:42:00 -05:00
}
}
2026-06-04 15:16:53 -04:00
export interface TransferHopMetrics {
id: string;
mbPerSec?: number;
+5
2026-03-08 18:02:14 -05:00
}
2026-06-04 15:16:53 -04:00
export interface TransferTimings {
prepareDestMs?: number;
compressMs?: number;
transferMs?: number;
extractMs?: number;
sourceDeleteMs?: number;
totalMs?: number;
transferBytes?: number;
endToEndMbPerSec?: number;
hops?: TransferHopMetrics[];
2025-08-07 02:20:27 -05:00
}
2026-06-04 15:16:53 -04:00
export function getTransferProgressPercent(
status: TransferProgressResponse,
): number | undefined {
if (
status.bytesTransferred !== undefined &&
status.totalBytes !== undefined &&
status.totalBytes > 0
) {
return Math.min(
100,
Math.round((status.bytesTransferred / status.totalBytes) * 100),
);
2025-09-12 14:42:00 -05:00
}
2026-06-04 15:16:53 -04:00
if (
status.itemsCompleted !== undefined &&
status.totalItems !== undefined &&
status.totalItems > 0
) {
return Math.min(
100,
Math.round((status.itemsCompleted / status.totalItems) * 100),
);
}
return undefined;
2025-08-07 02:20:27 -05:00
}
2026-06-04 15:16:53 -04:00
export function formatTransferMbPerSec(
mbPerSec?: number,
_bytes?: number,
_ms?: number,
): string {
if (mbPerSec === undefined || mbPerSec <= 0) return "";
if (mbPerSec < 1) return `${(mbPerSec * 1024).toFixed(0)} KB/s`;
return `${mbPerSec.toFixed(1)} MB/s`;
2025-10-03 00:02:10 -05:00
}
2026-06-04 15:16:53 -04:00
export function formatDurationMs(ms?: number): string {
if (ms === undefined) return "";
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.round((ms % 60000) / 1000);
return `${minutes}m ${seconds}s`;
+9
2026-04-22 16:55:23 -05:00
}
2026-06-04 15:16:53 -04:00
interface TransferProgressTracker {
update(status: TransferProgressResponse): { rate: number | undefined; stalled: boolean };
2025-10-01 15:40:10 -05:00
}
2026-06-04 15:16:53 -04:00
export function createTransferProgressTracker(): TransferProgressTracker {
let lastBytes: number | undefined;
let lastTime: number | undefined;
let lastRate: number | undefined;
let lastActivityTime: number | undefined;
const STALL_THRESHOLD_MS = 5000;
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
return {
update(status) {
const now = Date.now();
const bytes = status.bytesTransferred;
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
if (bytes !== undefined && lastBytes !== undefined && lastTime !== undefined) {
const deltaBytes = bytes - lastBytes;
const deltaMs = now - lastTime;
if (deltaMs > 0 && deltaBytes >= 0) {
lastRate = (deltaBytes / deltaMs / 1024 / 1024) * 1000;
if (deltaBytes > 0) lastActivityTime = now;
}
} else if (bytes !== undefined) {
lastActivityTime = now;
}
+5
2026-03-08 18:02:14 -05:00
2026-06-04 15:16:53 -04:00
lastBytes = bytes;
lastTime = now;
const stalled =
lastActivityTime !== undefined &&
now - lastActivityTime > STALL_THRESHOLD_MS &&
status.status === "running" &&
status.phase === "transferring";
return { rate: lastRate, stalled };
},
+5
2026-03-08 18:02:14 -05:00
};
2026-06-04 15:16:53 -04:00
}
export interface TransferProgressResponse {
transferId: string;
status: "running" | "success" | "partial" | "error" | "cancelled";
phase: "compressing" | "transferring" | "extracting" | "reconnecting";
bytesTransferred?: number;
totalBytes?: number;
itemsCompleted?: number;
totalItems?: number;
failedPaths?: string[];
message?: string;
method?: "stream" | "tar" | "item_sftp";
sourcePaths?: string[];
destPath?: string;
sourceSessionId?: string;
destSessionId?: string;
startedAt?: number;
timings?: TransferTimings;
sourceDeleted?: boolean;
moveRequested?: boolean;
partialDestRemaining?: boolean;
cleanupCompleted?: boolean;
retryable?: boolean;
parallelSegmentCount?: number;
}
export async function transferToHost(
sourceSessionId: string,
sourcePaths: string[],
destSessionId: string,
destPath: string,
move?: boolean,
methodPreference?: TransferMethodPreference,
parallelSegmentCount = 2,
): Promise<{ transferId: string }> {
+5
2026-03-08 18:02:14 -05:00
try {
2026-06-04 15:16:53 -04:00
fileLogger.info("Starting host transfer", {
operation: "host_transfer",
sourceSessionId,
destSessionId,
sourcePaths,
destPath,
move,
methodPreference,
parallelSegmentCount,
});
const response = await fileManagerApi.post("/ssh/transferToHost", {
sourceSessionId,
sourcePaths,
destSessionId,
destPath,
move,
methodPreference: methodPreference ?? "auto",
parallelSegmentCount,
});
+5
2026-03-08 18:02:14 -05:00
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
fileLogger.error("Failed to start host transfer", error, {
operation: "host_transfer",
sourceSessionId,
destSessionId,
sourcePaths,
});
handleApiError(error, "transfer to host");
throw error;
+5
2026-03-08 18:02:14 -05:00
}
}
2026-06-04 15:16:53 -04:00
export async function getTransferStatus(
transferId: string,
): Promise<TransferProgressResponse> {
2025-09-12 14:42:00 -05:00
try {
2026-06-04 15:16:53 -04:00
const response = await fileManagerApi.get(
`/ssh/transferStatus/${transferId}`,
2025-09-12 14:42:00 -05:00
);
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "get transfer status");
throw error;
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2026-06-04 15:16:53 -04:00
export async function listActiveTransfers(): Promise<{
transfers: TransferProgressResponse[];
}> {
2025-09-12 14:42:00 -05:00
try {
2026-06-04 15:16:53 -04:00
const response = await fileManagerApi.get("/ssh/activeTransfers");
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "list active transfers");
throw error;
2025-09-12 14:42:00 -05:00
}
}
2026-06-04 15:16:53 -04:00
export async function cancelTransferToHost(transferId: string): Promise<void> {
2025-10-01 15:40:10 -05:00
try {
2026-06-04 15:16:53 -04:00
await fileManagerApi.post(`/ssh/transferCancel/${transferId}`);
2025-10-01 15:40:10 -05:00
} catch (error) {
2026-06-04 15:16:53 -04:00
fileLogger.warn("Transfer cancel request failed (non-fatal)", {
operation: "host_transfer",
transferId,
error,
});
2025-10-01 15:40:10 -05:00
}
}
2026-06-04 15:16:53 -04:00
export async function cleanupCancelledTransfer(
transferId: string,
): Promise<{ removedPaths: string[]; failedPaths: string[] }> {
2025-10-01 15:40:10 -05:00
try {
const response = await fileManagerApi.post(
2026-06-04 15:16:53 -04:00
`/ssh/transferCleanup/${transferId}`,
2025-10-01 15:40:10 -05:00
);
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "clean up cancelled transfer");
2025-10-01 15:40:10 -05:00
throw error;
}
}
2026-06-04 15:16:53 -04:00
export async function retryTransferToHost(
transferId: string,
): Promise<{ ok: boolean; transferId: string }> {
2025-09-12 14:42:00 -05:00
try {
2026-06-04 15:16:53 -04:00
const response = await fileManagerApi.post(
`/ssh/transferRetry/${transferId}`,
2025-10-01 15:40:10 -05:00
);
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "retry transfer");
2025-10-01 15:40:10 -05:00
throw error;
}
}
2026-06-04 15:16:53 -04:00
export async function pollTransferUntilComplete(
transferId: string,
onProgress?: (status: TransferProgressResponse) => void,
intervalMs = 500,
): Promise<TransferProgressResponse> {
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const status = await getTransferStatus(transferId);
onProgress?.(status);
2025-11-17 09:46:05 -06:00
2026-06-04 15:16:53 -04:00
if (
status.status === "success" ||
status.status === "partial" ||
status.status === "error" ||
status.status === "cancelled"
) {
resolve(status);
return;
}
2025-11-17 09:46:05 -06:00
2026-06-04 15:16:53 -04:00
setTimeout(poll, intervalMs);
} catch (err) {
reject(err);
}
};
2025-11-17 09:46:05 -06:00
2026-06-04 15:16:53 -04:00
void poll();
});
2025-11-17 09:46:05 -06:00
}
2025-10-01 15:40:10 -05:00
// ============================================================================
// FILE MANAGER DATA
// ============================================================================
2026-06-04 15:16:53 -04:00
export interface TransferDestination {
id: number;
userId: string;
sourceHostId: number;
destHostId: number;
destPath: string;
destPathLabel?: string;
lastUsed?: string;
}
export async function getTransferRecent(
sourceHostId: number,
): Promise<TransferDestination[]> {
try {
const response = await authApi.get("/host/transfer/recent", {
params: { sourceHostId },
});
return response.data;
} catch (error) {
handleApiError(error, "get transfer recent destinations");
throw error;
}
}
export async function addTransferRecent(
sourceHostId: number,
destHostId: number,
destPath: string,
destPathLabel?: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
2026-06-04 15:16:53 -04:00
const response = await authApi.post("/host/transfer/recent", {
sourceHostId,
destHostId,
destPath,
destPathLabel,
2025-10-01 15:40:10 -05:00
});
return response.data;
} catch (error) {
2026-06-04 15:16:53 -04:00
handleApiError(error, "add transfer recent destination");
2025-10-01 15:40:10 -05:00
throw error;
}
}
2026-06-04 15:16:53 -04:00
export {
getSSHHosts,
createSSHHost,
updateSSHHost,
wakeOnLan,
bulkImportSSHHosts,
bulkUpdateSSHHosts,
deleteSSHHost,
getSSHHostById,
exportSSHHostWithCredentials,
exportAllSSHHosts,
enableAutoStart,
disableAutoStart,
getAutoStartStatus,
testProxyConnection,
} from "@/api/ssh-host-management-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getTunnelStatuses,
subscribeTunnelStatuses,
getTunnelStatusByName,
connectTunnel,
disconnectTunnel,
cancelTunnel,
getC2STunnelPresets,
createC2STunnelPreset,
updateC2STunnelPreset,
deleteC2STunnelPreset,
} from "@/api/tunnel-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getFileManagerRecent,
addFileManagerRecent,
removeFileManagerRecent,
getFileManagerPinned,
addFileManagerPinned,
removeFileManagerPinned,
getFileManagerShortcuts,
addFileManagerShortcut,
removeFileManagerShortcut,
} from "@/api/file-manager-metadata-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
connectSSH,
disconnectSSH,
verifySSHTOTP,
verifySSHWarpgate,
quickConnect,
getSSHStatus,
keepSSHAlive,
listSSHFiles,
identifySSHSymlink,
resolveSSHPath,
readSSHFile,
writeSSHFile,
uploadSSHFile,
downloadSSHFile,
downloadSSHFileStream,
createSSHFile,
createSSHFolder,
deleteSSHItem,
setSudoPassword,
copySSHItem,
renameSSHItem,
moveSSHItem,
changeSSHPermissions,
extractSSHArchive,
compressSSHFiles,
ensureSSHSessionForHost,
browseSSHDirectory,
type HostConnectionState,
type EnsureSSHSessionResult,
type BrowseSSHDirectoryResult,
} from "@/api/ssh-file-operations-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getRecentFiles,
addRecentFile,
removeRecentFile,
getPinnedFiles,
addPinnedFile,
removePinnedFile,
getFolderShortcuts,
addFolderShortcut,
removeFolderShortcut,
} from "@/api/file-manager-data-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getAllServerStatuses,
getServerStatusById,
getServerMetricsById,
startMetricsPolling,
stopMetricsPolling,
sendMetricsHeartbeat,
registerMetricsViewer,
unregisterMetricsViewer,
submitMetricsTOTP,
refreshServerPolling,
notifyHostCreatedOrUpdated,
} from "@/api/server-stats-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getGlobalMonitoringSettings,
updateGlobalMonitoringSettings,
getLogLevel,
updateLogLevel,
getSessionTimeout,
updateSessionTimeout,
getGuacamoleSettings,
updateGuacamoleSettings,
} from "@/api/settings-api";
2026-03-14 20:05:05 -05:00
2025-08-24 01:27:41 -05:00
// ============================================================================
// AUTHENTICATION
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function registerUser(
username: string,
password: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/create", {
username,
password,
});
return response.data;
} catch (error) {
handleApiError(error, "register user");
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export async function loginUser(
username: string,
password: string,
+5
2026-03-08 18:02:14 -05:00
rememberMe: boolean = false,
2025-09-12 14:42:00 -05:00
): Promise<AuthResponse> {
try {
+5
2026-03-08 18:02:14 -05:00
const response = await authApi.post("/users/login", {
username,
password,
rememberMe,
});
2025-10-01 15:40:10 -05:00
+7
2025-11-05 10:36:16 -06:00
const isInIframe =
typeof window !== "undefined" && window.self !== window.top;
2026-05-06 15:12:07 -05:00
if (isInIframe && isElectron() && response.data.success) {
+7
2025-11-05 10:36:16 -06:00
try {
window.parent.postMessage(
{
type: "AUTH_SUCCESS",
source: "login_api",
platform: "desktop",
timestamp: Date.now(),
},
+9
2026-04-22 16:55:23 -05:00
window.location.origin,
+7
2025-11-05 10:36:16 -06:00
);
} catch (e) {
console.error("[main-axios] Error posting message to parent:", e);
}
}
2026-05-28 22:29:20 -05:00
if (response.data.token) {
localStorage.setItem("jwt", response.data.token);
}
2026-05-06 15:12:07 -05:00
if (response.data.success && !response.data.requires_totp) {
markUserAuthenticated();
}
2025-10-01 15:40:10 -05:00
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,
+5
2026-03-08 18:02:14 -05:00
rememberMe: response.data.rememberMe,
2026-01-24 19:49:42 -06:00
is_oidc: response.data.is_oidc,
totp_enabled: response.data.totp_enabled,
data_unlocked: response.data.data_unlocked,
2026-06-04 15:16:53 -04:00
token: response.data.token,
2025-10-01 15:40:10 -05:00
};
2025-09-12 14:42:00 -05:00
} catch (error) {
2026-01-24 19:49:42 -06:00
throw handleApiError(error, "login user");
2025-09-12 14:42:00 -05:00
}
2025-08-21 22:47:38 -05:00
}
2025-10-01 15:40:10 -05:00
export async function logoutUser(): Promise<{
success: boolean;
message: string;
}> {
try {
const response = await authApi.post("/users/logout");
+5
2026-03-08 18:02:14 -05:00
clearTermixSessionStorage();
if (isElectron()) {
+5
2026-03-08 18:02:14 -05:00
const electronAPI = (
window as unknown as {
electronAPI?: { clearSessionCookies?: () => Promise<void> };
}
).electronAPI;
electronAPI?.clearSessionCookies?.().catch(() => {});
} else {
const isSecure = window.location.protocol === "https:";
const cookieString = isSecure
2026-05-06 15:12:07 -05:00
? "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;
}
2025-10-01 15:40:10 -05:00
return response.data;
} catch (error) {
+5
2026-03-08 18:02:14 -05:00
clearTermixSessionStorage();
if (isElectron()) {
+5
2026-03-08 18:02:14 -05:00
const electronAPI = (
window as unknown as {
electronAPI?: { clearSessionCookies?: () => Promise<void> };
}
).electronAPI;
electronAPI?.clearSessionCookies?.().catch(() => {});
} else {
const isSecure = window.location.protocol === "https:";
const cookieString = isSecure
2026-05-06 15:12:07 -05:00
? "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;
}
2025-10-01 15:40:10 -05:00
handleApiError(error, "logout user");
}
}
2025-08-21 22:47:38 -05:00
export async function getUserInfo(): Promise<UserInfo> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/me");
2026-05-06 15:12:07 -05:00
markUserAuthenticated();
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "fetch user info");
}
2025-08-21 22:47:38 -05:00
}
2026-05-12 21:55:14 -05:00
export async function getCurrentToken(): Promise<string | null> {
try {
const response = await authApi.get("/users/me/token");
return response.data?.token ?? null;
} catch {
return null;
}
}
2025-10-01 15:40:10 -05:00
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");
}
}
2025-08-21 22:47:38 -05:00
export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/registration-allowed");
return response.data;
} catch (error) {
handleApiError(error, "check registration status");
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
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<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/oidc-config");
return response.data;
+7
2025-11-05 10:36:16 -06:00
} catch (error: unknown) {
2025-09-12 14:42:00 -05:00
console.warn(
"Failed to fetch OIDC config:",
error.response?.data?.error || error.message,
);
return null;
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function getAdminOIDCConfig(): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/users/oidc-config/admin");
return response.data;
} catch (error) {
handleApiError(error, "fetch admin OIDC config");
}
}
2025-10-01 15:40:10 -05:00
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");
}
}
2025-08-21 22:47:38 -05:00
export async function getUserCount(): Promise<UserCount> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/count");
return response.data;
} catch (error) {
handleApiError(error, "fetch user count");
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function initiatePasswordReset(
username: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/initiate-reset", { username });
return response.data;
} catch (error) {
handleApiError(error, "initiate password reset");
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export async function verifyPasswordResetCode(
username: string,
resetCode: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/verify-reset-code", {
username,
resetCode,
});
return response.data;
} catch (error) {
handleApiError(error, "verify reset code");
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export async function completePasswordReset(
username: string,
tempToken: string,
newPassword: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/complete-reset", {
username,
tempToken,
newPassword,
});
return response.data;
} catch (error) {
handleApiError(error, "complete password reset");
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
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");
}
}
2026-03-14 20:05:05 -05:00
export async function getOIDCAuthorizeUrl(
rememberMe = false,
2026-05-28 22:29:20 -05:00
desktopCallbackPort?: number,
2026-03-14 20:05:05 -05:00
): Promise<OIDCAuthorize> {
2025-09-12 14:42:00 -05:00
try {
2026-03-14 20:05:05 -05:00
const response = await authApi.get("/users/oidc/authorize", {
2026-05-28 22:29:20 -05:00
params: { rememberMe, desktopCallbackPort },
2026-03-14 20:05:05 -05:00
});
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "get OIDC authorize URL");
}
}
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getUserList,
getSessions,
revokeSession,
revokeAllUserSessions,
createApiKey,
getApiKeys,
deleteApiKey,
makeUserAdmin,
removeAdminStatus,
deleteUser,
deleteAccount,
updateRegistrationAllowed,
getOidcAutoProvision,
updateOidcAutoProvision,
updatePasswordLoginAllowed,
getPasswordResetAllowed,
updatePasswordResetAllowed,
updateOIDCConfig,
disableOIDCConfig,
type ApiKey,
type CreatedApiKey,
} from "@/api/user-management-api";
2026-06-04 15:16:53 -04:00
export {
setupTOTP,
enableTOTP,
disableTOTP,
verifyTOTPLogin,
generateBackupCodes,
getUserAlerts,
dismissAlert,
getReleasesRSS,
getVersionInfo,
getDatabaseHealth,
} from "@/api/system-status-api";
2025-09-12 14:42:00 -05:00
// SSH CREDENTIALS MANAGEMENT
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getCredentials,
getCredentialDetails,
createCredential,
updateCredential,
deleteCredential,
getCredentialHosts,
getCredentialFolders,
getSSHHostWithCredentials,
getHostPassword,
applyCredentialToHost,
removeCredentialFromHost,
migrateHostToCredential,
getFoldersWithStats,
renameFolder,
getSSHFolders,
updateFolderMetadata,
deleteAllHostsInFolder,
renameCredentialFolder,
detectKeyType,
detectPublicKeyType,
validateKeyPair,
generatePublicKeyFromPrivate,
generateKeyPair,
deployCredentialToHost,
} from "@/api/credentials-api";
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
// SNIPPETS API
// ============================================================================
2026-06-04 15:16:53 -04:00
export type {
NetworkTopologyNode,
NetworkTopologyEdge,
NetworkTopologyData,
} from "@/api/snippets-api";
export {
getSnippets,
createSnippet,
updateSnippet,
deleteSnippet,
executeSnippet,
getNetworkTopology,
saveNetworkTopology,
getSnippetFolders,
createSnippetFolder,
updateSnippetFolderMetadata,
renameSnippetFolder,
deleteSnippetFolder,
reorderSnippets,
} from "@/api/snippets-api";
2025-11-17 09:46:05 -06:00
2026-01-24 19:49:42 -06:00
// ============================================================================
2026-06-04 15:16:53 -04:00
export type { UptimeInfo, RecentActivityItem } from "@/api/dashboard-api";
export {
getUptime,
getRecentActivity,
logActivity,
resetRecentActivity,
} from "@/api/dashboard-api";
2026-01-24 19:49:42 -06:00
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
saveCommandToHistory,
getCommandHistory,
deleteCommandFromHistory,
clearCommandHistory,
} from "@/api/command-history-api";
+7
2025-11-05 10:36:16 -06:00
2026-06-04 15:16:53 -04:00
export {
linkOIDCToPasswordAccount,
unlinkOIDCFromPasswordAccount,
} from "@/api/oidc-account-api";
+7
2025-11-05 10:36:16 -06:00
2026-06-04 15:16:53 -04:00
export type {
GuacamoleTokenRequest,
GuacamoleTokenResponse,
} from "@/api/guacamole-api";
export {
getGuacamoleDpi,
getGuacamoleToken,
getGuacamoleTokenFromHost,
getGuacdStatus,
} from "@/api/guacamole-api";
2026-05-28 22:29:20 -05:00
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// RBAC MANAGEMENT
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getRoles,
createRole,
updateRole,
deleteRole,
getUserRoles,
assignRoleToUser,
removeRoleFromUser,
shareHost,
getHostAccess,
revokeHostAccess,
shareSnippet,
getSnippetAccess,
revokeSnippetAccess,
getSharedSnippets,
} from "@/api/rbac-api";
+9
2026-04-22 16:55:23 -05:00
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// DOCKER MANAGEMENT API
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
connectDockerSession,
verifyDockerTOTP,
verifyDockerWarpgate,
disconnectDockerSession,
keepaliveDockerSession,
getDockerSessionStatus,
validateDockerAvailability,
listDockerContainers,
getDockerContainerDetails,
startDockerContainer,
stopDockerContainer,
restartDockerContainer,
pauseDockerContainer,
unpauseDockerContainer,
removeDockerContainer,
getContainerLogs,
downloadContainerLogs,
getContainerStats,
} from "@/api/docker-api";
+1
2025-12-31 22:20:12 -06:00
2026-06-04 15:16:53 -04:00
export {
getDashboardPreferences,
saveDashboardPreferences,
type DashboardLayout,
} from "@/api/dashboard-preferences-api";
+1
2025-12-31 22:20:12 -06:00
2026-06-04 15:16:53 -04:00
export {
getOpenTabs,
syncOpenTabs,
deleteOpenTab,
patchOpenTab,
addOpenTab,
getActiveSessions,
getUserPreferences,
saveUserPreferences,
type OpenTabRecord,
type OpenTabSyncPayload,
type OpenTabUpsertPayload,
type ActiveSessionInfo,
type UserPreferences,
} from "@/api/open-tabs-api";