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

4045 lines
104 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";
2025-09-12 14:42:00 -05:00
import type {
SSHHost,
SSHHostData,
2025-11-17 09:46:05 -06:00
SSHFolder,
2025-09-12 14:42:00 -05:00
TunnelConfig,
TunnelStatus,
FileManagerFile,
FileManagerShortcut,
+1
2025-12-31 22:20:12 -06:00
DockerContainer,
DockerStats,
DockerLogOptions,
DockerValidation,
2025-09-12 14:42:00 -05:00
} from "../types/index.js";
+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,
} from "../lib/frontend-logger.js";
2026-01-24 19:49:42 -06:00
import { dbHealthMonitor } from "../lib/db-health-monitor.js";
2025-08-07 02:20:27 -05:00
2025-08-24 01:27:41 -05:00
interface FileManagerOperation {
2025-09-12 14:42:00 -05:00
name: string;
path: string;
isSSH: boolean;
sshSessionId?: string;
hostId: number;
2025-08-24 01:27:41 -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;
};
2025-08-21 22:47:38 -05:00
interface AuthResponse {
2025-09-12 14:42:00 -05:00
token: string;
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;
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 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
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
2025-09-12 14:42:00 -05:00
export function isElectron(): boolean {
+7
2025-11-05 10:36:16 -06:00
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;
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 {
const electronAPI = (
window as Window &
typeof globalThis & {
electronAPI?: any;
}
).electronAPI;
if (electronAPI?.getSetting) {
const settingsToLoad = ["rightClickCopyPaste", "jwt"];
for (const key of settingsToLoad) {
const value = await electronAPI.getSetting(key);
if (value !== null && value !== undefined) {
electronSettingsCache.set(key, value);
localStorage.setItem(key, value);
}
}
}
} catch (error) {
console.error("[Electron] Failed to load settings cache:", error);
}
})();
}
2025-09-12 14:42:00 -05:00
export function setCookie(name: string, value: string, days = 7): void {
if (isElectron()) {
2025-11-17 09:46:05 -06:00
try {
electronSettingsCache.set(name, value);
localStorage.setItem(name, value);
const electronAPI = (
window as Window &
typeof globalThis & {
electronAPI?: any;
}
).electronAPI;
if (electronAPI?.setSetting) {
electronAPI.setSetting(name, value).catch((err: Error) => {
console.error(`[Electron] Failed to persist setting ${name}:`, err);
});
}
console.log(`[Electron] Set setting: ${name} = ${value}`);
} 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 {
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;
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-01-24 19:49:42 -06:00
(config as any).startTime = startTime;
(config as any).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);
if (process.env.NODE_ENV === "development") {
logger.requestStart(method, fullUrl, context);
}
if (isElectron()) {
config.headers["X-Electron-App"] = "true";
2025-10-01 15:40:10 -05:00
const token = localStorage.getItem("jwt");
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
2026-01-24 19:49:42 -06:00
userWasAuthenticated = true;
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
}
+7
2025-11-05 10:36:16 -06:00
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}`;
}
2026-01-24 19:49:42 -06:00
if (!isElectron()) {
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("jwt="));
if (token) {
userWasAuthenticated = true;
}
}
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-01-24 19:49:42 -06:00
const startTime = (response.config as any).startTime;
const requestId = (response.config as any).requestId;
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);
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,
);
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";
+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" ||
errorMessage === "Missing authentication token";
2025-10-01 15:40:10 -05:00
2025-11-17 09:46:05 -06:00
if (isSessionExpired || isSessionNotFound || isInvalidToken) {
2026-01-24 19:49:42 -06:00
const wasAuthenticated = userWasAuthenticated;
2025-09-12 14:42:00 -05:00
localStorage.removeItem("jwt");
2025-10-01 15:40:10 -05:00
2025-11-17 09:46:05 -06:00
if (isElectron()) {
electronSettingsCache.delete("jwt");
}
+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
+4
2026-02-12 22:28:13 -06:00
if (wasAuthenticated) {
dbHealthMonitor.reportSessionExpired();
}
2026-01-24 19:49:42 -06:00
userWasAuthenticated = false;
2025-10-01 15:40:10 -05:00
}
2026-01-24 19:49:42 -06:00
} else {
const wasAuthenticated = !!localStorage.getItem("jwt");
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;
export interface ServerConfig {
serverUrl: string;
lastUpdated: string;
}
2026-01-24 19:49:42 -06:00
interface AxiosRequestConfigExtended extends AxiosRequestConfig {
startTime?: number;
requestId?: string;
}
interface AxiosResponseExtended extends AxiosResponse {
config: AxiosRequestConfigExtended;
}
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;
}
interface AxiosRequestConfigExtended extends AxiosRequestConfig {
startTime?: number;
requestId?: string;
}
interface AxiosResponseExtended extends AxiosResponse {
config: AxiosRequestConfigExtended;
}
interface AxiosErrorExtended extends AxiosError {
config?: AxiosRequestConfigExtended;
}
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;
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 {
+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
}
function getApiUrl(path: string, defaultPort: number): string {
+7
2025-11-05 10:36:16 -06:00
const devMode = isDev();
const electronMode = isElectron();
if (electronMode) {
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() {
2025-10-01 15:40:10 -05:00
// SSH Host Management API (port 30001)
sshHostApi = createApiInstance(getApiUrl("/ssh", 30001), "SSH_HOST");
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
2025-10-01 15:40:10 -05:00
// SSH Host Management API (port 30001)
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;
+7
2025-11-05 10:36:16 -06:00
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);
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
}
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.";
throw new ApiError(errorMessage, 401, "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
}
// ============================================================================
// SSH HOST MANAGEMENT
// ============================================================================
2025-08-07 02:20:27 -05:00
2026-01-24 19:49:42 -06:00
export async function getSSHHosts(): Promise<SSHHostWithStatus[]> {
2025-09-12 14:42:00 -05:00
try {
2026-01-24 19:49:42 -06:00
const hostsResponse = await sshHostApi.get("/db/host");
const hosts: SSHHost[] = hostsResponse.data;
const statusesResponse = await getAllServerStatuses();
const statuses = statusesResponse || {};
return hosts.map((host) => ({
...host,
status: statuses[host.id]?.status || "unknown",
}));
2025-09-12 14:42:00 -05:00
} catch (error) {
2026-01-24 19:49:42 -06:00
throw handleApiError(error, "fetch SSH hosts");
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
2025-09-12 14:42:00 -05:00
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,
2025-11-17 09:46:05 -06:00
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
2025-09-12 14:42:00 -05:00
enableTerminal: Boolean(hostData.enableTerminal),
enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager),
+1
2025-12-31 22:20:12 -06:00
enableDocker: Boolean(hostData.enableDocker),
2026-01-24 19:49:42 -06:00
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
2025-09-12 14:42:00 -05:00
defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [],
2025-11-17 09:46:05 -06:00
jumpHosts: hostData.jumpHosts || [],
quickActions: hostData.quickActions || [],
+7
2025-11-05 10:36:16 -06:00
statsConfig: hostData.statsConfig
? typeof hostData.statsConfig === "string"
? hostData.statsConfig
: JSON.stringify(hostData.statsConfig)
: null,
terminalConfig: hostData.terminalConfig || null,
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
+1
2025-12-31 22:20:12 -06:00
notes: hostData.notes || "",
useSocks5: Boolean(hostData.useSocks5),
socks5Host: hostData.socks5Host || null,
socks5Port: hostData.socks5Port || null,
socks5Username: hostData.socks5Username || null,
socks5Password: hostData.socks5Password || null,
socks5ProxyChain: hostData.socks5ProxyChain || null,
2025-09-12 14:42:00 -05:00
};
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if (!submitData.enableTunnel) {
submitData.tunnelConnections = [];
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
if (!submitData.enableFileManager) {
submitData.defaultPath = "";
}
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
const dataWithoutFile = { ...submitData };
delete dataWithoutFile.key;
formData.append("data", JSON.stringify(dataWithoutFile));
const response = await sshHostApi.post("/db/host", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
} else {
const response = await sshHostApi.post("/db/host", submitData);
return response.data;
}
} catch (error) {
throw handleApiError(error, "create SSH host");
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function updateSSHHost(
hostId: number,
hostData: SSHHostData,
): Promise<SSHHost> {
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,
2025-11-17 09:46:05 -06:00
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
2025-09-12 14:42:00 -05:00
enableTerminal: Boolean(hostData.enableTerminal),
enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager),
+1
2025-12-31 22:20:12 -06:00
enableDocker: Boolean(hostData.enableDocker),
2026-01-24 19:49:42 -06:00
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
2025-09-12 14:42:00 -05:00
defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [],
2025-11-17 09:46:05 -06:00
jumpHosts: hostData.jumpHosts || [],
quickActions: hostData.quickActions || [],
+7
2025-11-05 10:36:16 -06:00
statsConfig: hostData.statsConfig
? typeof hostData.statsConfig === "string"
? hostData.statsConfig
: JSON.stringify(hostData.statsConfig)
: null,
terminalConfig: hostData.terminalConfig || null,
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
+1
2025-12-31 22:20:12 -06:00
notes: hostData.notes || "",
useSocks5: Boolean(hostData.useSocks5),
socks5Host: hostData.socks5Host || null,
socks5Port: hostData.socks5Port || null,
socks5Username: hostData.socks5Username || null,
socks5Password: hostData.socks5Password || null,
socks5ProxyChain: hostData.socks5ProxyChain || null,
2025-09-12 14:42:00 -05:00
};
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if (!submitData.enableTunnel) {
submitData.tunnelConnections = [];
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
if (!submitData.enableFileManager) {
submitData.defaultPath = "";
}
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
const dataWithoutFile = { ...submitData };
delete dataWithoutFile.key;
formData.append("data", JSON.stringify(dataWithoutFile));
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
} else {
const response = await sshHostApi.put(`/db/host/${hostId}`, submitData);
return response.data;
}
} catch (error) {
throw handleApiError(error, "update SSH host");
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{
2025-09-12 14:42:00 -05:00
message: string;
success: number;
failed: number;
errors: string[];
}> {
2025-09-12 14:42:00 -05:00
try {
const response = await sshHostApi.post("/bulk-import", { hosts });
return response.data;
} catch (error) {
handleApiError(error, "bulk import SSH hosts");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function deleteSSHHost(
hostId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await sshHostApi.delete(`/db/host/${hostId}`);
return response.data;
} catch (error) {
handleApiError(error, "delete SSH host");
}
2025-08-07 02:20:27 -05:00
}
export async function getSSHHostById(hostId: number): Promise<SSHHost> {
2025-09-12 14:42:00 -05:00
try {
const response = await sshHostApi.get(`/db/host/${hostId}`);
return response.data;
} catch (error) {
handleApiError(error, "fetch SSH host");
}
2025-08-07 02:20:27 -05:00
}
2025-10-03 00:02:10 -05:00
export async function exportSSHHostWithCredentials(
hostId: number,
): Promise<SSHHost> {
try {
const response = await sshHostApi.get(`/db/host/${hostId}/export`);
return response.data;
} catch (error) {
handleApiError(error, "export SSH host with credentials");
}
}
2025-10-01 15:40:10 -05:00
// ============================================================================
// SSH AUTOSTART MANAGEMENT
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function enableAutoStart(
sshConfigId: number,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
const response = await sshHostApi.post("/autostart/enable", {
sshConfigId,
});
return response.data;
} catch (error) {
handleApiError(error, "enable autostart");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function disableAutoStart(
sshConfigId: number,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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");
}
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// TUNNEL MANAGEMENT
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function getTunnelStatuses(): Promise<
Record<string, TunnelStatus>
> {
try {
const response = await tunnelApi.get("/tunnel/status");
return response.data || {};
} catch (error) {
handleApiError(error, "fetch tunnel statuses");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function getTunnelStatusByName(
tunnelName: string,
): Promise<TunnelStatus | undefined> {
const statuses = await getTunnelStatuses();
return statuses[tunnelName];
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function connectTunnel(
tunnelConfig: TunnelConfig,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await tunnelApi.post("/tunnel/connect", tunnelConfig);
return response.data;
} catch (error) {
handleApiError(error, "connect tunnel");
}
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function disconnectTunnel(
tunnelName: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName });
return response.data;
} catch (error) {
handleApiError(error, "disconnect tunnel");
}
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function cancelTunnel(
tunnelName: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await tunnelApi.post("/tunnel/cancel", { tunnelName });
return response.data;
} catch (error) {
handleApiError(error, "cancel tunnel");
}
2025-08-07 02:20:27 -05:00
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// FILE MANAGER METADATA (Recent, Pinned, Shortcuts)
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function getFileManagerRecent(
hostId: number,
): Promise<FileManagerFile[]> {
try {
const response = await sshHostApi.get(
`/file_manager/recent?hostId=${hostId}`,
);
return response.data || [];
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
return [];
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function addFileManagerRecent(
file: FileManagerOperation,
+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 sshHostApi.post("/file_manager/recent", file);
return response.data;
} catch (error) {
handleApiError(error, "add recent file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function removeFileManagerRecent(
file: FileManagerOperation,
+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 sshHostApi.delete("/file_manager/recent", {
data: file,
});
return response.data;
} catch (error) {
handleApiError(error, "remove recent file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function getFileManagerPinned(
hostId: number,
): Promise<FileManagerFile[]> {
try {
const response = await sshHostApi.get(
`/file_manager/pinned?hostId=${hostId}`,
);
return response.data || [];
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
return [];
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function addFileManagerPinned(
file: FileManagerOperation,
+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 sshHostApi.post("/file_manager/pinned", file);
return response.data;
} catch (error) {
handleApiError(error, "add pinned file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function removeFileManagerPinned(
file: FileManagerOperation,
+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 sshHostApi.delete("/file_manager/pinned", {
data: file,
});
return response.data;
} catch (error) {
handleApiError(error, "remove pinned file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function getFileManagerShortcuts(
hostId: number,
): Promise<FileManagerShortcut[]> {
try {
const response = await sshHostApi.get(
`/file_manager/shortcuts?hostId=${hostId}`,
);
return response.data || [];
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
return [];
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function addFileManagerShortcut(
shortcut: FileManagerOperation,
+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 sshHostApi.post("/file_manager/shortcuts", shortcut);
return response.data;
} catch (error) {
handleApiError(error, "add shortcut");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function removeFileManagerShortcut(
shortcut: FileManagerOperation,
+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 sshHostApi.delete("/file_manager/shortcuts", {
data: shortcut,
});
return response.data;
} catch (error) {
handleApiError(error, "remove shortcut");
}
2025-08-07 02:20:27 -05:00
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// SSH FILE OPERATIONS
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function connectSSH(
sessionId: string,
config: {
hostId?: number;
2025-08-07 02:20:27 -05:00
ip: string;
port: number;
username: string;
password?: string;
sshKey?: string;
keyPassword?: string;
2025-09-12 14:42:00 -05:00
authType?: string;
credentialId?: number;
userId?: string;
+7
2025-11-05 10:36:16 -06:00
forceKeyboardInteractive?: boolean;
+1
2025-12-31 22:20:12 -06:00
useSocks5?: boolean;
socks5Host?: string;
socks5Port?: number;
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: unknown;
2026-01-24 19:49:42 -06:00
jumpHosts?: any[];
2025-09-12 14:42:00 -05:00
},
+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 fileManagerApi.post("/ssh/connect", {
sessionId,
...config,
});
return response.data;
2026-01-24 19:49:42 -06:00
} catch (error: any) {
// Preserve connection logs from error response
if (error?.response?.data?.connectionLogs) {
const errorWithLogs = new Error(
error?.response?.data?.error ||
error?.response?.data?.message ||
error.message,
);
(errorWithLogs as any).connectionLogs =
error.response.data.connectionLogs;
// Also preserve other fields like requires_totp
if (error.response.data.requires_totp) {
(errorWithLogs as any).requires_totp = true;
(errorWithLogs as any).sessionId = error.response.data.sessionId;
(errorWithLogs as any).prompt = error.response.data.prompt;
}
if (error.response.data.requires_warpgate) {
(errorWithLogs as any).requires_warpgate = true;
(errorWithLogs as any).sessionId = error.response.data.sessionId;
(errorWithLogs as any).url = error.response.data.url;
(errorWithLogs as any).securityKey = error.response.data.securityKey;
}
if (error.response.data.status === "auth_required") {
(errorWithLogs as any).status = "auth_required";
(errorWithLogs as any).reason = error.response.data.reason;
}
throw errorWithLogs;
}
2025-09-12 14:42:00 -05:00
handleApiError(error, "connect SSH");
}
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function disconnectSSH(
sessionId: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await fileManagerApi.post("/ssh/disconnect", {
sessionId,
});
return response.data;
} catch (error) {
handleApiError(error, "disconnect SSH");
}
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function verifySSHTOTP(
sessionId: string,
totpCode: string,
): Promise<Record<string, unknown>> {
try {
const response = await fileManagerApi.post("/ssh/connect-totp", {
sessionId,
totpCode,
});
return response.data;
} catch (error) {
handleApiError(error, "verify SSH TOTP");
}
}
2026-01-24 19:49:42 -06:00
export async function verifySSHWarpgate(
sessionId: string,
): Promise<Record<string, unknown>> {
try {
const response = await fileManagerApi.post("/ssh/connect-warpgate", {
sessionId,
});
return response.data;
} catch (error) {
handleApiError(error, "verify SSH Warpgate");
}
}
/**
* @openapi
* /ssh/quick-connect:
* post:
* summary: Create a temporary SSH connection without saving to database
* description: Returns a temporary host configuration for immediate use
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - ip
* - port
* - username
* - authType
* properties:
* ip:
* type: string
* description: SSH server IP or hostname
* port:
* type: number
* description: SSH server port
* username:
* type: string
* description: SSH username
* authType:
* type: string
* enum: [password, key, credential]
* description: Authentication method
* password:
* type: string
* description: Password (required if authType is password)
* key:
* type: string
* description: SSH private key (required if authType is key)
* keyPassword:
* type: string
* description: SSH key password (optional)
* keyType:
* type: string
* description: SSH key type
* credentialId:
* type: number
* description: Credential ID (required if authType is credential)
* overrideCredentialUsername:
* type: boolean
* description: Use provided username instead of credential username
* responses:
* 200:
* description: Temporary host configuration created successfully
* content:
* application/json:
* schema:
* type: object
* description: SSHHost object
* 400:
* description: Invalid request data
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
export async function quickConnect(
data: Record<string, unknown>,
): Promise<SSHHost> {
try {
const response = await authApi.post("/ssh/quick-connect", data);
return response.data;
} catch (error) {
throw handleApiError(error, "quick connect");
}
}
2025-09-12 14:42:00 -05:00
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");
}
2025-08-07 02:20:27 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function keepSSHAlive(
sessionId: string,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
const response = await fileManagerApi.post("/ssh/keepalive", {
sessionId,
});
return response.data;
} catch (error) {
handleApiError(error, "SSH keepalive");
}
}
2025-09-12 14:42:00 -05:00
export async function listSSHFiles(
sessionId: string,
path: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<{ files: unknown[]; path: string }> {
2025-09-12 14:42:00 -05:00
try {
const response = await fileManagerApi.get("/ssh/listFiles", {
params: { sessionId, path },
});
2025-10-01 15:40:10 -05:00
return response.data || { files: [], path };
2025-09-12 14:42:00 -05:00
} catch (error) {
handleApiError(error, "list SSH files");
2025-10-01 15:40:10 -05:00
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");
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function readSSHFile(
sessionId: string,
path: string,
): Promise<{
content: string;
path: string;
encoding?: "base64" | "utf8";
}> {
2025-09-12 14:42:00 -05:00
try {
const response = await fileManagerApi.get("/ssh/readFile", {
params: { sessionId, path },
});
return response.data;
+7
2025-11-05 10:36:16 -06:00
} catch (error: unknown) {
2025-10-01 15:40:10 -05:00
if (error.response?.status === 404) {
const customError = new Error("File not found");
+7
2025-11-05 10:36:16 -06:00
(
customError as Error & { response?: unknown; isFileNotFound?: boolean }
).response = error.response;
(
customError as Error & { response?: unknown; isFileNotFound?: boolean }
).isFileNotFound = error.response.data?.fileNotFound || true;
2025-10-01 15:40:10 -05:00
throw customError;
}
2025-09-12 14:42:00 -05:00
handleApiError(error, "read SSH file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function writeSSHFile(
sessionId: string,
path: string,
content: string,
hostId?: number,
userId?: 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 fileManagerApi.post("/ssh/writeFile", {
sessionId,
path,
content,
hostId,
userId,
});
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
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");
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
} catch (error) {
handleApiError(error, "write SSH file");
}
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
export async function uploadSSHFile(
sessionId: string,
path: string,
fileName: string,
content: string,
hostId?: number,
userId?: 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 fileManagerApi.post("/ssh/uploadFile", {
sessionId,
path,
fileName,
content,
hostId,
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "upload SSH file");
}
}
2025-10-01 15:40:10 -05:00
export async function downloadSSHFile(
sessionId: string,
filePath: string,
hostId?: number,
userId?: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
const response = await fileManagerApi.post("/ssh/downloadFile", {
sessionId,
path: filePath,
hostId,
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "download SSH file");
}
}
2025-09-12 14:42:00 -05:00
export async function createSSHFile(
sessionId: string,
path: string,
fileName: string,
content: string = "",
hostId?: number,
userId?: 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 fileManagerApi.post("/ssh/createFile", {
sessionId,
path,
fileName,
content,
hostId,
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "create SSH file");
}
}
2025-09-12 14:42:00 -05:00
export async function createSSHFolder(
sessionId: string,
path: string,
folderName: string,
hostId?: number,
userId?: 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 fileManagerApi.post("/ssh/createFolder", {
sessionId,
path,
folderName,
hostId,
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "create SSH folder");
}
}
2025-09-12 14:42:00 -05:00
export async function deleteSSHItem(
sessionId: string,
path: string,
isDirectory: boolean,
hostId?: number,
userId?: 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 fileManagerApi.delete("/ssh/deleteItem", {
data: {
sessionId,
path,
isDirectory,
hostId,
userId,
},
});
return response.data;
} catch (error) {
handleApiError(error, "delete SSH item");
}
}
2026-01-24 19:49:42 -06:00
export async function setSudoPassword(
sessionId: string,
password: string,
): Promise<void> {
try {
await fileManagerApi.post("/sudo-password", {
sessionId,
password,
});
} catch (error) {
handleApiError(error, "set sudo password");
}
}
2025-10-01 15:40:10 -05:00
export async function copySSHItem(
sessionId: string,
sourcePath: string,
targetDir: string,
hostId?: number,
userId?: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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;
}
}
2025-09-12 14:42:00 -05:00
export async function renameSSHItem(
sessionId: string,
oldPath: string,
newName: string,
hostId?: number,
userId?: 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 fileManagerApi.put("/ssh/renameItem", {
sessionId,
oldPath,
newName,
hostId,
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "rename SSH item");
2025-10-01 15:40:10 -05:00
throw error;
}
}
export async function moveSSHItem(
sessionId: string,
oldPath: string,
newPath: string,
hostId?: number,
userId?: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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;
}
}
2025-11-17 09:46:05 -06:00
export async function changeSSHPermissions(
sessionId: string,
path: string,
permissions: string,
hostId?: number,
userId?: string,
): Promise<{ success: boolean; message: string }> {
try {
fileLogger.info("Changing SSH file permissions", {
operation: "change_permissions",
sessionId,
path,
permissions,
hostId,
userId,
});
const response = await fileManagerApi.post("/ssh/changePermissions", {
sessionId,
path,
permissions,
hostId,
userId,
});
fileLogger.success("SSH file permissions changed successfully", {
operation: "change_permissions",
sessionId,
path,
permissions,
});
return response.data;
} catch (error) {
fileLogger.error("Failed to change SSH file permissions", error, {
operation: "change_permissions",
sessionId,
path,
permissions,
});
handleApiError(error, "change SSH permissions");
throw error;
}
}
export async function extractSSHArchive(
sessionId: string,
archivePath: string,
extractPath?: string,
hostId?: number,
userId?: string,
): Promise<{ success: boolean; message: string; extractPath: string }> {
try {
fileLogger.info("Extracting archive", {
operation: "extract_archive",
sessionId,
archivePath,
extractPath,
hostId,
userId,
});
const response = await fileManagerApi.post("/ssh/extractArchive", {
sessionId,
archivePath,
extractPath,
hostId,
userId,
});
fileLogger.success("Archive extracted successfully", {
operation: "extract_archive",
sessionId,
archivePath,
extractPath: response.data.extractPath,
});
return response.data;
} catch (error) {
fileLogger.error("Failed to extract archive", error, {
operation: "extract_archive",
sessionId,
archivePath,
extractPath,
});
handleApiError(error, "extract archive");
throw error;
}
}
export async function compressSSHFiles(
sessionId: string,
paths: string[],
archiveName: string,
format?: string,
hostId?: number,
userId?: string,
): Promise<{ success: boolean; message: string; archivePath: string }> {
try {
fileLogger.info("Compressing files", {
operation: "compress_files",
sessionId,
paths,
archiveName,
format,
hostId,
userId,
});
const response = await fileManagerApi.post("/ssh/compressFiles", {
sessionId,
paths,
archiveName,
format: format || "zip",
hostId,
userId,
});
fileLogger.success("Files compressed successfully", {
operation: "compress_files",
sessionId,
paths,
archivePath: response.data.archivePath,
});
return response.data;
} catch (error) {
fileLogger.error("Failed to compress files", error, {
operation: "compress_files",
sessionId,
paths,
archiveName,
format,
});
handleApiError(error, "compress files");
throw error;
}
}
2025-10-01 15:40:10 -05:00
// ============================================================================
// FILE MANAGER DATA
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function getRecentFiles(
hostId: number,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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;
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getPinnedFiles(
hostId: number,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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;
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getFolderShortcuts(
hostId: number,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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;
2025-09-12 14:42:00 -05:00
}
}
2025-08-24 01:27:41 -05:00
// ============================================================================
// SERVER STATISTICS
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function getAllServerStatuses(): Promise<
Record<number, ServerStatus>
> {
try {
const response = await statsApi.get("/status");
return response.data || {};
} catch (error) {
handleApiError(error, "fetch server statuses");
}
}
export async function getServerStatusById(id: number): Promise<ServerStatus> {
2025-09-12 14:42:00 -05:00
try {
const response = await statsApi.get(`/status/${id}`);
return response.data;
} catch (error) {
handleApiError(error, "fetch server status");
+1
2025-12-31 22:20:12 -06:00
throw error;
2025-09-12 14:42:00 -05:00
}
}
export async function getServerMetricsById(id: number): Promise<ServerMetrics> {
2025-09-12 14:42:00 -05:00
try {
const response = await statsApi.get(`/metrics/${id}`);
return response.data;
} catch (error) {
handleApiError(error, "fetch server metrics");
+1
2025-12-31 22:20:12 -06:00
throw error;
}
}
export async function startMetricsPolling(hostId: number): Promise<{
success: boolean;
requires_totp?: boolean;
sessionId?: string;
prompt?: string;
viewerSessionId?: string;
2026-01-24 19:49:42 -06:00
connectionLogs?: any[];
+1
2025-12-31 22:20:12 -06:00
}> {
try {
const response = await statsApi.post(`/metrics/start/${hostId}`);
return response.data;
2026-01-24 19:49:42 -06:00
} catch (error: any) {
// Preserve connection logs from error response
if (error?.response?.data?.connectionLogs) {
const errorWithLogs = new Error(
error?.response?.data?.error || error.message,
);
(errorWithLogs as any).connectionLogs =
error.response.data.connectionLogs;
throw errorWithLogs;
}
+1
2025-12-31 22:20:12 -06:00
handleApiError(error, "start metrics polling");
throw error;
}
}
export async function stopMetricsPolling(
hostId: number,
viewerSessionId?: string,
): Promise<void> {
try {
await statsApi.post(`/metrics/stop/${hostId}`, { viewerSessionId });
} catch (error) {
handleApiError(error, "stop metrics polling");
throw error;
}
}
export async function sendMetricsHeartbeat(
viewerSessionId: string,
): Promise<void> {
try {
await statsApi.post("/metrics/heartbeat", { viewerSessionId });
} catch (error) {
handleApiError(error, "send metrics heartbeat");
throw error;
}
}
export async function registerMetricsViewer(
hostId: number,
): Promise<{ success: boolean; viewerSessionId: string }> {
try {
const response = await statsApi.post("/metrics/register-viewer", {
hostId,
});
return response.data;
} catch (error) {
handleApiError(error, "register metrics viewer");
throw error;
}
}
export async function unregisterMetricsViewer(
hostId: number,
viewerSessionId: string,
): Promise<void> {
try {
await statsApi.post("/metrics/unregister-viewer", {
hostId,
viewerSessionId,
});
} catch (error) {
handleApiError(error, "unregister metrics viewer");
throw error;
}
}
export async function submitMetricsTOTP(
sessionId: string,
totpCode: string,
): Promise<{
success: boolean;
viewerSessionId?: string;
}> {
try {
const response = await statsApi.post("/metrics/connect-totp", {
sessionId,
totpCode,
});
return response.data;
} catch (error) {
handleApiError(error, "submit metrics TOTP");
throw error;
2025-09-12 14:42:00 -05:00
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function refreshServerPolling(): Promise<void> {
try {
await statsApi.post("/refresh");
} catch (error) {
console.warn("Failed to refresh server polling:", error);
}
}
2025-11-17 09:46:05 -06:00
export async function notifyHostCreatedOrUpdated(
hostId: number,
): Promise<void> {
try {
await statsApi.post("/host-updated", { hostId });
} catch (error) {
console.warn("Failed to notify stats server of host update:", error);
}
}
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,
): Promise<AuthResponse> {
try {
const response = await authApi.post("/users/login", { username, password });
2025-10-01 15:40:10 -05:00
+7
2025-11-05 10:36:16 -06:00
const hasToken = response.data.token;
if (isElectron() && hasToken) {
2025-10-01 15:40:10 -05:00
localStorage.setItem("jwt", response.data.token);
}
+7
2025-11-05 10:36:16 -06:00
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);
}
}
2025-10-01 15:40:10 -05:00
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,
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,
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");
if (isElectron()) {
localStorage.removeItem("jwt");
electronSettingsCache.delete("jwt");
} else {
const isSecure = window.location.protocol === "https:";
const cookieString = isSecure
? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Strict"
: "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict";
document.cookie = cookieString;
}
2025-10-01 15:40:10 -05:00
return response.data;
} catch (error) {
if (isElectron()) {
localStorage.removeItem("jwt");
electronSettingsCache.delete("jwt");
} else {
const isSecure = window.location.protocol === "https:";
const cookieString = isSecure
? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Strict"
: "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict";
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");
return response.data;
} catch (error) {
handleApiError(error, "fetch user info");
}
2025-08-21 22:47:38 -05:00
}
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");
}
}
2025-08-21 22:47:38 -05:00
export async function getOIDCAuthorizeUrl(): Promise<OIDCAuthorize> {
2025-09-12 14:42:00 -05:00
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[] }> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/list");
return response.data;
} catch (error) {
handleApiError(error, "fetch user list");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getSessions(): Promise<{
sessions: {
id: string;
userId: string;
username?: string;
deviceType: string;
deviceInfo: string;
createdAt: string;
expiresAt: string;
lastActiveAt: string;
jwtToken: string;
isRevoked?: boolean;
}[];
}> {
try {
const response = await authApi.get("/users/sessions");
return response.data;
} catch (error) {
handleApiError(error, "fetch sessions");
}
}
export async function revokeSession(
sessionId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await authApi.delete(`/users/sessions/${sessionId}`);
return response.data;
} catch (error) {
handleApiError(error, "revoke session");
}
}
export async function revokeAllUserSessions(
userId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await authApi.post("/users/sessions/revoke-all", {
targetUserId: userId,
exceptCurrent: false,
});
return response.data;
} catch (error) {
handleApiError(error, "revoke all user sessions");
}
}
export async function makeUserAdmin(
username: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/make-admin", { username });
return response.data;
} catch (error) {
handleApiError(error, "make user admin");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function removeAdminStatus(
username: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/remove-admin", { username });
return response.data;
} catch (error) {
handleApiError(error, "remove admin status");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function deleteUser(
username: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.delete("/users/delete-user", {
data: { username },
});
return response.data;
} catch (error) {
handleApiError(error, "delete user");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function deleteAccount(
password: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.delete("/users/delete-account", {
data: { password },
});
return response.data;
} catch (error) {
handleApiError(error, "delete account");
}
}
2025-09-12 14:42:00 -05:00
export async function updateRegistrationAllowed(
allowed: boolean,
+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.patch("/users/registration-allowed", {
allowed,
});
return response.data;
} catch (error) {
handleApiError(error, "update registration allowed");
}
}
+7
2025-11-05 10:36:16 -06:00
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");
}
}
2026-01-24 19:49:42 -06:00
export async function getPasswordResetAllowed(): Promise<boolean> {
try {
const response = await authApi.get("/users/password-reset-allowed");
return response.data.allowed;
} catch (error) {
handleApiError(error, "get password reset allowed");
}
}
export async function updatePasswordResetAllowed(
allowed: boolean,
): Promise<{ allowed: boolean }> {
try {
const response = await authApi.patch("/users/password-reset-allowed", {
allowed,
});
return response.data;
} catch (error) {
handleApiError(error, "update password reset allowed");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function updateOIDCConfig(
config: Record<string, unknown>,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/oidc-config", config);
return response.data;
} catch (error) {
handleApiError(error, "update OIDC config");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function disableOIDCConfig(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.delete("/users/oidc-config");
return response.data;
} catch (error) {
handleApiError(error, "disable OIDC config");
}
}
// ============================================================================
// ALERTS
// ============================================================================
2025-09-12 14:42:00 -05:00
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;
}
}
2025-09-12 14:42:00 -05:00
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;
}
}
2025-09-12 14:42:00 -05:00
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;
}
}
2025-09-12 14:42:00 -05:00
export async function verifyTOTPLogin(
temp_token: string,
totp_code: string,
): Promise<AuthResponse> {
try {
const response = await authApi.post("/users/totp/verify-login", {
temp_token,
totp_code,
});
+7
2025-11-05 10:36:16 -06:00
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);
}
}
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error as AxiosError, "verify TOTP login");
throw error;
}
}
2025-09-12 14:42:00 -05:00
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;
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getUserAlerts(): Promise<{
alerts: Array<Record<string, unknown>>;
}> {
2025-09-12 14:42:00 -05:00
try {
2025-10-01 15:40:10 -05:00
const response = await authApi.get(`/alerts`);
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "fetch user alerts");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function dismissAlert(
alertId: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
2025-10-01 15:40:10 -05:00
const response = await authApi.post("/alerts/dismiss", { alertId });
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "dismiss alert");
}
}
// ============================================================================
// UPDATES & RELEASES
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function getReleasesRSS(
perPage: number = 100,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get(`/releases/rss?per_page=${perPage}`);
return response.data;
} catch (error) {
handleApiError(error, "fetch releases RSS");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getVersionInfo(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/version");
return response.data;
} catch (error) {
handleApiError(error, "fetch version info");
}
}
// ============================================================================
// DATABASE HEALTH
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function getDatabaseHealth(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
+7
2025-11-05 10:36:16 -06:00
const response = await authApi.get("/health");
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "check database health");
}
}
// ============================================================================
// SSH CREDENTIALS MANAGEMENT
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function getCredentials(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/credentials");
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "fetch credentials");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getCredentialDetails(
credentialId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get(`/credentials/${credentialId}`);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "fetch credential details");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
export async function createCredential(
credentialData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/credentials", credentialData);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "create credential");
2025-09-12 14:42:00 -05:00
}
}
export async function updateCredential(
credentialId: number,
+7
2025-11-05 10:36:16 -06:00
credentialData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.put(
`/credentials/${credentialId}`,
credentialData,
);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "update credential");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
export async function deleteCredential(
credentialId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.delete(`/credentials/${credentialId}`);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "delete credential");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getCredentialHosts(
credentialId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get(`/credentials/${credentialId}/hosts`);
return response.data;
} catch (error) {
handleApiError(error, "fetch credential hosts");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getCredentialFolders(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/credentials/folders");
return response.data;
} catch (error) {
handleApiError(error, "fetch credential folders");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function getSSHHostWithCredentials(
hostId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
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,
+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 sshHostApi.post(
`/db/host/${hostId}/apply-credential`,
{ credentialId },
);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "apply credential to host");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
export async function removeCredentialFromHost(
hostId: number,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await sshHostApi.delete(`/db/host/${hostId}/credential`);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "remove credential from host");
2025-09-12 14:42:00 -05:00
}
}
export async function migrateHostToCredential(
hostId: number,
credentialName: 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 sshHostApi.post(
`/db/host/${hostId}/migrate-to-credential`,
{ credentialName },
);
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "migrate host to credential");
2025-09-12 14:42:00 -05:00
}
}
// ============================================================================
// SSH FOLDER MANAGEMENT
// ============================================================================
+7
2025-11-05 10:36:16 -06:00
export async function getFoldersWithStats(): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
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,
+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.put("/ssh/folders/rename", {
oldName,
newName,
});
return response.data;
} catch (error) {
handleApiError(error, "rename folder");
}
}
2025-11-17 09:46:05 -06:00
export async function getSSHFolders(): Promise<SSHFolder[]> {
try {
sshLogger.info("Fetching SSH folders", {
operation: "fetch_ssh_folders",
});
const response = await authApi.get("/ssh/folders");
sshLogger.success("SSH folders fetched successfully", {
operation: "fetch_ssh_folders",
count: response.data.length,
});
return response.data;
} catch (error) {
sshLogger.error("Failed to fetch SSH folders", error, {
operation: "fetch_ssh_folders",
});
handleApiError(error, "fetch SSH folders");
throw error;
}
}
export async function updateFolderMetadata(
name: string,
color?: string,
icon?: string,
): Promise<void> {
try {
sshLogger.info("Updating folder metadata", {
operation: "update_folder_metadata",
name,
color,
icon,
});
await authApi.put("/ssh/folders/metadata", {
name,
color,
icon,
});
sshLogger.success("Folder metadata updated successfully", {
operation: "update_folder_metadata",
name,
});
} catch (error) {
sshLogger.error("Failed to update folder metadata", error, {
operation: "update_folder_metadata",
name,
});
handleApiError(error, "update folder metadata");
throw error;
}
}
export async function deleteAllHostsInFolder(
folderName: string,
): Promise<{ deletedCount: number }> {
try {
sshLogger.info("Deleting all hosts in folder", {
operation: "delete_folder_hosts",
folderName,
});
const response = await authApi.delete(
`/ssh/folders/${encodeURIComponent(folderName)}/hosts`,
);
sshLogger.success("All hosts in folder deleted successfully", {
operation: "delete_folder_hosts",
folderName,
deletedCount: response.data.deletedCount,
});
return response.data;
} catch (error) {
sshLogger.error("Failed to delete hosts in folder", error, {
operation: "delete_folder_hosts",
folderName,
});
handleApiError(error, "delete hosts in folder");
throw error;
}
}
2025-09-12 14:42:00 -05:00
export async function renameCredentialFolder(
oldName: string,
newName: 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.put("/credentials/folders/rename", {
oldName,
newName,
});
return response.data;
} catch (error) {
2025-10-01 15:40:10 -05:00
throw handleApiError(error, "rename credential folder");
}
}
export async function detectKeyType(
privateKey: string,
keyPassword?: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
const response = await authApi.post("/credentials/detect-key-type", {
privateKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "detect key type");
}
}
+7
2025-11-05 10:36:16 -06:00
export async function detectPublicKeyType(
publicKey: string,
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
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,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-10-01 15:40:10 -05:00
try {
const response = await authApi.post(
`/credentials/${credentialId}/deploy-to-host`,
{ targetHostId },
);
return response.data;
} catch (error) {
throw handleApiError(error, "deploy credential to host");
2025-09-12 14:42:00 -05:00
}
}
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
// SNIPPETS API
// ============================================================================
export async function getSnippets(): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/snippets");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch snippets");
}
}
export async function createSnippet(
snippetData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
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<string, unknown>,
): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
try {
const response = await authApi.delete(`/snippets/${snippetId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "delete snippet");
}
}
2025-11-17 09:46:05 -06:00
export async function executeSnippet(
snippetId: number,
hostId: number,
): Promise<{ success: boolean; output: string; error?: string }> {
try {
const response = await authApi.post("/snippets/execute", {
snippetId,
hostId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "execute snippet");
}
}
2026-01-24 19:49:42 -06:00
// ============================================================================
// MISCELLANEOUS API CALLS
// ============================================================================
export interface NetworkTopologyData {
nodes: any[];
edges: any[];
}
export async function getNetworkTopology(): Promise<NetworkTopologyData | null> {
2025-11-17 09:46:05 -06:00
try {
2026-01-24 19:49:42 -06:00
const response = await authApi.get("/network-topology/");
2025-11-17 09:46:05 -06:00
return response.data;
} catch (error) {
2026-01-24 19:49:42 -06:00
throw handleApiError(error, "fetch network topology");
}
}
export async function saveNetworkTopology(
topology: NetworkTopologyData,
): Promise<{ success: boolean }> {
try {
const response = await authApi.post("/network-topology/", { topology });
return response.data;
} catch (error) {
throw handleApiError(error, "save network topology");
2025-11-17 09:46:05 -06:00
}
}
export async function getSnippetFolders(): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/snippets/folders");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch snippet folders");
}
}
export async function createSnippetFolder(folderData: {
name: string;
color?: string;
icon?: string;
}): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/snippets/folders", folderData);
return response.data;
} catch (error) {
throw handleApiError(error, "create snippet folder");
}
}
export async function updateSnippetFolderMetadata(
folderName: string,
metadata: { color?: string; icon?: string },
): Promise<Record<string, unknown>> {
try {
const response = await authApi.put(
`/snippets/folders/${encodeURIComponent(folderName)}/metadata`,
metadata,
);
return response.data;
} catch (error) {
throw handleApiError(error, "update snippet folder metadata");
}
}
export async function renameSnippetFolder(
oldName: string,
newName: string,
): Promise<{ success: boolean; oldName: string; newName: string }> {
try {
const response = await authApi.put("/snippets/folders/rename", {
oldName,
newName,
});
return response.data;
} catch (error) {
throw handleApiError(error, "rename snippet folder");
}
}
export async function deleteSnippetFolder(
folderName: string,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(
`/snippets/folders/${encodeURIComponent(folderName)}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "delete snippet folder");
}
}
2026-01-24 19:49:42 -06:00
export async function reorderSnippets(
updates: Array<{ id: number; order: number; folder?: string }>,
): Promise<{ success: boolean }> {
try {
const response = await authApi.post("/snippets/reorder", { updates });
return response.data;
} catch (error) {
throw handleApiError(error, "reorder snippets");
}
}
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
2026-01-24 19:49:42 -06:00
// DASHBOARD API
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
export interface UptimeInfo {
uptimeMs: number;
uptimeSeconds: number;
formatted: string;
}
export interface RecentActivityItem {
id: number;
userId: string;
+1
2025-12-31 22:20:12 -06:00
type: "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker";
+7
2025-11-05 10:36:16 -06:00
hostId: number;
hostName: string;
timestamp: string;
}
export async function getUptime(): Promise<UptimeInfo> {
try {
2026-01-24 19:49:42 -06:00
const response = await dashboardApi.get("/uptime");
+7
2025-11-05 10:36:16 -06:00
return response.data;
} catch (error) {
throw handleApiError(error, "fetch uptime");
}
}
export async function getRecentActivity(
limit?: number,
): Promise<RecentActivityItem[]> {
try {
2026-01-24 19:49:42 -06:00
const response = await dashboardApi.get("/activity/recent", {
+7
2025-11-05 10:36:16 -06:00
params: { limit },
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch recent activity");
}
}
export async function logActivity(
+1
2025-12-31 22:20:12 -06:00
type: "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker",
+7
2025-11-05 10:36:16 -06:00
hostId: number,
hostName: string,
): Promise<{ message: string; id: number | string }> {
try {
2026-01-24 19:49:42 -06:00
const response = await dashboardApi.post("/activity/log", {
+7
2025-11-05 10:36:16 -06:00
type,
hostId,
hostName,
});
return response.data;
} catch (error) {
throw handleApiError(error, "log activity");
}
}
export async function resetRecentActivity(): Promise<{ message: string }> {
try {
2026-01-24 19:49:42 -06:00
const response = await dashboardApi.delete("/activity/reset");
+7
2025-11-05 10:36:16 -06:00
return response.data;
} catch (error) {
throw handleApiError(error, "reset recent activity");
}
}
2025-11-17 09:46:05 -06:00
// ============================================================================
// COMMAND HISTORY API
// ============================================================================
export async function saveCommandToHistory(
hostId: number,
command: string,
): Promise<{ id: number; command: string; executedAt: string }> {
try {
const response = await authApi.post("/terminal/command_history", {
hostId,
command,
});
return response.data;
} catch (error) {
throw handleApiError(error, "save command to history");
}
}
export async function getCommandHistory(
hostId: number,
limit: number = 100,
): Promise<string[]> {
try {
const response = await authApi.get(`/terminal/command_history/${hostId}`, {
params: { limit },
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch command history");
}
}
export async function deleteCommandFromHistory(
hostId: number,
command: string,
): Promise<{ success: boolean }> {
try {
const response = await authApi.post("/terminal/command_history/delete", {
hostId,
command,
});
return response.data;
} catch (error) {
throw handleApiError(error, "delete command from history");
}
}
export async function clearCommandHistory(
hostId: number,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(
`/terminal/command_history/${hostId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "clear command history");
}
}
// ============================================================================
// OIDC ACCOUNT LINKING
// ============================================================================
export async function linkOIDCToPasswordAccount(
oidcUserId: string,
targetUsername: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await authApi.post("/users/link-oidc-to-password", {
oidcUserId,
targetUsername,
});
return response.data;
} catch (error) {
throw handleApiError(error, "link OIDC account to password account");
}
}
export async function unlinkOIDCFromPasswordAccount(
userId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await authApi.post("/users/unlink-oidc-from-password", {
userId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "unlink OIDC from password account");
}
}
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// RBAC MANAGEMENT
// ============================================================================
// Role Management
export async function getRoles(): Promise<{ roles: Role[] }> {
try {
const response = await rbacApi.get("/rbac/roles");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch roles");
}
}
export async function createRole(roleData: {
name: string;
displayName: string;
description?: string | null;
}): Promise<{ role: Role }> {
try {
const response = await rbacApi.post("/rbac/roles", roleData);
return response.data;
} catch (error) {
throw handleApiError(error, "create role");
}
}
export async function updateRole(
roleId: number,
roleData: {
displayName?: string;
description?: string | null;
},
): Promise<{ role: Role }> {
try {
const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
return response.data;
} catch (error) {
throw handleApiError(error, "update role");
}
}
export async function deleteRole(
roleId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(`/rbac/roles/${roleId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "delete role");
}
}
// User-Role Management
export async function getUserRoles(
userId: string,
): Promise<{ roles: UserRole[] }> {
try {
const response = await rbacApi.get(`/rbac/users/${userId}/roles`);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch user roles");
}
}
export async function assignRoleToUser(
userId: string,
roleId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.post(`/rbac/users/${userId}/roles`, {
roleId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "assign role to user");
}
}
export async function removeRoleFromUser(
userId: string,
roleId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(
`/rbac/users/${userId}/roles/${roleId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "remove role from user");
}
}
// Host Sharing Management
export async function shareHost(
hostId: number,
shareData: {
targetType: "user" | "role";
targetUserId?: string;
targetRoleId?: number;
permissionLevel: "view"; // Only view permission is supported
durationHours?: number;
},
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.post(
`/rbac/host/${hostId}/share`,
shareData,
);
return response.data;
} catch (error) {
throw handleApiError(error, "share host");
}
}
export async function getHostAccess(
hostId: number,
): Promise<{ accessList: AccessRecord[] }> {
try {
const response = await rbacApi.get(`/rbac/host/${hostId}/access`);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch host access");
}
}
export async function revokeHostAccess(
hostId: number,
accessId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(
`/rbac/host/${hostId}/access/${accessId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "revoke host access");
}
}
// ============================================================================
// DOCKER MANAGEMENT API
// ============================================================================
export async function connectDockerSession(
sessionId: string,
hostId: number,
config?: {
userProvidedPassword?: string;
userProvidedSshKey?: string;
userProvidedKeyPassword?: string;
forceKeyboardInteractive?: boolean;
useSocks5?: boolean;
socks5Host?: string;
socks5Port?: number;
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: unknown;
},
): Promise<{
success?: boolean;
message?: string;
requires_totp?: boolean;
prompt?: string;
isPassword?: boolean;
status?: string;
reason?: string;
2026-01-24 19:49:42 -06:00
connectionLogs?: any[];
requires_warpgate?: boolean;
url?: string;
securityKey?: string;
+1
2025-12-31 22:20:12 -06:00
}> {
try {
const response = await dockerApi.post("/ssh/connect", {
sessionId,
hostId,
...config,
});
return response.data;
} catch (error: any) {
if (error.response?.data?.status === "auth_required") {
return error.response.data;
}
if (error.response?.data?.requires_totp) {
return error.response.data;
}
2026-01-24 19:49:42 -06:00
if (error.response?.data?.requires_warpgate) {
return error.response.data;
}
// Preserve connection logs from error response
if (error?.response?.data?.connectionLogs) {
const errorWithLogs = new Error(
error?.response?.data?.error ||
error?.response?.data?.message ||
error.message,
);
(errorWithLogs as any).connectionLogs =
error.response.data.connectionLogs;
throw errorWithLogs;
}
+1
2025-12-31 22:20:12 -06:00
throw handleApiError(error, "connect to Docker SSH session");
}
}
export async function verifyDockerTOTP(
sessionId: string,
totpCode: string,
): Promise<{ status: string; message: string }> {
try {
const response = await dockerApi.post("/ssh/connect-totp", {
sessionId,
totpCode,
});
return response.data;
} catch (error) {
throw handleApiError(error, "verify Docker TOTP");
}
}
2026-01-24 19:49:42 -06:00
export async function verifyDockerWarpgate(
sessionId: string,
): Promise<{ status: string; message: string }> {
try {
const response = await dockerApi.post("/ssh/connect-warpgate", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "verify Docker Warpgate");
}
}
+1
2025-12-31 22:20:12 -06:00
export async function disconnectDockerSession(
sessionId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post("/ssh/disconnect", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "disconnect from Docker SSH session");
}
}
export async function keepaliveDockerSession(
sessionId: string,
): Promise<{ success: boolean }> {
try {
const response = await dockerApi.post("/ssh/keepalive", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "keepalive Docker SSH session");
}
}
export async function getDockerSessionStatus(
sessionId: string,
): Promise<{ success: boolean; connected: boolean }> {
try {
const response = await dockerApi.get("/ssh/status", {
params: { sessionId },
});
return response.data;
} catch (error) {
throw handleApiError(error, "get Docker session status");
}
}
export async function validateDockerAvailability(
sessionId: string,
): Promise<DockerValidation> {
try {
const response = await dockerApi.get(`/validate/${sessionId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "validate Docker availability");
}
}
export async function listDockerContainers(
sessionId: string,
all: boolean = true,
): Promise<DockerContainer[]> {
try {
const response = await dockerApi.get(`/containers/${sessionId}`, {
params: { all },
});
return response.data;
} catch (error) {
throw handleApiError(error, "list Docker containers");
}
}
export async function getDockerContainerDetails(
sessionId: string,
containerId: string,
): Promise<DockerContainer> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "get Docker container details");
}
}
export async function startDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/start`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "start Docker container");
}
}
export async function stopDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/stop`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "stop Docker container");
}
}
export async function restartDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/restart`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "restart Docker container");
}
}
export async function pauseDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/pause`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "pause Docker container");
}
}
export async function unpauseDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/unpause`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "unpause Docker container");
}
}
export async function removeDockerContainer(
sessionId: string,
containerId: string,
force: boolean = false,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.delete(
`/containers/${sessionId}/${containerId}/remove`,
{
params: { force },
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "remove Docker container");
}
}
export async function getContainerLogs(
sessionId: string,
containerId: string,
options?: DockerLogOptions,
): Promise<{ logs: string }> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/logs`,
{
params: options,
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "get container logs");
}
}
export async function downloadContainerLogs(
sessionId: string,
containerId: string,
options?: DockerLogOptions,
): Promise<Blob> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/logs`,
{
params: { ...options, download: true },
responseType: "blob",
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "download container logs");
}
}
export async function getContainerStats(
sessionId: string,
containerId: string,
): Promise<DockerStats> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/stats`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "get container stats");
}
}
2026-01-24 19:49:42 -06:00
export interface DashboardLayout {
cards: Array<{ id: string; enabled: boolean; order: number }>;
}
export async function getDashboardPreferences(): Promise<DashboardLayout> {
const response = await dashboardApi.get("/dashboard/preferences");
return response.data;
}
export async function saveDashboardPreferences(
layout: DashboardLayout,
): Promise<{ success: boolean }> {
const response = await dashboardApi.post("/dashboard/preferences", layout);
return response.data;
}