2158 lines
54 KiB
TypeScript
2158 lines
54 KiB
TypeScript
import axios, { AxiosError, type AxiosInstance } from "axios";
|
|
import type {
|
|
SSHHost,
|
|
SSHHostData,
|
|
TunnelConfig,
|
|
TunnelStatus,
|
|
Credential,
|
|
CredentialData,
|
|
HostInfo,
|
|
ApiResponse,
|
|
FileManagerFile,
|
|
FileManagerShortcut,
|
|
} from "../types/index.js";
|
|
import {
|
|
apiLogger,
|
|
authLogger,
|
|
sshLogger,
|
|
tunnelLogger,
|
|
fileLogger,
|
|
statsLogger,
|
|
systemLogger,
|
|
type LogContext,
|
|
} from "../lib/frontend-logger.js";
|
|
|
|
interface FileManagerOperation {
|
|
name: string;
|
|
path: string;
|
|
isSSH: boolean;
|
|
sshSessionId?: string;
|
|
hostId: number;
|
|
}
|
|
|
|
export type ServerStatus = {
|
|
status: "online" | "offline";
|
|
lastChecked: string;
|
|
};
|
|
|
|
interface CpuMetrics {
|
|
percent: number | null;
|
|
cores: number | null;
|
|
load: [number, number, number] | null;
|
|
}
|
|
|
|
interface MemoryMetrics {
|
|
percent: number | null;
|
|
usedGiB: number | null;
|
|
totalGiB: number | null;
|
|
}
|
|
|
|
interface DiskMetrics {
|
|
percent: number | null;
|
|
usedHuman: string | null;
|
|
totalHuman: string | null;
|
|
availableHuman?: string | null;
|
|
}
|
|
|
|
export type ServerMetrics = {
|
|
cpu: CpuMetrics;
|
|
memory: MemoryMetrics;
|
|
disk: DiskMetrics;
|
|
lastChecked: string;
|
|
};
|
|
|
|
interface AuthResponse {
|
|
token: string;
|
|
success?: boolean;
|
|
is_admin?: boolean;
|
|
username?: string;
|
|
userId?: string;
|
|
is_oidc?: boolean;
|
|
totp_enabled?: boolean;
|
|
data_unlocked?: boolean;
|
|
}
|
|
|
|
interface UserInfo {
|
|
totp_enabled: boolean;
|
|
userId: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
is_oidc: boolean;
|
|
data_unlocked: boolean;
|
|
}
|
|
|
|
interface UserCount {
|
|
count: number;
|
|
}
|
|
|
|
interface OIDCAuthorize {
|
|
auth_url: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
export function isElectron(): boolean {
|
|
return (
|
|
(window as any).IS_ELECTRON === true ||
|
|
(window as any).electronAPI?.isElectron === true
|
|
);
|
|
}
|
|
|
|
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 {
|
|
return apiLogger;
|
|
}
|
|
}
|
|
|
|
export function setCookie(name: string, value: string, days = 7): void {
|
|
if (isElectron()) {
|
|
localStorage.setItem(name, value);
|
|
} else {
|
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
|
}
|
|
}
|
|
|
|
export function getCookie(name: string): string | undefined {
|
|
if (isElectron()) {
|
|
const token = localStorage.getItem(name) || undefined;
|
|
return token;
|
|
} else {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
const encodedToken =
|
|
parts.length === 2 ? parts.pop()?.split(";").shift() : undefined;
|
|
const token = encodedToken ? decodeURIComponent(encodedToken) : undefined;
|
|
return token;
|
|
}
|
|
}
|
|
|
|
function createApiInstance(
|
|
baseURL: string,
|
|
serviceName: string = "API",
|
|
): AxiosInstance {
|
|
const instance = axios.create({
|
|
baseURL,
|
|
headers: { "Content-Type": "application/json" },
|
|
timeout: 30000,
|
|
withCredentials: true,
|
|
});
|
|
|
|
instance.interceptors.request.use((config) => {
|
|
const startTime = performance.now();
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
(config as any).startTime = startTime;
|
|
(config as any).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}`;
|
|
}
|
|
}
|
|
|
|
return config;
|
|
});
|
|
|
|
instance.interceptors.response.use(
|
|
(response) => {
|
|
const endTime = performance.now();
|
|
const startTime = (response.config as any).startTime;
|
|
const requestId = (response.config as any).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 any)?.startTime;
|
|
const requestId = (error.config as any)?.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 any)?.error ||
|
|
(error as Error).message ||
|
|
"Unknown error";
|
|
const errorCode = (error.response?.data as any)?.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 any)?.code;
|
|
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
|
|
|
if (isElectron()) {
|
|
localStorage.removeItem("jwt");
|
|
} else {
|
|
localStorage.removeItem("jwt");
|
|
}
|
|
|
|
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");
|
|
});
|
|
|
|
setTimeout(() => window.location.reload(), 100);
|
|
}
|
|
}
|
|
|
|
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")
|
|
);
|
|
}
|
|
|
|
let apiHost = import.meta.env.VITE_API_HOST || "localhost";
|
|
let apiPort = 30001;
|
|
let configuredServerUrl: string | null = null;
|
|
|
|
if (isElectron()) {
|
|
apiPort = 30001;
|
|
}
|
|
|
|
export interface ServerConfig {
|
|
serverUrl: string;
|
|
lastUpdated: string;
|
|
}
|
|
|
|
export async function getServerConfig(): Promise<ServerConfig | null> {
|
|
if (!isElectron()) return null;
|
|
|
|
try {
|
|
const result = await (window as any).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 any).electronAPI?.invoke(
|
|
"save-server-config",
|
|
config,
|
|
);
|
|
if (result?.success) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(window as any).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 any).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 any).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 {
|
|
if (isDev()) {
|
|
const protocol = window.location.protocol === "https:" ? "https" : "http";
|
|
const sslPort = protocol === "https" ? 8443 : defaultPort;
|
|
return `${protocol}://${apiHost}:${sslPort}${path}`;
|
|
} else if (isElectron()) {
|
|
if (configuredServerUrl) {
|
|
const baseUrl = configuredServerUrl.replace(/\/$/, "");
|
|
return `${baseUrl}${path}`;
|
|
}
|
|
return "http://no-server-configured";
|
|
} 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");
|
|
}
|
|
|
|
// 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;
|
|
|
|
if (isElectron()) {
|
|
getServerConfig()
|
|
.then((config) => {
|
|
if (config?.serverUrl) {
|
|
configuredServerUrl = config.serverUrl;
|
|
(window as any).configuredServerUrl = configuredServerUrl;
|
|
}
|
|
initializeApiInstances();
|
|
})
|
|
.catch((error) => {
|
|
console.error(
|
|
"Failed to load server config, initializing with default:",
|
|
error,
|
|
);
|
|
initializeApiInstances();
|
|
});
|
|
} else {
|
|
initializeApiInstances();
|
|
}
|
|
|
|
function updateApiInstances() {
|
|
systemLogger.info("Updating API instances with new server configuration", {
|
|
operation: "api_instance_update",
|
|
configuredServerUrl,
|
|
});
|
|
|
|
initializeApiInstances();
|
|
|
|
(window as any).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?.error || error.message;
|
|
const code = error.response?.data?.code;
|
|
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);
|
|
throw new ApiError(
|
|
"Access denied. You do not have permission to perform this action.",
|
|
403,
|
|
"ACCESS_DENIED",
|
|
);
|
|
} 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,
|
|
enableTerminal: Boolean(hostData.enableTerminal),
|
|
enableTunnel: Boolean(hostData.enableTunnel),
|
|
enableFileManager: Boolean(hostData.enableFileManager),
|
|
defaultPath: hostData.defaultPath || "/",
|
|
tunnelConnections: hostData.tunnelConnections || [],
|
|
};
|
|
|
|
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,
|
|
enableTerminal: Boolean(hostData.enableTerminal),
|
|
enableTunnel: Boolean(hostData.enableTunnel),
|
|
enableFileManager: Boolean(hostData.enableFileManager),
|
|
defaultPath: hostData.defaultPath || "/",
|
|
tunnelConnections: hostData.tunnelConnections || [],
|
|
};
|
|
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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 (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerRecent(
|
|
file: FileManagerOperation,
|
|
): Promise<any> {
|
|
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<any> {
|
|
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 (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerPinned(
|
|
file: FileManagerOperation,
|
|
): Promise<any> {
|
|
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<any> {
|
|
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 (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function addFileManagerShortcut(
|
|
shortcut: FileManagerOperation,
|
|
): Promise<any> {
|
|
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<any> {
|
|
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;
|
|
},
|
|
): Promise<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await fileManagerApi.post("/ssh/disconnect", {
|
|
sessionId,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "disconnect SSH");
|
|
}
|
|
}
|
|
|
|
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<any> {
|
|
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: any[]; 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: any) {
|
|
if (error.response?.status === 404) {
|
|
const customError = new Error("File not found");
|
|
(customError as any).response = error.response;
|
|
(customError as any).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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await fileManagerApi.put(
|
|
"/ssh/moveItem",
|
|
{
|
|
sessionId,
|
|
oldPath,
|
|
newPath,
|
|
hostId,
|
|
userId,
|
|
},
|
|
{
|
|
timeout: 60000,
|
|
},
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "move SSH item");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// FILE MANAGER DATA
|
|
// ============================================================================
|
|
|
|
// Recent Files
|
|
export async function getRecentFiles(hostId: number): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTHENTICATION
|
|
// ============================================================================
|
|
|
|
export async function registerUser(
|
|
username: string,
|
|
password: string,
|
|
): Promise<any> {
|
|
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 });
|
|
|
|
if (isElectron() && response.data.token) {
|
|
localStorage.setItem("jwt", response.data.token);
|
|
}
|
|
|
|
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 getOIDCConfig(): Promise<any> {
|
|
try {
|
|
const response = await authApi.get("/users/oidc-config");
|
|
return response.data;
|
|
} catch (error: any) {
|
|
console.warn(
|
|
"Failed to fetch OIDC config:",
|
|
error.response?.data?.error || error.message,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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 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 makeUserAdmin(username: string): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await authApi.patch("/users/registration-allowed", {
|
|
allowed,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "update registration allowed");
|
|
}
|
|
}
|
|
|
|
export async function updateOIDCConfig(config: any): Promise<any> {
|
|
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<any> {
|
|
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,
|
|
});
|
|
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: any[] }> {
|
|
try {
|
|
const response = await authApi.get(`/alerts`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch user alerts");
|
|
}
|
|
}
|
|
|
|
export async function dismissAlert(alertId: string): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await authApi.get("/version");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch version info");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DATABASE HEALTH
|
|
// ============================================================================
|
|
|
|
export async function getDatabaseHealth(): Promise<any> {
|
|
try {
|
|
const response = await authApi.get("/users/db-health");
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "check database health");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SSH CREDENTIALS MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getCredentials(): Promise<any> {
|
|
try {
|
|
const response = await authApi.get("/credentials");
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch credentials");
|
|
}
|
|
}
|
|
|
|
export async function getCredentialDetails(credentialId: number): Promise<any> {
|
|
try {
|
|
const response = await authApi.get(`/credentials/${credentialId}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "fetch credential details");
|
|
}
|
|
}
|
|
|
|
export async function createCredential(credentialData: any): Promise<any> {
|
|
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: any,
|
|
): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await authApi.get(`/credentials/${credentialId}/hosts`);
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "fetch credential hosts");
|
|
}
|
|
}
|
|
|
|
export async function getCredentialFolders(): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await authApi.put("/ssh/folders/rename", {
|
|
oldName,
|
|
newName,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
handleApiError(error, "rename folder");
|
|
}
|
|
}
|
|
|
|
export async function renameCredentialFolder(
|
|
oldName: string,
|
|
newName: string,
|
|
): Promise<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
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<any> {
|
|
try {
|
|
const response = await authApi.post(
|
|
`/credentials/${credentialId}/deploy-to-host`,
|
|
{ targetHostId },
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleApiError(error, "deploy credential to host");
|
|
}
|
|
}
|