3652 lines
93 KiB
TypeScript
3652 lines
93 KiB
TypeScript
import axios, { AxiosError, type AxiosInstance } from "axios";
|
|
import type {
|
|
SSHHost,
|
|
SSHHostData,
|
|
SSHFolder,
|
|
TunnelConfig,
|
|
TunnelStatus,
|
|
FileManagerFile,
|
|
FileManagerShortcut,
|
|
DockerContainer,
|
|
DockerStats,
|
|
DockerLogOptions,
|
|
DockerValidation,
|
|
} from "../types/index.js";
|
|
|
|
// ============================================================================
|
|
// RBAC TYPE DEFINITIONS
|
|
// ============================================================================
|
|
|
|
export interface Role {
|
|
id: number;
|
|
name: string;
|
|
displayName: string;
|
|
description: string | null;
|
|
isSystem: boolean;
|
|
permissions: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface UserRole {
|
|
userId: string;
|
|
roleId: number;
|
|
roleName: string;
|
|
roleDisplayName: string;
|
|
grantedBy: string;
|
|
grantedByUsername: string;
|
|
grantedAt: string;
|
|
}
|
|
|
|
export interface AccessRecord {
|
|
id: number;
|
|
targetType: "user" | "role";
|
|
userId: string | null;
|
|
roleId: number | null;
|
|
username: string | null;
|
|
roleName: string | null;
|
|
roleDisplayName: string | null;
|
|
grantedBy: string;
|
|
grantedByUsername: string;
|
|
permissionLevel: "view"; // Only view permission is supported
|
|
expiresAt: string | null;
|
|
createdAt: string;
|
|
}
|
|
import {
|
|
apiLogger,
|
|
authLogger,
|
|
sshLogger,
|
|
tunnelLogger,
|
|
fileLogger,
|
|
statsLogger,
|
|
systemLogger,
|
|
dashboardLogger,
|
|
type LogContext,
|
|
} from "../lib/frontend-logger.js";
|
|
|
|
interface FileManagerOperation {
|
|
name: string;
|
|
path: string;
|
|
isSSH: boolean;
|
|
sshSessionId?: string;
|
|
hostId: number;
|
|
}
|
|
|
|
export type ServerStatus = {
|
|
status: "online" | "offline";
|
|
lastChecked: string;
|
|
};
|
|
|
|
interface CpuMetrics {
|
|
percent: number | null;
|
|
cores: number | null;
|
|
load: [number, number, number] | null;
|
|
}
|
|
|
|
interface MemoryMetrics {
|
|
percent: number | null;
|
|
usedGiB: number | null;
|
|
totalGiB: number | null;
|
|
}
|
|
|
|
interface DiskMetrics {
|
|
percent: number | null;
|
|
usedHuman: string | null;
|
|
totalHuman: string | null;
|
|
availableHuman?: string | null;
|
|
}
|
|
|
|
export type ServerMetrics = {
|
|
cpu: CpuMetrics;
|
|
memory: MemoryMetrics;
|
|
disk: DiskMetrics;
|
|
lastChecked: string;
|
|
};
|
|
|
|
interface AuthResponse {
|
|
token: string;
|
|
success?: boolean;
|
|
is_admin?: boolean;
|
|
username?: string;
|
|
userId?: string;
|
|
is_oidc?: boolean;
|
|
totp_enabled?: boolean;
|
|
data_unlocked?: boolean;
|
|
}
|
|
|
|
interface UserInfo {
|
|
totp_enabled: boolean;
|
|
userId: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
is_oidc: boolean;
|
|
data_unlocked: boolean;
|
|
password_hash?: string;
|
|
}
|
|
|
|
interface UserCount {
|
|
count: number;
|
|
}
|
|
|
|
interface OIDCAuthorize {
|
|
auth_url: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
export function isElectron(): boolean {
|
|
const hasISElectron =
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).IS_ELECTRON === true;
|
|
|
|
const hasElectronAPI = !!(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI;
|
|
|
|
const result = hasISElectron || hasElectronAPI;
|
|
|
|
return result;
|
|
}
|
|
|
|
function getLoggerForService(serviceName: string) {
|
|
if (serviceName.includes("SSH") || serviceName.includes("ssh")) {
|
|
return sshLogger;
|
|
} else if (serviceName.includes("TUNNEL") || serviceName.includes("tunnel")) {
|
|
return tunnelLogger;
|
|
} else if (serviceName.includes("FILE") || serviceName.includes("file")) {
|
|
return fileLogger;
|
|
} else if (serviceName.includes("STATS") || serviceName.includes("stats")) {
|
|
return statsLogger;
|
|
} else if (serviceName.includes("AUTH") || serviceName.includes("auth")) {
|
|
return authLogger;
|
|
} else if (
|
|
serviceName.includes("DASHBOARD") ||
|
|
serviceName.includes("dashboard")
|
|
) {
|
|
return dashboardLogger;
|
|
} else {
|
|
return apiLogger;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
})();
|
|
}
|
|
|
|
export function setCookie(name: string, value: string, days = 7): void {
|
|
if (isElectron()) {
|
|
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);
|
|
}
|
|
} else {
|
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
|
}
|
|
}
|
|
|
|
export function getCookie(name: string): string | undefined {
|
|
if (isElectron()) {
|
|
try {
|
|
if (electronSettingsCache.has(name)) {
|
|
return electronSettingsCache.get(name);
|
|
}
|
|
|
|
const token = localStorage.getItem(name) || undefined;
|
|
if (token) {
|
|
electronSettingsCache.set(name, token);
|
|
}
|
|
console.log(`[Electron] Get setting: ${name} = ${token}`);
|
|
return token;
|
|
} catch (error) {
|
|
console.error(`[Electron] Failed to get setting: ${name}`, error);
|
|
return undefined;
|
|
}
|
|
} else {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
const encodedToken =
|
|
parts.length === 2 ? parts.pop()?.split(";").shift() : undefined;
|
|
const token = encodedToken ? decodeURIComponent(encodedToken) : undefined;
|
|
return token;
|
|
}
|
|
}
|
|
|
|
function createApiInstance(
|
|
baseURL: string,
|
|
serviceName: string = "API",
|
|
): AxiosInstance {
|
|
const instance = axios.create({
|
|
baseURL,
|
|
headers: { "Content-Type": "application/json" },
|
|
timeout: 30000,
|
|
withCredentials: true,
|
|
});
|
|
|
|
instance.interceptors.request.use((config) => {
|
|
const startTime = performance.now();
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
(config as Record<string, unknown>).startTime = startTime;
|
|
(config as Record<string, unknown>).requestId = requestId;
|
|
|
|
const method = config.method?.toUpperCase() || "UNKNOWN";
|
|
const url = config.url || "UNKNOWN";
|
|
const fullUrl = `${config.baseURL}${url}`;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
operation: "request_start",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
logger.requestStart(method, fullUrl, context);
|
|
}
|
|
|
|
if (isElectron()) {
|
|
config.headers["X-Electron-App"] = "true";
|
|
|
|
const token = localStorage.getItem("jwt");
|
|
if (token) {
|
|
config.headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
if (typeof window !== "undefined" && (window as any).ReactNativeWebView) {
|
|
let platform = "Unknown";
|
|
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
if (navigator.userAgent.includes("Android")) {
|
|
platform = "Android";
|
|
} else if (
|
|
navigator.userAgent.includes("iPhone") ||
|
|
navigator.userAgent.includes("iPad") ||
|
|
navigator.userAgent.includes("iOS")
|
|
) {
|
|
platform = "iOS";
|
|
}
|
|
}
|
|
config.headers["User-Agent"] = `Termix-Mobile/${platform}`;
|
|
}
|
|
|
|
return config;
|
|
});
|
|
|
|
instance.interceptors.response.use(
|
|
(response) => {
|
|
const endTime = performance.now();
|
|
const startTime = (response.config as Record<string, unknown>).startTime;
|
|
const requestId = (response.config as Record<string, unknown>).requestId;
|
|
const responseTime = Math.round(endTime - startTime);
|
|
|
|
const method = response.config.method?.toUpperCase() || "UNKNOWN";
|
|
const url = response.config.url || "UNKNOWN";
|
|
const fullUrl = `${response.config.baseURL}${url}`;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
responseTime,
|
|
operation: "request_success",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
logger.requestSuccess(
|
|
method,
|
|
fullUrl,
|
|
response.status,
|
|
responseTime,
|
|
context,
|
|
);
|
|
}
|
|
|
|
if (responseTime > 3000) {
|
|
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
|
}
|
|
|
|
return response;
|
|
},
|
|
(error: AxiosError) => {
|
|
const endTime = performance.now();
|
|
const startTime = (error.config as Record<string, unknown> | undefined)
|
|
?.startTime;
|
|
const requestId = (error.config as Record<string, unknown> | undefined)
|
|
?.requestId;
|
|
const responseTime = startTime
|
|
? Math.round(endTime - startTime)
|
|
: undefined;
|
|
|
|
const method = error.config?.method?.toUpperCase() || "UNKNOWN";
|
|
const url = error.config?.url || "UNKNOWN";
|
|
const fullUrl = error.config ? `${error.config.baseURL}${url}` : url;
|
|
const status = error.response?.status;
|
|
const message =
|
|
(error.response?.data as Record<string, unknown>)?.error ||
|
|
(error as Error).message ||
|
|
"Unknown error";
|
|
const errorCode =
|
|
(error.response?.data as Record<string, unknown>)?.code || error.code;
|
|
|
|
const context: LogContext = {
|
|
requestId,
|
|
method,
|
|
url: fullUrl,
|
|
status,
|
|
responseTime,
|
|
errorCode,
|
|
errorMessage: message,
|
|
operation: "request_error",
|
|
};
|
|
|
|
const logger = getLoggerForService(serviceName);
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
if (status === 401) {
|
|
logger.authError(method, fullUrl, context);
|
|
} else if (status === 0 || !status) {
|
|
logger.networkError(method, fullUrl, message, context);
|
|
} else {
|
|
logger.requestError(
|
|
method,
|
|
fullUrl,
|
|
status || 0,
|
|
message,
|
|
responseTime,
|
|
context,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (status === 401) {
|
|
const errorCode = (error.response?.data as Record<string, unknown>)
|
|
?.code;
|
|
const errorMessage = (error.response?.data as Record<string, unknown>)
|
|
?.error;
|
|
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
|
const isSessionNotFound = errorCode === "SESSION_NOT_FOUND";
|
|
const isInvalidToken =
|
|
errorCode === "AUTH_REQUIRED" ||
|
|
errorMessage === "Invalid token" ||
|
|
errorMessage === "Authentication required";
|
|
|
|
if (isSessionExpired || isSessionNotFound || isInvalidToken) {
|
|
localStorage.removeItem("jwt");
|
|
|
|
if (isElectron()) {
|
|
electronSettingsCache.delete("jwt");
|
|
}
|
|
|
|
if (typeof window !== "undefined") {
|
|
document.cookie =
|
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
}
|
|
|
|
if (isSessionExpired && typeof window !== "undefined") {
|
|
console.warn("Session expired - please log in again");
|
|
import("sonner").then(({ toast }) => {
|
|
toast.warning("Session expired. Please log in again.");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
return instance;
|
|
}
|
|
|
|
// ============================================================================
|
|
// API INSTANCES
|
|
// ============================================================================
|
|
|
|
function isDev(): boolean {
|
|
if (isElectron()) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
process.env.NODE_ENV === "development" &&
|
|
(window.location.port === "3000" ||
|
|
window.location.port === "5173" ||
|
|
window.location.port === "" ||
|
|
window.location.hostname === "localhost" ||
|
|
window.location.hostname === "127.0.0.1")
|
|
);
|
|
}
|
|
|
|
const apiHost = import.meta.env.VITE_API_HOST || "localhost";
|
|
let configuredServerUrl: string | null = null;
|
|
|
|
export interface ServerConfig {
|
|
serverUrl: string;
|
|
lastUpdated: string;
|
|
}
|
|
|
|
export async function getServerConfig(): Promise<ServerConfig | null> {
|
|
if (!isElectron()) return null;
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("get-server-config");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to get server config:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
|
|
if (!isElectron()) return false;
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("save-server-config", config);
|
|
if (result?.success) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
updateApiInstances();
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error("Failed to save server config:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function testServerConnection(
|
|
serverUrl: string,
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
if (!isElectron())
|
|
return { success: false, error: "Not in Electron environment" };
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("test-server-connection", serverUrl);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to test server connection:", error);
|
|
return { success: false, error: "Connection test failed" };
|
|
}
|
|
}
|
|
|
|
export async function checkElectronUpdate(): Promise<{
|
|
success: boolean;
|
|
status?: "up_to_date" | "requires_update";
|
|
localVersion?: string;
|
|
remoteVersion?: string;
|
|
latest_release?: {
|
|
tag_name: string;
|
|
name: string;
|
|
published_at: string;
|
|
html_url: string;
|
|
body: string;
|
|
};
|
|
cached?: boolean;
|
|
cache_age?: number;
|
|
error?: string;
|
|
}> {
|
|
if (!isElectron())
|
|
return { success: false, error: "Not in Electron environment" };
|
|
|
|
try {
|
|
const result = await (
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).electronAPI?.invoke("check-electron-update");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Failed to check Electron update:", error);
|
|
return { success: false, error: "Update check failed" };
|
|
}
|
|
}
|
|
|
|
function getApiUrl(path: string, defaultPort: number): string {
|
|
const devMode = isDev();
|
|
const electronMode = isElectron();
|
|
|
|
if (electronMode) {
|
|
if (configuredServerUrl) {
|
|
const baseUrl = configuredServerUrl.replace(/\/$/, "");
|
|
const url = `${baseUrl}${path}`;
|
|
return url;
|
|
}
|
|
console.warn("Electron mode but no server configured!");
|
|
return "http://no-server-configured";
|
|
} else if (devMode) {
|
|
const protocol = window.location.protocol === "https:" ? "https" : "http";
|
|
const sslPort = protocol === "https" ? 8443 : defaultPort;
|
|
const url = `${protocol}://${apiHost}:${sslPort}${path}`;
|
|
return url;
|
|
} else {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
function initializeApiInstances() {
|
|
// SSH Host Management API (port 30001)
|
|
sshHostApi = createApiInstance(getApiUrl("/ssh", 30001), "SSH_HOST");
|
|
|
|
// Tunnel Management API (port 30003)
|
|
tunnelApi = createApiInstance(getApiUrl("/ssh", 30003), "TUNNEL");
|
|
|
|
// File Manager Operations API (port 30004)
|
|
fileManagerApi = createApiInstance(
|
|
getApiUrl("/ssh/file_manager", 30004),
|
|
"FILE_MANAGER",
|
|
);
|
|
|
|
// Server Statistics API (port 30005)
|
|
statsApi = createApiInstance(getApiUrl("", 30005), "STATS");
|
|
|
|
// Authentication API (port 30001)
|
|
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
|
|
|
|
// Homepage API (port 30006)
|
|
homepageApi = createApiInstance(getApiUrl("", 30006), "HOMEPAGE");
|
|
|
|
// RBAC API (port 30001)
|
|
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
|
|
|
|
// Docker Management API (port 30007)
|
|
dockerApi = createApiInstance(getApiUrl("/docker", 30007), "DOCKER");
|
|
}
|
|
|
|
// SSH Host Management API (port 30001)
|
|
export let sshHostApi: AxiosInstance;
|
|
|
|
// Tunnel Management API (port 30003)
|
|
export let tunnelApi: AxiosInstance;
|
|
|
|
// File Manager Operations API (port 30004)
|
|
export let fileManagerApi: AxiosInstance;
|
|
|
|
// Server Statistics API (port 30005)
|
|
export let statsApi: AxiosInstance;
|
|
|
|
// Authentication API (port 30001)
|
|
export let authApi: AxiosInstance;
|
|
|
|
// Homepage API (port 30006)
|
|
export let homepageApi: AxiosInstance;
|
|
|
|
// RBAC API (port 30001)
|
|
export let rbacApi: AxiosInstance;
|
|
|
|
// Docker Management API (port 30007)
|
|
export let dockerApi: AxiosInstance;
|
|
|
|
function initializeApp() {
|
|
if (isElectron()) {
|
|
getServerConfig()
|
|
.then((config) => {
|
|
if (config?.serverUrl) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
} else {
|
|
console.warn("No server URL in config");
|
|
}
|
|
initializeApiInstances();
|
|
})
|
|
.catch((error) => {
|
|
console.error(
|
|
"Failed to load server config, initializing with default:",
|
|
error,
|
|
);
|
|
initializeApiInstances();
|
|
});
|
|
} else {
|
|
initializeApiInstances();
|
|
}
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initializeApp);
|
|
} else {
|
|
initializeApp();
|
|
}
|
|
|
|
function updateApiInstances() {
|
|
systemLogger.info("Updating API instances with new server configuration", {
|
|
operation: "api_instance_update",
|
|
configuredServerUrl,
|
|
});
|
|
|
|
initializeApiInstances();
|
|
|
|
(
|
|
window as Window &
|
|
typeof globalThis & {
|
|
IS_ELECTRON?: boolean;
|
|
electronAPI?: unknown;
|
|
configuredServerUrl?: string;
|
|
}
|
|
).configuredServerUrl = configuredServerUrl;
|
|
|
|
systemLogger.success("All API instances updated successfully", {
|
|
operation: "api_instance_update_complete",
|
|
configuredServerUrl,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING
|
|
// ============================================================================
|
|
|
|
class ApiError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public status?: number,
|
|
public code?: string,
|
|
) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
function handleApiError(error: unknown, operation: string): never {
|
|
const context: LogContext = {
|
|
operation: "error_handling",
|
|
errorOperation: operation,
|
|
};
|
|
|
|
if (axios.isAxiosError(error)) {
|
|
const status = error.response?.status;
|
|
const message =
|
|
error.response?.data?.message ||
|
|
error.response?.data?.error ||
|
|
error.message;
|
|
const code = error.response?.data?.code || error.response?.data?.error;
|
|
const url = error.config?.url;
|
|
const method = error.config?.method?.toUpperCase();
|
|
|
|
const errorContext: LogContext = {
|
|
...context,
|
|
method,
|
|
url,
|
|
status,
|
|
errorCode: code,
|
|
errorMessage: message,
|
|
};
|
|
|
|
if (status === 401) {
|
|
authLogger.warn(
|
|
`Auth failed: ${method} ${url} - ${message}`,
|
|
errorContext,
|
|
);
|
|
|
|
const isLoginEndpoint = url?.includes("/users/login");
|
|
const errorMessage = isLoginEndpoint
|
|
? message
|
|
: "Authentication required. Please log in again.";
|
|
|
|
throw new ApiError(errorMessage, 401, "AUTH_REQUIRED");
|
|
} else if (status === 403) {
|
|
authLogger.warn(`Access denied: ${method} ${url}`, errorContext);
|
|
const apiError = new ApiError(
|
|
code === "TOTP_REQUIRED"
|
|
? message
|
|
: "Access denied. You do not have permission to perform this action.",
|
|
403,
|
|
code || "ACCESS_DENIED",
|
|
);
|
|
(apiError as ApiError & { response?: unknown }).response = error.response;
|
|
throw apiError;
|
|
} else if (status === 404) {
|
|
apiLogger.warn(`Not found: ${method} ${url}`, errorContext);
|
|
throw new ApiError(
|
|
"Resource not found. The requested item may have been deleted.",
|
|
404,
|
|
"NOT_FOUND",
|
|
);
|
|
} else if (status === 409) {
|
|
apiLogger.warn(`Conflict: ${method} ${url}`, errorContext);
|
|
throw new ApiError(
|
|
"Conflict. The resource already exists or is in use.",
|
|
409,
|
|
"CONFLICT",
|
|
);
|
|
} else if (status === 422) {
|
|
apiLogger.warn(
|
|
`Validation error: ${method} ${url} - ${message}`,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Validation error. Please check your input and try again.",
|
|
422,
|
|
"VALIDATION_ERROR",
|
|
);
|
|
} else if (status && status >= 500) {
|
|
apiLogger.error(
|
|
`Server error: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Server error occurred. Please try again later.",
|
|
status,
|
|
"SERVER_ERROR",
|
|
);
|
|
} else if (status === 0) {
|
|
if (url.includes("no-server-configured")) {
|
|
apiLogger.error(
|
|
`No server configured: ${method} ${url}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"No server configured. Please configure a Termix server first.",
|
|
0,
|
|
"NO_SERVER_CONFIGURED",
|
|
);
|
|
}
|
|
apiLogger.error(
|
|
`Network error: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(
|
|
"Network error. Please check your connection and try again.",
|
|
0,
|
|
"NETWORK_ERROR",
|
|
);
|
|
} else {
|
|
apiLogger.error(
|
|
`Request failed: ${method} ${url} - ${message}`,
|
|
error,
|
|
errorContext,
|
|
);
|
|
throw new ApiError(message || `Failed to ${operation}`, status, code);
|
|
}
|
|
}
|
|
|
|
if (error instanceof ApiError) {
|
|
throw error;
|
|
}
|
|
|
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
apiLogger.error(
|
|
`Unexpected error during ${operation}: ${errorMessage}`,
|
|
error,
|
|
context,
|
|
);
|
|
throw new ApiError(
|
|
`Unexpected error during ${operation}: ${errorMessage}`,
|
|
undefined,
|
|
"UNKNOWN_ERROR",
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH HOST MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getSSHHosts(): Promise<SSHHost[]> {
|
|
try {
|
|
const response = await sshHostApi.get("/db/host");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch SSH hosts");
|
|
}
|
|
}
|
|
|
|
export async function createSSHHost(hostData: SSHHostData): Promise<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,
|
|
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
|
enableTerminal: Boolean(hostData.enableTerminal),
|
|
enableTunnel: Boolean(hostData.enableTunnel),
|
|
enableFileManager: Boolean(hostData.enableFileManager),
|
|
enableDocker: Boolean(hostData.enableDocker),
|
|
defaultPath: hostData.defaultPath || "/",
|
|
tunnelConnections: hostData.tunnelConnections || [],
|
|
jumpHosts: hostData.jumpHosts || [],
|
|
quickActions: hostData.quickActions || [],
|
|
statsConfig: hostData.statsConfig
|
|
? typeof hostData.statsConfig === "string"
|
|
? hostData.statsConfig
|
|
: JSON.stringify(hostData.statsConfig)
|
|
: null,
|
|
terminalConfig: hostData.terminalConfig || null,
|
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
|
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,
|
|
};
|
|
|
|
if (!submitData.enableTunnel) {
|
|
submitData.tunnelConnections = [];
|
|
}
|
|
|
|
if (!submitData.enableFileManager) {
|
|
submitData.defaultPath = "";
|
|
}
|
|
|
|
if (hostData.authType === "key" && hostData.key instanceof File) {
|
|
const formData = new FormData();
|
|
formData.append("key", hostData.key);
|
|
|
|
const dataWithoutFile = { ...submitData };
|
|
delete dataWithoutFile.key;
|
|
formData.append("data", JSON.stringify(dataWithoutFile));
|
|
|
|
const response = await sshHostApi.post("/db/host", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
return response.data;
|
|
} else {
|
|
const response = await sshHostApi.post("/db/host", submitData);
|
|
return response.data;
|
|
}
|
|
} catch (error) {
|
|
handleApiError(error, "create SSH host");
|
|
}
|
|
}
|
|
|
|
export async function updateSSHHost(
|
|
hostId: number,
|
|
hostData: SSHHostData,
|
|
): Promise<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,
|
|
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
|
enableTerminal: Boolean(hostData.enableTerminal),
|
|
enableTunnel: Boolean(hostData.enableTunnel),
|
|
enableFileManager: Boolean(hostData.enableFileManager),
|
|
enableDocker: Boolean(hostData.enableDocker),
|
|
defaultPath: hostData.defaultPath || "/",
|
|
tunnelConnections: hostData.tunnelConnections || [],
|
|
jumpHosts: hostData.jumpHosts || [],
|
|
quickActions: hostData.quickActions || [],
|
|
statsConfig: hostData.statsConfig
|
|
? typeof hostData.statsConfig === "string"
|
|
? hostData.statsConfig
|
|
: JSON.stringify(hostData.statsConfig)
|
|
: null,
|
|
terminalConfig: hostData.terminalConfig || null,
|
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
|
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,
|
|
};
|
|
|
|
if (!submitData.enableTunnel) {
|
|
submitData.tunnelConnections = [];
|
|
}
|
|
if (!submitData.enableFileManager) {
|
|
submitData.defaultPath = "";
|
|
}
|
|
|
|
if (hostData.authType === "key" && hostData.key instanceof File) {
|
|
const formData = new FormData();
|
|
formData.append("key", hostData.key);
|
|
|
|
const dataWithoutFile = { ...submitData };
|
|
delete dataWithoutFile.key;
|
|
formData.append("data", JSON.stringify(dataWithoutFile));
|
|
|
|
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
return response.data;
|
|
} else {
|
|
const response = await sshHostApi.put(`/db/host/${hostId}`, submitData);
|
|
return response.data;
|
|
}
|
|
} catch (error) {
|
|
handleApiError(error, "update SSH host");
|
|
}
|
|
}
|
|
|
|
export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{
|
|
message: string;
|
|
success: number;
|
|
failed: number;
|
|
errors: string[];
|
|
}> {
|
|
try {
|
|
const response = await sshHostApi.post("/bulk-import", { hosts });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "bulk import SSH hosts");
|
|
}
|
|
}
|
|
|
|
export async function deleteSSHHost(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete(`/db/host/${hostId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "delete SSH host");
|
|
}
|
|
}
|
|
|
|
export async function getSSHHostById(hostId: number): Promise<SSHHost> {
|
|
try {
|
|
const response = await sshHostApi.get(`/db/host/${hostId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch SSH host");
|
|
}
|
|
}
|
|
|
|
export async function exportSSHHostWithCredentials(
|
|
hostId: number,
|
|
): Promise<SSHHost> {
|
|
try {
|
|
const response = await sshHostApi.get(`/db/host/${hostId}/export`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "export SSH host with credentials");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH AUTOSTART MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function enableAutoStart(
|
|
sshConfigId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post("/autostart/enable", {
|
|
sshConfigId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "enable autostart");
|
|
}
|
|
}
|
|
|
|
export async function disableAutoStart(
|
|
sshConfigId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete("/autostart/disable", {
|
|
data: { sshConfigId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "disable autostart");
|
|
}
|
|
}
|
|
|
|
export async function getAutoStartStatus(): Promise<{
|
|
autostart_configs: Array<{
|
|
sshConfigId: number;
|
|
host: string;
|
|
port: number;
|
|
username: string;
|
|
authType: string;
|
|
}>;
|
|
total_count: number;
|
|
}> {
|
|
try {
|
|
const response = await sshHostApi.get("/autostart/status");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch autostart status");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TUNNEL MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getTunnelStatuses(): Promise<
|
|
Record<string, TunnelStatus>
|
|
> {
|
|
try {
|
|
const response = await tunnelApi.get("/tunnel/status");
|
|
return response.data || {};
|
|
} catch (error) {
|
|
handleApiError(error, "fetch tunnel statuses");
|
|
}
|
|
}
|
|
|
|
export async function getTunnelStatusByName(
|
|
tunnelName: string,
|
|
): Promise<TunnelStatus | undefined> {
|
|
const statuses = await getTunnelStatuses();
|
|
return statuses[tunnelName];
|
|
}
|
|
|
|
export async function connectTunnel(
|
|
tunnelConfig: TunnelConfig,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await tunnelApi.post("/tunnel/connect", tunnelConfig);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "connect tunnel");
|
|
}
|
|
}
|
|
|
|
export async function disconnectTunnel(
|
|
tunnelName: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "disconnect tunnel");
|
|
}
|
|
}
|
|
|
|
export async function cancelTunnel(
|
|
tunnelName: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await tunnelApi.post("/tunnel/cancel", { tunnelName });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "cancel tunnel");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// FILE MANAGER METADATA (Recent, Pinned, Shortcuts)
|
|
// ============================================================================
|
|
|
|
export async function getFileManagerRecent(
|
|
hostId: number,
|
|
): Promise<FileManagerFile[]> {
|
|
try {
|
|
const response = await sshHostApi.get(
|
|
`/file_manager/recent?hostId=${hostId}`,
|
|
);
|
|
return response.data || [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerRecent(
|
|
file: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post("/file_manager/recent", file);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add recent file");
|
|
}
|
|
}
|
|
|
|
export async function removeFileManagerRecent(
|
|
file: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete("/file_manager/recent", {
|
|
data: file,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove recent file");
|
|
}
|
|
}
|
|
|
|
export async function getFileManagerPinned(
|
|
hostId: number,
|
|
): Promise<FileManagerFile[]> {
|
|
try {
|
|
const response = await sshHostApi.get(
|
|
`/file_manager/pinned?hostId=${hostId}`,
|
|
);
|
|
return response.data || [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerPinned(
|
|
file: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post("/file_manager/pinned", file);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add pinned file");
|
|
}
|
|
}
|
|
|
|
export async function removeFileManagerPinned(
|
|
file: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete("/file_manager/pinned", {
|
|
data: file,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove pinned file");
|
|
}
|
|
}
|
|
|
|
export async function getFileManagerShortcuts(
|
|
hostId: number,
|
|
): Promise<FileManagerShortcut[]> {
|
|
try {
|
|
const response = await sshHostApi.get(
|
|
`/file_manager/shortcuts?hostId=${hostId}`,
|
|
);
|
|
return response.data || [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerShortcut(
|
|
shortcut: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post("/file_manager/shortcuts", shortcut);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add shortcut");
|
|
}
|
|
}
|
|
|
|
export async function removeFileManagerShortcut(
|
|
shortcut: FileManagerOperation,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete("/file_manager/shortcuts", {
|
|
data: shortcut,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove shortcut");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH FILE OPERATIONS
|
|
// ============================================================================
|
|
|
|
export async function connectSSH(
|
|
sessionId: string,
|
|
config: {
|
|
hostId?: number;
|
|
ip: string;
|
|
port: number;
|
|
username: string;
|
|
password?: string;
|
|
sshKey?: string;
|
|
keyPassword?: string;
|
|
authType?: string;
|
|
credentialId?: number;
|
|
userId?: string;
|
|
forceKeyboardInteractive?: boolean;
|
|
useSocks5?: boolean;
|
|
socks5Host?: string;
|
|
socks5Port?: number;
|
|
socks5Username?: string;
|
|
socks5Password?: string;
|
|
socks5ProxyChain?: unknown;
|
|
},
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/connect", {
|
|
sessionId,
|
|
...config,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "connect SSH");
|
|
}
|
|
}
|
|
|
|
export async function disconnectSSH(
|
|
sessionId: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/disconnect", {
|
|
sessionId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "disconnect SSH");
|
|
}
|
|
}
|
|
|
|
export async function verifySSHTOTP(
|
|
sessionId: string,
|
|
totpCode: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/connect-totp", {
|
|
sessionId,
|
|
totpCode,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "verify SSH TOTP");
|
|
}
|
|
}
|
|
|
|
export async function getSSHStatus(
|
|
sessionId: string,
|
|
): Promise<{ connected: boolean }> {
|
|
try {
|
|
const response = await fileManagerApi.get("/ssh/status", {
|
|
params: { sessionId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get SSH status");
|
|
}
|
|
}
|
|
|
|
export async function keepSSHAlive(
|
|
sessionId: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/keepalive", {
|
|
sessionId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "SSH keepalive");
|
|
}
|
|
}
|
|
|
|
export async function listSSHFiles(
|
|
sessionId: string,
|
|
path: string,
|
|
): Promise<{ files: unknown[]; path: string }> {
|
|
try {
|
|
const response = await fileManagerApi.get("/ssh/listFiles", {
|
|
params: { sessionId, path },
|
|
});
|
|
return response.data || { files: [], path };
|
|
} catch (error) {
|
|
handleApiError(error, "list SSH files");
|
|
return { files: [], path };
|
|
}
|
|
}
|
|
|
|
export async function identifySSHSymlink(
|
|
sessionId: string,
|
|
path: string,
|
|
): Promise<{ path: string; target: string; type: "directory" | "file" }> {
|
|
try {
|
|
const response = await fileManagerApi.get("/ssh/identifySymlink", {
|
|
params: { sessionId, path },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "identify SSH symlink");
|
|
}
|
|
}
|
|
|
|
export async function readSSHFile(
|
|
sessionId: string,
|
|
path: string,
|
|
): Promise<{ content: string; path: string }> {
|
|
try {
|
|
const response = await fileManagerApi.get("/ssh/readFile", {
|
|
params: { sessionId, path },
|
|
});
|
|
return response.data;
|
|
} catch (error: unknown) {
|
|
if (error.response?.status === 404) {
|
|
const customError = new Error("File not found");
|
|
(
|
|
customError as Error & { response?: unknown; isFileNotFound?: boolean }
|
|
).response = error.response;
|
|
(
|
|
customError as Error & { response?: unknown; isFileNotFound?: boolean }
|
|
).isFileNotFound = error.response.data?.fileNotFound || true;
|
|
throw customError;
|
|
}
|
|
handleApiError(error, "read SSH file");
|
|
}
|
|
}
|
|
|
|
export async function writeSSHFile(
|
|
sessionId: string,
|
|
path: string,
|
|
content: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/writeFile", {
|
|
sessionId,
|
|
path,
|
|
content,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
if (
|
|
response.data &&
|
|
(response.data.message === "File written successfully" ||
|
|
response.status === 200)
|
|
) {
|
|
return response.data;
|
|
} else {
|
|
throw new Error("File write operation did not return success status");
|
|
}
|
|
} catch (error) {
|
|
handleApiError(error, "write SSH file");
|
|
}
|
|
}
|
|
|
|
export async function uploadSSHFile(
|
|
sessionId: string,
|
|
path: string,
|
|
fileName: string,
|
|
content: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/uploadFile", {
|
|
sessionId,
|
|
path,
|
|
fileName,
|
|
content,
|
|
hostId,
|
|
userId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "upload SSH file");
|
|
}
|
|
}
|
|
|
|
export async function downloadSSHFile(
|
|
sessionId: string,
|
|
filePath: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/downloadFile", {
|
|
sessionId,
|
|
path: filePath,
|
|
hostId,
|
|
userId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "download SSH file");
|
|
}
|
|
}
|
|
|
|
export async function createSSHFile(
|
|
sessionId: string,
|
|
path: string,
|
|
fileName: string,
|
|
content: string = "",
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/createFile", {
|
|
sessionId,
|
|
path,
|
|
fileName,
|
|
content,
|
|
hostId,
|
|
userId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "create SSH file");
|
|
}
|
|
}
|
|
|
|
export async function createSSHFolder(
|
|
sessionId: string,
|
|
path: string,
|
|
folderName: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/createFolder", {
|
|
sessionId,
|
|
path,
|
|
folderName,
|
|
hostId,
|
|
userId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "create SSH folder");
|
|
}
|
|
}
|
|
|
|
export async function deleteSSHItem(
|
|
sessionId: string,
|
|
path: string,
|
|
isDirectory: boolean,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.delete("/ssh/deleteItem", {
|
|
data: {
|
|
sessionId,
|
|
path,
|
|
isDirectory,
|
|
hostId,
|
|
userId,
|
|
},
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "delete SSH item");
|
|
}
|
|
}
|
|
|
|
export async function copySSHItem(
|
|
sessionId: string,
|
|
sourcePath: string,
|
|
targetDir: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.post(
|
|
"/ssh/copyItem",
|
|
{
|
|
sessionId,
|
|
sourcePath,
|
|
targetDir,
|
|
hostId,
|
|
userId,
|
|
},
|
|
{
|
|
timeout: 60000,
|
|
},
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "copy SSH item");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function renameSSHItem(
|
|
sessionId: string,
|
|
oldPath: string,
|
|
newName: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.put("/ssh/renameItem", {
|
|
sessionId,
|
|
oldPath,
|
|
newName,
|
|
hostId,
|
|
userId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "rename SSH item");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function moveSSHItem(
|
|
sessionId: string,
|
|
oldPath: string,
|
|
newPath: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await fileManagerApi.put(
|
|
"/ssh/moveItem",
|
|
{
|
|
sessionId,
|
|
oldPath,
|
|
newPath,
|
|
hostId,
|
|
userId,
|
|
},
|
|
{
|
|
timeout: 60000,
|
|
},
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "move SSH item");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function changeSSHPermissions(
|
|
sessionId: string,
|
|
path: string,
|
|
permissions: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
fileLogger.info("Changing SSH file permissions", {
|
|
operation: "change_permissions",
|
|
sessionId,
|
|
path,
|
|
permissions,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
const response = await fileManagerApi.post("/ssh/changePermissions", {
|
|
sessionId,
|
|
path,
|
|
permissions,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
fileLogger.success("SSH file permissions changed successfully", {
|
|
operation: "change_permissions",
|
|
sessionId,
|
|
path,
|
|
permissions,
|
|
});
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
fileLogger.error("Failed to change SSH file permissions", error, {
|
|
operation: "change_permissions",
|
|
sessionId,
|
|
path,
|
|
permissions,
|
|
});
|
|
handleApiError(error, "change SSH permissions");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function extractSSHArchive(
|
|
sessionId: string,
|
|
archivePath: string,
|
|
extractPath?: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<{ success: boolean; message: string; extractPath: string }> {
|
|
try {
|
|
fileLogger.info("Extracting archive", {
|
|
operation: "extract_archive",
|
|
sessionId,
|
|
archivePath,
|
|
extractPath,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
const response = await fileManagerApi.post("/ssh/extractArchive", {
|
|
sessionId,
|
|
archivePath,
|
|
extractPath,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
fileLogger.success("Archive extracted successfully", {
|
|
operation: "extract_archive",
|
|
sessionId,
|
|
archivePath,
|
|
extractPath: response.data.extractPath,
|
|
});
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
fileLogger.error("Failed to extract archive", error, {
|
|
operation: "extract_archive",
|
|
sessionId,
|
|
archivePath,
|
|
extractPath,
|
|
});
|
|
handleApiError(error, "extract archive");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function compressSSHFiles(
|
|
sessionId: string,
|
|
paths: string[],
|
|
archiveName: string,
|
|
format?: string,
|
|
hostId?: number,
|
|
userId?: string,
|
|
): Promise<{ success: boolean; message: string; archivePath: string }> {
|
|
try {
|
|
fileLogger.info("Compressing files", {
|
|
operation: "compress_files",
|
|
sessionId,
|
|
paths,
|
|
archiveName,
|
|
format,
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
const response = await fileManagerApi.post("/ssh/compressFiles", {
|
|
sessionId,
|
|
paths,
|
|
archiveName,
|
|
format: format || "zip",
|
|
hostId,
|
|
userId,
|
|
});
|
|
|
|
fileLogger.success("Files compressed successfully", {
|
|
operation: "compress_files",
|
|
sessionId,
|
|
paths,
|
|
archivePath: response.data.archivePath,
|
|
});
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
fileLogger.error("Failed to compress files", error, {
|
|
operation: "compress_files",
|
|
sessionId,
|
|
paths,
|
|
archiveName,
|
|
format,
|
|
});
|
|
handleApiError(error, "compress files");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// FILE MANAGER DATA
|
|
// ============================================================================
|
|
|
|
export async function getRecentFiles(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/ssh/file_manager/recent", {
|
|
params: { hostId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get recent files");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function addRecentFile(
|
|
hostId: number,
|
|
path: string,
|
|
name?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/ssh/file_manager/recent", {
|
|
hostId,
|
|
path,
|
|
name,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add recent file");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function removeRecentFile(
|
|
hostId: number,
|
|
path: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/ssh/file_manager/recent", {
|
|
data: { hostId, path },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove recent file");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getPinnedFiles(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/ssh/file_manager/pinned", {
|
|
params: { hostId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get pinned files");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function addPinnedFile(
|
|
hostId: number,
|
|
path: string,
|
|
name?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/ssh/file_manager/pinned", {
|
|
hostId,
|
|
path,
|
|
name,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add pinned file");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function removePinnedFile(
|
|
hostId: number,
|
|
path: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/ssh/file_manager/pinned", {
|
|
data: { hostId, path },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove pinned file");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getFolderShortcuts(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/ssh/file_manager/shortcuts", {
|
|
params: { hostId },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get folder shortcuts");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function addFolderShortcut(
|
|
hostId: number,
|
|
path: string,
|
|
name?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/ssh/file_manager/shortcuts", {
|
|
hostId,
|
|
path,
|
|
name,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "add folder shortcut");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function removeFolderShortcut(
|
|
hostId: number,
|
|
path: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/ssh/file_manager/shortcuts", {
|
|
data: { hostId, path },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove folder shortcut");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SERVER STATISTICS
|
|
// ============================================================================
|
|
|
|
export async function getAllServerStatuses(): Promise<
|
|
Record<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> {
|
|
try {
|
|
const response = await statsApi.get(`/status/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch server status");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getServerMetricsById(id: number): Promise<ServerMetrics> {
|
|
try {
|
|
const response = await statsApi.get(`/metrics/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch server metrics");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function startMetricsPolling(hostId: number): Promise<{
|
|
success: boolean;
|
|
requires_totp?: boolean;
|
|
sessionId?: string;
|
|
prompt?: string;
|
|
}> {
|
|
try {
|
|
const response = await statsApi.post(`/metrics/start/${hostId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "start metrics polling");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function stopMetricsPolling(hostId: number): Promise<void> {
|
|
try {
|
|
await statsApi.post(`/metrics/stop/${hostId}`);
|
|
} catch (error) {
|
|
handleApiError(error, "stop metrics polling");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function submitMetricsTOTP(
|
|
sessionId: string,
|
|
totpCode: string,
|
|
): Promise<{
|
|
success: boolean;
|
|
}> {
|
|
try {
|
|
const response = await statsApi.post("/metrics/connect-totp", {
|
|
sessionId,
|
|
totpCode,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "submit metrics TOTP");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function refreshServerPolling(): Promise<void> {
|
|
try {
|
|
await statsApi.post("/refresh");
|
|
} catch (error) {
|
|
console.warn("Failed to refresh server polling:", error);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTHENTICATION
|
|
// ============================================================================
|
|
|
|
export async function registerUser(
|
|
username: string,
|
|
password: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/create", {
|
|
username,
|
|
password,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "register user");
|
|
}
|
|
}
|
|
|
|
export async function loginUser(
|
|
username: string,
|
|
password: string,
|
|
): Promise<AuthResponse> {
|
|
try {
|
|
const response = await authApi.post("/users/login", { username, password });
|
|
|
|
const hasToken = response.data.token;
|
|
|
|
if (isElectron() && hasToken) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
}
|
|
|
|
const isInIframe =
|
|
typeof window !== "undefined" && window.self !== window.top;
|
|
|
|
if (isInIframe && hasToken) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
|
|
try {
|
|
window.parent.postMessage(
|
|
{
|
|
type: "AUTH_SUCCESS",
|
|
token: response.data.token,
|
|
source: "login_api",
|
|
platform: "desktop",
|
|
timestamp: Date.now(),
|
|
},
|
|
"*",
|
|
);
|
|
} catch (e) {
|
|
console.error("[main-axios] Error posting message to parent:", e);
|
|
}
|
|
}
|
|
|
|
return {
|
|
token: response.data.token || "cookie-based",
|
|
success: response.data.success,
|
|
is_admin: response.data.is_admin,
|
|
username: response.data.username,
|
|
requires_totp: response.data.requires_totp,
|
|
temp_token: response.data.temp_token,
|
|
};
|
|
} catch (error) {
|
|
handleApiError(error, "login user");
|
|
}
|
|
}
|
|
|
|
export async function logoutUser(): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
const response = await authApi.post("/users/logout");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "logout user");
|
|
}
|
|
}
|
|
|
|
export async function getUserInfo(): Promise<UserInfo> {
|
|
try {
|
|
const response = await authApi.get("/users/me");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user info");
|
|
}
|
|
}
|
|
|
|
export async function unlockUserData(
|
|
password: string,
|
|
): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
const response = await authApi.post("/users/unlock-data", { password });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "unlock user data");
|
|
}
|
|
}
|
|
|
|
export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/registration-allowed");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check registration status");
|
|
}
|
|
}
|
|
|
|
export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/password-login-allowed");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check password login status");
|
|
}
|
|
}
|
|
|
|
export async function getOIDCConfig(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc-config");
|
|
return response.data;
|
|
} catch (error: unknown) {
|
|
console.warn(
|
|
"Failed to fetch OIDC config:",
|
|
error.response?.data?.error || error.message,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getAdminOIDCConfig(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc-config/admin");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch admin OIDC config");
|
|
}
|
|
}
|
|
|
|
export async function getSetupRequired(): Promise<{ setup_required: boolean }> {
|
|
try {
|
|
const response = await authApi.get("/users/setup-required");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check setup status");
|
|
}
|
|
}
|
|
|
|
export async function getUserCount(): Promise<UserCount> {
|
|
try {
|
|
const response = await authApi.get("/users/count");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user count");
|
|
}
|
|
}
|
|
|
|
export async function initiatePasswordReset(
|
|
username: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/initiate-reset", { username });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "initiate password reset");
|
|
}
|
|
}
|
|
|
|
export async function verifyPasswordResetCode(
|
|
username: string,
|
|
resetCode: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/verify-reset-code", {
|
|
username,
|
|
resetCode,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "verify reset code");
|
|
}
|
|
}
|
|
|
|
export async function completePasswordReset(
|
|
username: string,
|
|
tempToken: string,
|
|
newPassword: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/complete-reset", {
|
|
username,
|
|
tempToken,
|
|
newPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "complete password reset");
|
|
}
|
|
}
|
|
|
|
export async function changePassword(oldPassword: string, newPassword: string) {
|
|
try {
|
|
const response = await authApi.post("/users/change-password", {
|
|
oldPassword,
|
|
newPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "change password");
|
|
}
|
|
}
|
|
|
|
export async function getOIDCAuthorizeUrl(): Promise<OIDCAuthorize> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc/authorize");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "get OIDC authorize URL");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getUserList(): Promise<{ users: UserInfo[] }> {
|
|
try {
|
|
const response = await authApi.get("/users/list");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user list");
|
|
}
|
|
}
|
|
|
|
export async function 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>> {
|
|
try {
|
|
const response = await authApi.post("/users/make-admin", { username });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "make user admin");
|
|
}
|
|
}
|
|
|
|
export async function removeAdminStatus(
|
|
username: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/remove-admin", { username });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "remove admin status");
|
|
}
|
|
}
|
|
|
|
export async function deleteUser(
|
|
username: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/users/delete-user", {
|
|
data: { username },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "delete user");
|
|
}
|
|
}
|
|
|
|
export async function deleteAccount(
|
|
password: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/users/delete-account", {
|
|
data: { password },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "delete account");
|
|
}
|
|
}
|
|
|
|
export async function updateRegistrationAllowed(
|
|
allowed: boolean,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.patch("/users/registration-allowed", {
|
|
allowed,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "update registration allowed");
|
|
}
|
|
}
|
|
|
|
export async function updatePasswordLoginAllowed(
|
|
allowed: boolean,
|
|
): Promise<{ allowed: boolean }> {
|
|
try {
|
|
const response = await authApi.patch("/users/password-login-allowed", {
|
|
allowed,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "update password login allowed");
|
|
}
|
|
}
|
|
|
|
export async function updateOIDCConfig(
|
|
config: Record<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/users/oidc-config", config);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "update OIDC config");
|
|
}
|
|
}
|
|
|
|
export async function disableOIDCConfig(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete("/users/oidc-config");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "disable OIDC config");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ALERTS
|
|
// ============================================================================
|
|
|
|
export async function setupTOTP(): Promise<{
|
|
secret: string;
|
|
qr_code: string;
|
|
}> {
|
|
try {
|
|
const response = await authApi.post("/users/totp/setup");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error as AxiosError, "setup TOTP");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function enableTOTP(
|
|
totp_code: string,
|
|
): Promise<{ message: string; backup_codes: string[] }> {
|
|
try {
|
|
const response = await authApi.post("/users/totp/enable", { totp_code });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error as AxiosError, "enable TOTP");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function disableTOTP(
|
|
password?: string,
|
|
totp_code?: string,
|
|
): Promise<{ message: string }> {
|
|
try {
|
|
const response = await authApi.post("/users/totp/disable", {
|
|
password,
|
|
totp_code,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error as AxiosError, "disable TOTP");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function verifyTOTPLogin(
|
|
temp_token: string,
|
|
totp_code: string,
|
|
): Promise<AuthResponse> {
|
|
try {
|
|
const response = await authApi.post("/users/totp/verify-login", {
|
|
temp_token,
|
|
totp_code,
|
|
});
|
|
|
|
const hasToken = response.data.token;
|
|
|
|
if (isElectron() && hasToken) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
}
|
|
|
|
const isInIframe =
|
|
typeof window !== "undefined" && window.self !== window.top;
|
|
|
|
if (isInIframe && hasToken) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
|
|
try {
|
|
window.parent.postMessage(
|
|
{
|
|
type: "AUTH_SUCCESS",
|
|
token: response.data.token,
|
|
source: "totp_verify",
|
|
platform: "desktop",
|
|
timestamp: Date.now(),
|
|
},
|
|
"*",
|
|
);
|
|
} catch (e) {
|
|
console.error("[main-axios] Error posting message to parent:", e);
|
|
}
|
|
}
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error as AxiosError, "verify TOTP login");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function generateBackupCodes(
|
|
password?: string,
|
|
totp_code?: string,
|
|
): Promise<{ backup_codes: string[] }> {
|
|
try {
|
|
const response = await authApi.post("/users/totp/backup-codes", {
|
|
password,
|
|
totp_code,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error as AxiosError, "generate backup codes");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getUserAlerts(): Promise<{
|
|
alerts: Array<Record<string, unknown>>;
|
|
}> {
|
|
try {
|
|
const response = await authApi.get(`/alerts`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user alerts");
|
|
}
|
|
}
|
|
|
|
export async function dismissAlert(
|
|
alertId: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/alerts/dismiss", { alertId });
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "dismiss alert");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// UPDATES & RELEASES
|
|
// ============================================================================
|
|
|
|
export async function getReleasesRSS(
|
|
perPage: number = 100,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get(`/releases/rss?per_page=${perPage}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch releases RSS");
|
|
}
|
|
}
|
|
|
|
export async function getVersionInfo(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/version");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch version info");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DATABASE HEALTH
|
|
// ============================================================================
|
|
|
|
export async function getDatabaseHealth(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/health");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check database health");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH CREDENTIALS MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getCredentials(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/credentials");
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch credentials");
|
|
}
|
|
}
|
|
|
|
export async function getCredentialDetails(
|
|
credentialId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get(`/credentials/${credentialId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch credential details");
|
|
}
|
|
}
|
|
|
|
export async function createCredential(
|
|
credentialData: Record<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials", credentialData);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "create credential");
|
|
}
|
|
}
|
|
|
|
export async function updateCredential(
|
|
credentialId: number,
|
|
credentialData: Record<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.put(
|
|
`/credentials/${credentialId}`,
|
|
credentialData,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "update credential");
|
|
}
|
|
}
|
|
|
|
export async function deleteCredential(
|
|
credentialId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.delete(`/credentials/${credentialId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "delete credential");
|
|
}
|
|
}
|
|
|
|
export async function getCredentialHosts(
|
|
credentialId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get(`/credentials/${credentialId}/hosts`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch credential hosts");
|
|
}
|
|
}
|
|
|
|
export async function getCredentialFolders(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/credentials/folders");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch credential folders");
|
|
}
|
|
}
|
|
|
|
export async function getSSHHostWithCredentials(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.get(
|
|
`/db/host/${hostId}/with-credentials`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch SSH host with credentials");
|
|
}
|
|
}
|
|
|
|
export async function applyCredentialToHost(
|
|
hostId: number,
|
|
credentialId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post(
|
|
`/db/host/${hostId}/apply-credential`,
|
|
{ credentialId },
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "apply credential to host");
|
|
}
|
|
}
|
|
|
|
export async function removeCredentialFromHost(
|
|
hostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.delete(`/db/host/${hostId}/credential`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "remove credential from host");
|
|
}
|
|
}
|
|
|
|
export async function migrateHostToCredential(
|
|
hostId: number,
|
|
credentialName: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await sshHostApi.post(
|
|
`/db/host/${hostId}/migrate-to-credential`,
|
|
{ credentialName },
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "migrate host to credential");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH FOLDER MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getFoldersWithStats(): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.get("/ssh/db/folders/with-stats");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch folders with statistics");
|
|
}
|
|
}
|
|
|
|
export async function renameFolder(
|
|
oldName: string,
|
|
newName: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.put("/ssh/folders/rename", {
|
|
oldName,
|
|
newName,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "rename folder");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
export async function renameCredentialFolder(
|
|
oldName: string,
|
|
newName: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.put("/credentials/folders/rename", {
|
|
oldName,
|
|
newName,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "rename credential folder");
|
|
}
|
|
}
|
|
|
|
export async function detectKeyType(
|
|
privateKey: string,
|
|
keyPassword?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials/detect-key-type", {
|
|
privateKey,
|
|
keyPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "detect key type");
|
|
}
|
|
}
|
|
|
|
export async function detectPublicKeyType(
|
|
publicKey: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials/detect-public-key-type", {
|
|
publicKey,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "detect public key type");
|
|
}
|
|
}
|
|
|
|
export async function validateKeyPair(
|
|
privateKey: string,
|
|
publicKey: string,
|
|
keyPassword?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials/validate-key-pair", {
|
|
privateKey,
|
|
publicKey,
|
|
keyPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "validate key pair");
|
|
}
|
|
}
|
|
|
|
export async function generatePublicKeyFromPrivate(
|
|
privateKey: string,
|
|
keyPassword?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials/generate-public-key", {
|
|
privateKey,
|
|
keyPassword,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "generate public key from private key");
|
|
}
|
|
}
|
|
|
|
export async function generateKeyPair(
|
|
keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256",
|
|
keySize?: number,
|
|
passphrase?: string,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post("/credentials/generate-key-pair", {
|
|
keyType,
|
|
keySize,
|
|
passphrase,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "generate SSH key pair");
|
|
}
|
|
}
|
|
|
|
export async function deployCredentialToHost(
|
|
credentialId: number,
|
|
targetHostId: number,
|
|
): Promise<Record<string, unknown>> {
|
|
try {
|
|
const response = await authApi.post(
|
|
`/credentials/${credentialId}/deploy-to-host`,
|
|
{ targetHostId },
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "deploy credential to host");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SNIPPETS API
|
|
// ============================================================================
|
|
|
|
export async function getSnippets(): Promise<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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
export async function reorderSnippets(
|
|
snippets: Array<{ id: number; order: number; folder?: string }>,
|
|
): Promise<{ success: boolean; updated: number }> {
|
|
try {
|
|
const response = await authApi.put("/snippets/reorder", { snippets });
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "reorder snippets");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOMEPAGE API
|
|
// ============================================================================
|
|
|
|
export interface UptimeInfo {
|
|
uptimeMs: number;
|
|
uptimeSeconds: number;
|
|
formatted: string;
|
|
}
|
|
|
|
export interface RecentActivityItem {
|
|
id: number;
|
|
userId: string;
|
|
type: "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker";
|
|
hostId: number;
|
|
hostName: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
export async function getUptime(): Promise<UptimeInfo> {
|
|
try {
|
|
const response = await homepageApi.get("/uptime");
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch uptime");
|
|
}
|
|
}
|
|
|
|
export async function getRecentActivity(
|
|
limit?: number,
|
|
): Promise<RecentActivityItem[]> {
|
|
try {
|
|
const response = await homepageApi.get("/activity/recent", {
|
|
params: { limit },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch recent activity");
|
|
}
|
|
}
|
|
|
|
export async function logActivity(
|
|
type: "terminal" | "file_manager" | "server_stats" | "tunnel" | "docker",
|
|
hostId: number,
|
|
hostName: string,
|
|
): Promise<{ message: string; id: number | string }> {
|
|
try {
|
|
const response = await homepageApi.post("/activity/log", {
|
|
type,
|
|
hostId,
|
|
hostName,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "log activity");
|
|
}
|
|
}
|
|
|
|
export async function resetRecentActivity(): Promise<{ message: string }> {
|
|
try {
|
|
const response = await homepageApi.delete("/activity/reset");
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "reset recent activity");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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;
|
|
}> {
|
|
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;
|
|
}
|
|
throw handleApiError(error, "connect to Docker SSH session");
|
|
}
|
|
}
|
|
|
|
export async function verifyDockerTOTP(
|
|
sessionId: string,
|
|
totpCode: string,
|
|
): Promise<{ status: string; message: string }> {
|
|
try {
|
|
const response = await dockerApi.post("/ssh/connect-totp", {
|
|
sessionId,
|
|
totpCode,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "verify Docker TOTP");
|
|
}
|
|
}
|
|
|
|
export async function 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");
|
|
}
|
|
}
|