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

1350 lines
38 KiB
TypeScript
Raw Normal View History

+5
2026-08-19 14:12:06 -05:00
import { getErrorMessage } from "./lib/error-message.js";
import axios, {
AxiosError,
type AxiosInstance,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from "axios";
import { toast } from "sonner";
2026-10-01 16:20:16 -05:00
import i18n from "i18next";
+4
2026-02-12 22:28:13 -06:00
import { getBasePath } from "@/lib/base-path";
2026-10-18 00:34:50 -05:00
import { isElectron, localBackendPort } from "@/lib/electron";
2026-05-11 01:23:11 -05:00
import { clearTermixSessionStorage } from "@/shell/TabContext";
2026-06-04 15:16:53 -04:00
import type { SSHHost } from "@/types/index";
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// RBAC TYPE DEFINITIONS
// ============================================================================
export interface Role {
id: number;
name: string;
displayName: string;
description: string | null;
isSystem: boolean;
+8
2026-07-19 12:29:52 -05:00
permissions: string[] | string | null;
+1
2025-12-31 22:20:12 -06:00
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;
+8
2026-07-19 12:29:52 -05:00
permissionLevel: "connect" | "view" | "edit" | "manage";
+1
2025-12-31 22:20:12 -06:00
expiresAt: string | null;
createdAt: string;
}
2025-09-12 14:42:00 -05:00
import {
apiLogger,
authLogger,
sshLogger,
fileLogger,
+7
2025-11-05 10:36:16 -06:00
dashboardLogger,
2025-09-12 14:42:00 -05:00
type LogContext,
2026-05-11 01:23:11 -05:00
} from "@/lib/frontend-logger";
import { dbHealthMonitor } from "@/lib/db-health-monitor";
+5
2026-08-19 14:12:06 -05:00
import { getDeviceId } from "@/lib/device-id";
2025-08-07 02:20:27 -05:00
export type ServerStatus = {
2026-10-04 15:11:41 -05:00
status: "online" | "offline";
2025-09-12 14:42:00 -05:00
lastChecked: string;
};
2026-01-24 19:49:42 -06:00
export type SSHHostWithStatus = SSHHost & {
2026-10-04 15:11:41 -05:00
status: "online" | "offline" | "unknown";
2026-01-24 19:49:42 -06:00
};
2026-06-04 15:16:53 -04:00
export interface AuthResponse {
2025-10-01 15:40:10 -05:00
success?: boolean;
is_admin?: boolean;
username?: string;
userId?: string;
2026-10-01 16:20:16 -05:00
is_external?: boolean;
2025-10-01 15:40:10 -05:00
totp_enabled?: boolean;
2026-01-24 19:49:42 -06:00
requires_totp?: boolean;
temp_token?: string;
+5
2026-03-08 18:02:14 -05:00
rememberMe?: boolean;
2026-06-04 15:16:53 -04:00
token?: string;
2025-08-21 22:47:38 -05:00
}
2025-08-07 02:20:27 -05:00
2026-06-04 15:16:53 -04:00
export interface UserInfo {
2026-10-01 16:20:16 -05:00
/** Any second factor, kept under its 2.8 name for older clients. */
2025-09-12 14:42:00 -05:00
totp_enabled: boolean;
2026-10-01 16:20:16 -05:00
/** Admin user list only. */
second_factor_enabled?: boolean;
2025-10-01 15:40:10 -05:00
userId: string;
2025-09-12 14:42:00 -05:00
username: string;
is_admin: boolean;
2026-10-01 16:20:16 -05:00
/** Signs in through an external login such as SSO or LDAP. */
is_external?: boolean;
+3
2026-08-06 14:41:39 -05:00
is_dual_auth?: boolean;
2025-11-17 09:46:05 -06:00
password_hash?: string;
+8
2026-07-19 12:29:52 -05:00
data_unlocked?: boolean;
2026-10-01 16:20:16 -05:00
/** On a desktop linked to a server, the account it is signed in to there. */
linked?: LinkedAccountInfo | null;
2025-08-21 22:47:38 -05:00
}
2025-08-07 02:20:27 -05:00
2026-10-01 16:20:16 -05:00
export interface LinkedAccountInfo {
serverUrl: string;
serverName: string | null;
username: string | null;
isAdmin: boolean;
roles: string[];
permissions: string[];
status: string;
+3
2026-08-06 14:41:39 -05:00
}
2025-08-21 22:47:38 -05:00
interface UserCount {
2025-09-12 14:42:00 -05:00
count: number;
2025-08-21 22:47:38 -05:00
}
2026-05-06 15:12:07 -05:00
type ElectronApi = {
isElectron?: boolean;
getSetting?: (key: string) => Promise<string | null | undefined>;
setSetting?: (key: string, value: string) => Promise<void>;
};
type ElectronWindow = Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: ElectronApi;
ReactNativeWebView?: unknown;
};
2025-08-24 01:27:41 -05:00
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
2026-05-06 15:12:07 -05:00
export { isElectron };
2025-09-12 14:42:00 -05:00
2026-10-10 14:58:27 -05:00
export const TRUST_TOKEN_KEY = "termixTrustToken";
2025-09-12 14:42:00 -05:00
function getLoggerForService(serviceName: string) {
if (serviceName.includes("SSH") || serviceName.includes("ssh")) {
return sshLogger;
} else if (serviceName.includes("FILE") || serviceName.includes("file")) {
return fileLogger;
} else if (serviceName.includes("AUTH") || serviceName.includes("auth")) {
return authLogger;
+7
2025-11-05 10:36:16 -06:00
} else if (
serviceName.includes("DASHBOARD") ||
serviceName.includes("dashboard")
) {
return dashboardLogger;
2025-09-12 14:42:00 -05:00
} else {
return apiLogger;
}
}
2025-11-17 09:46:05 -06:00
const electronSettingsCache = new Map<string, string>();
if (isElectron()) {
(async () => {
try {
2026-05-06 15:12:07 -05:00
const electronAPI = (window as ElectronWindow).electronAPI;
2025-11-17 09:46:05 -06:00
if (electronAPI?.getSetting) {
+17
2026-09-20 14:56:39 -05:00
const settingsToLoad = ["rightClickCopyPaste", "copyOnSelect"];
2025-11-17 09:46:05 -06:00
for (const key of settingsToLoad) {
const value = await electronAPI.getSetting(key);
if (value !== null && value !== undefined) {
+9
2026-04-22 16:55:23 -05:00
// Only populate if not already set to prevent overwriting new values during login
if (!localStorage.getItem(key)) {
electronSettingsCache.set(key, value);
localStorage.setItem(key, value);
} else {
// Even if we don't overwrite localStorage, update the cache
electronSettingsCache.set(key, localStorage.getItem(key)!);
}
2025-11-17 09:46:05 -06:00
}
}
}
} catch (error) {
console.error("[Electron] Failed to load settings cache:", error);
}
})();
}
2026-05-06 15:12:07 -05:00
export function setCookie(
name: string,
value: string,
days = 7,
): void | Promise<void> {
2025-09-12 14:42:00 -05:00
if (isElectron()) {
2025-11-17 09:46:05 -06:00
try {
2026-05-06 15:12:07 -05:00
if (name === "jwt") {
return;
}
2025-11-17 09:46:05 -06:00
2026-05-06 15:12:07 -05:00
const electronAPI = (window as ElectronWindow).electronAPI;
2025-11-17 09:46:05 -06:00
if (electronAPI?.setSetting) {
2026-05-06 15:12:07 -05:00
electronSettingsCache.set(name, value);
localStorage.setItem(name, value);
2025-11-17 09:46:05 -06:00
electronAPI.setSetting(name, value).catch((err: Error) => {
console.error(`[Electron] Failed to persist setting ${name}:`, err);
});
}
} catch (error) {
console.error(`[Electron] Failed to set setting: ${name}`, error);
}
2025-09-12 14:42:00 -05:00
} else {
2025-08-21 22:47:38 -05:00
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
2025-09-12 14:42:00 -05:00
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export function getCookie(name: string): string | undefined {
if (isElectron()) {
2025-11-17 09:46:05 -06:00
try {
2026-05-06 15:12:07 -05:00
if (name === "jwt") {
return undefined;
}
2025-11-17 09:46:05 -06:00
if (electronSettingsCache.has(name)) {
return electronSettingsCache.get(name);
}
const token = localStorage.getItem(name) || undefined;
if (token) {
electronSettingsCache.set(name, token);
}
return token;
} catch (error) {
console.error(`[Electron] Failed to get setting: ${name}`, error);
return undefined;
}
2025-09-12 14:42:00 -05:00
} else {
2025-08-07 02:20:27 -05:00
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
2025-10-01 15:40:10 -05:00
const encodedToken =
2025-09-12 14:42:00 -05:00
parts.length === 2 ? parts.pop()?.split(";").shift() : undefined;
2025-10-01 15:40:10 -05:00
const token = encodedToken ? decodeURIComponent(encodedToken) : undefined;
2025-09-12 14:42:00 -05:00
return token;
}
2025-08-07 02:20:27 -05:00
}
2026-01-24 19:49:42 -06:00
let userWasAuthenticated = false;
2026-05-06 15:12:07 -05:00
let latestAuthSuccessAt = 0;
+17
2026-09-20 14:56:39 -05:00
let authInvalidationHandled = false;
2026-05-06 15:12:07 -05:00
2026-06-04 15:16:53 -04:00
export function markUserAuthenticated(): void {
2026-05-06 15:12:07 -05:00
userWasAuthenticated = true;
+17
2026-09-20 14:56:39 -05:00
authInvalidationHandled = false;
2026-05-06 15:12:07 -05:00
latestAuthSuccessAt =
typeof performance !== "undefined" ? performance.now() : Date.now();
}
+17
2026-09-20 14:56:39 -05:00
function clearClientAuthState(): void {
clearTermixSessionStorage();
try {
localStorage.removeItem("jwt");
localStorage.removeItem("termix_auth");
} catch {
// localStorage may be unavailable in restricted contexts.
}
if (isElectron()) {
const electronAPI = (
window as unknown as {
electronAPI?: { clearSessionCookies?: () => Promise<void> };
}
).electronAPI;
electronAPI?.clearSessionCookies?.().catch(() => {});
} else if (typeof window !== "undefined") {
const isSecure = window.location.protocol === "https:";
document.cookie = isSecure
? "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; Secure; SameSite=Lax"
: "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax";
}
}
2026-05-06 15:12:07 -05:00
export function isCurrentAuthInvalidationError(error: unknown): boolean {
const authError = error as {
__staleAuthInvalidation?: boolean;
};
if (authError.__staleAuthInvalidation) {
return false;
}
const axiosError = error as AxiosError;
const apiError = error as ApiError;
const responseData = axiosError.response?.data as
+3
2026-08-06 14:41:39 -05:00
Record<string, unknown> | undefined;
2026-05-06 15:12:07 -05:00
const errorCode = responseData?.code || apiError.code;
const errorMessage = responseData?.error || apiError.message;
const status = axiosError.response?.status || apiError.status;
const isMissingAuthenticationToken =
errorMessage === "Missing authentication token";
return (
status === 401 &&
(errorCode === "SESSION_EXPIRED" ||
errorCode === "SESSION_NOT_FOUND" ||
(errorCode === "AUTH_REQUIRED" && userWasAuthenticated) ||
errorMessage === "Invalid token" ||
(errorMessage === "Authentication required" && userWasAuthenticated) ||
(isMissingAuthenticationToken && userWasAuthenticated))
);
}
2026-01-24 19:49:42 -06:00
2025-09-12 14:42:00 -05:00
function createApiInstance(
baseURL: string,
serviceName: string = "API",
): AxiosInstance {
const instance = axios.create({
baseURL,
headers: { "Content-Type": "application/json" },
timeout: 30000,
2025-10-01 15:40:10 -05:00
withCredentials: true,
2025-09-12 14:42:00 -05:00
});
2025-08-07 02:20:27 -05:00
+5
2026-08-19 14:12:06 -05:00
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
2025-09-12 14:42:00 -05:00
const startTime = performance.now();
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2026-05-06 15:12:07 -05:00
const configWithMetadata = config as AxiosRequestConfigExtended;
configWithMetadata.startTime = startTime;
configWithMetadata.requestId = requestId;
2025-09-12 14:42:00 -05:00
const method = config.method?.toUpperCase() || "UNKNOWN";
const url = config.url || "UNKNOWN";
const fullUrl = `${config.baseURL}${url}`;
const context: LogContext = {
requestId,
method,
url: fullUrl,
operation: "request_start",
};
const logger = getLoggerForService(serviceName);
+9
2026-04-22 16:55:23 -05:00
const isDevMode = process.env.NODE_ENV === "development";
if (isDevMode) {
2025-09-12 14:42:00 -05:00
logger.requestStart(method, fullUrl, context);
}
+5
2026-08-19 14:12:06 -05:00
const deviceId = getDeviceId();
if (deviceId) {
if (config.headers.set) {
config.headers.set("X-Termix-Device-ID", deviceId);
} else {
config.headers["X-Termix-Device-ID"] = deviceId;
}
}
2025-09-12 14:42:00 -05:00
if (isElectron()) {
+9
2026-04-22 16:55:23 -05:00
if (config.headers.set) {
config.headers.set("X-Electron-App", "true");
} else {
config.headers["X-Electron-App"] = "true";
}
2026-05-12 21:55:14 -05:00
const jwt = localStorage.getItem("jwt");
if (jwt) {
if (config.headers.set) {
config.headers.set("Authorization", `Bearer ${jwt}`);
} else {
config.headers["Authorization"] = `Bearer ${jwt}`;
}
}
2026-10-10 14:58:27 -05:00
// The desktop app has no cookies with the server, so it carries the
// remember-this-device token itself, and only to login.
const trustToken = localStorage.getItem(TRUST_TOKEN_KEY);
const isLogin = isLoginRequestUrl(config.url);
if (trustToken && isLogin) {
if (config.headers.set) {
config.headers.set("X-Termix-Trust-Token", trustToken);
} else {
config.headers["X-Termix-Trust-Token"] = trustToken;
}
}
2025-09-12 14:42:00 -05:00
}
2026-05-06 15:12:07 -05:00
if (
typeof window !== "undefined" &&
(window as ElectronWindow).ReactNativeWebView
) {
+7
2025-11-05 10:36:16 -06:00
let platform = "Unknown";
if (typeof navigator !== "undefined" && navigator.userAgent) {
if (navigator.userAgent.includes("Android")) {
platform = "Android";
} else if (
navigator.userAgent.includes("iPhone") ||
navigator.userAgent.includes("iPad") ||
navigator.userAgent.includes("iOS")
) {
platform = "iOS";
}
}
+9
2026-04-22 16:55:23 -05:00
if (config.headers.set) {
config.headers.set("User-Agent", `Termix-Mobile/${platform}`);
} else {
config.headers["User-Agent"] = `Termix-Mobile/${platform}`;
}
+7
2025-11-05 10:36:16 -06:00
}
2025-09-12 14:42:00 -05:00
return config;
});
instance.interceptors.response.use(
2026-01-24 19:49:42 -06:00
(response: AxiosResponse) => {
2025-09-12 14:42:00 -05:00
const endTime = performance.now();
2026-05-06 15:12:07 -05:00
const responseConfig = response.config as AxiosRequestConfigExtended;
const startTime = responseConfig.startTime;
const requestId = responseConfig.requestId;
2026-01-24 19:49:42 -06:00
const responseTime = Math.round(endTime - (startTime || endTime));
2025-09-12 14:42:00 -05:00
const method = response.config.method?.toUpperCase() || "UNKNOWN";
const url = response.config.url || "UNKNOWN";
const fullUrl = `${response.config.baseURL}${url}`;
const context: LogContext = {
requestId,
method,
url: fullUrl,
status: response.status,
statusText: response.statusText,
responseTime,
operation: "request_success",
};
const logger = getLoggerForService(serviceName);
if (process.env.NODE_ENV === "development") {
logger.requestSuccess(
method,
fullUrl,
response.status,
responseTime,
context,
);
}
if (responseTime > 3000) {
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
}
2026-01-24 19:49:42 -06:00
dbHealthMonitor.reportDatabaseSuccess();
2025-09-12 14:42:00 -05:00
return response;
},
2026-01-24 19:49:42 -06:00
(error: AxiosErrorExtended) => {
2025-09-12 14:42:00 -05:00
const endTime = performance.now();
2026-01-24 19:49:42 -06:00
const startTime = error.config?.startTime;
const requestId = error.config?.requestId;
2025-09-12 14:42:00 -05:00
const responseTime = startTime
? Math.round(endTime - startTime)
: undefined;
const method = error.config?.method?.toUpperCase() || "UNKNOWN";
const url = error.config?.url || "UNKNOWN";
const fullUrl = error.config ? `${error.config.baseURL}${url}` : url;
const status = error.response?.status;
const message =
2026-01-24 19:49:42 -06:00
(error.response?.data as { error?: string })?.error ||
2025-09-12 14:42:00 -05:00
(error as Error).message ||
"Unknown error";
+7
2025-11-05 10:36:16 -06:00
const errorCode =
2026-01-24 19:49:42 -06:00
(error.response?.data as { code?: string })?.code || error.code;
2025-09-12 14:42:00 -05:00
const context: LogContext = {
requestId,
method,
url: fullUrl,
status,
responseTime,
errorCode,
errorMessage: message,
operation: "request_error",
};
const logger = getLoggerForService(serviceName);
+9
2026-04-22 16:55:23 -05:00
// A caller can mark a request as a silent retry (see progressive /status
// retry) so we don't spam error logs / health events on each attempt.
2026-05-06 15:12:07 -05:00
const isSilentRetry = !!error.config?.__silentRetry;
2025-09-12 14:42:00 -05:00
+9
2026-04-22 16:55:23 -05:00
if (process.env.NODE_ENV === "development" && !isSilentRetry) {
2025-09-12 14:42:00 -05:00
if (status === 401) {
logger.authError(method, fullUrl, context);
} else if (status === 0 || !status) {
logger.networkError(method, fullUrl, message, context);
} else {
logger.requestError(
method,
fullUrl,
status || 0,
message,
responseTime,
context,
);
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2025-09-12 14:42:00 -05:00
if (status === 401) {
+7
2025-11-05 10:36:16 -06:00
const errorCode = (error.response?.data as Record<string, unknown>)
?.code;
const errorMessage = (error.response?.data as Record<string, unknown>)
?.error;
2025-10-01 15:40:10 -05:00
const isSessionExpired = errorCode === "SESSION_EXPIRED";
2025-11-17 09:46:05 -06:00
const isSessionNotFound = errorCode === "SESSION_NOT_FOUND";
2026-05-06 15:12:07 -05:00
const isMissingAuthenticationToken =
errorMessage === "Missing authentication token";
+7
2025-11-05 10:36:16 -06:00
const isInvalidToken =
errorCode === "AUTH_REQUIRED" ||
errorMessage === "Invalid token" ||
2026-01-24 19:49:42 -06:00
errorMessage === "Authentication required" ||
2026-05-06 15:12:07 -05:00
(isMissingAuthenticationToken && userWasAuthenticated);
2025-10-01 15:40:10 -05:00
2026-05-06 15:12:07 -05:00
if (isSessionExpired || isSessionNotFound || isInvalidToken) {
const requestStartedAt =
typeof error.config?.startTime === "number"
? error.config.startTime
: 0;
const isStaleAuthInvalidation =
latestAuthSuccessAt > 0 &&
requestStartedAt > 0 &&
requestStartedAt < latestAuthSuccessAt;
if (isStaleAuthInvalidation) {
(
error as { __staleAuthInvalidation?: boolean }
).__staleAuthInvalidation = true;
return Promise.reject(error);
+9
2026-04-22 16:55:23 -05:00
}
+17
2026-09-20 14:56:39 -05:00
if (!authInvalidationHandled) {
authInvalidationHandled = true;
clearClientAuthState();
+7
2025-11-05 10:36:16 -06:00
+17
2026-09-20 14:56:39 -05:00
if (typeof window !== "undefined") {
console.warn("Session expired - please log in again");
2026-10-01 16:20:16 -05:00
toast.warning(i18n.t("errors.sessionExpiredLogin"));
+17
2026-09-20 14:56:39 -05:00
window.dispatchEvent(new Event("termix:logout"));
}
2025-10-01 15:40:10 -05:00
+17
2026-09-20 14:56:39 -05:00
dbHealthMonitor.reportSessionExpired();
2025-11-17 09:46:05 -06:00
}
2026-01-24 19:49:42 -06:00
userWasAuthenticated = false;
2025-10-01 15:40:10 -05:00
}
+9
2026-04-22 16:55:23 -05:00
} else if (!isSilentRetry) {
+5
2026-08-19 14:12:06 -05:00
dbHealthMonitor.reportDatabaseError(error);
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2025-09-12 14:42:00 -05:00
return Promise.reject(error);
},
);
return instance;
2025-08-24 01:27:41 -05:00
}
// ============================================================================
// API INSTANCES
// ============================================================================
2025-09-12 14:42:00 -05:00
function isDev(): boolean {
2025-10-01 15:40:10 -05:00
if (isElectron()) {
return false;
}
2025-09-12 14:42:00 -05:00
return (
process.env.NODE_ENV === "development" &&
(window.location.port === "3000" ||
window.location.port === "5173" ||
window.location.port === "" ||
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1")
);
}
+5
2026-08-19 14:12:06 -05:00
const apiHost =
import.meta.env.VITE_API_HOST ||
(typeof window !== "undefined" ? window.location.hostname : "localhost");
2025-09-12 14:42:00 -05:00
+5
2026-08-19 14:12:06 -05:00
interface AxiosRequestConfigExtended extends InternalAxiosRequestConfig {
2026-01-24 19:49:42 -06:00
startTime?: number;
requestId?: string;
2026-05-06 15:12:07 -05:00
__silentRetry?: boolean;
2026-01-24 19:49:42 -06:00
}
interface AxiosErrorExtended extends AxiosError {
config?: AxiosRequestConfigExtended;
}
2026-10-01 16:20:16 -05:00
/** A URL on the main backend, resolved for web, dev proxy and Electron. */
export function getBackendUrl(path: string): string {
return getApiUrl(path, 30001);
2025-09-12 14:42:00 -05:00
}
function getApiUrl(path: string, defaultPort: number): string {
+7
2025-11-05 10:36:16 -06:00
const devMode = isDev();
const electronMode = isElectron();
if (electronMode) {
2026-10-01 16:20:16 -05:00
// The desktop app always talks to its embedded backend. The server it
// may be linked to is reached through createRemoteOriginApiInstance.
2026-10-18 00:34:50 -05:00
return `http://localhost:${localBackendPort()}${path}`;
+7
2025-11-05 10:36:16 -06:00
} else if (devMode) {
+5
2026-08-19 14:12:06 -05:00
if (!import.meta.env.VITE_API_HOST) {
return `/__termix_api/${defaultPort}${path}`;
}
+7
2025-11-05 10:36:16 -06:00
const protocol = window.location.protocol === "https:" ? "https" : "http";
const sslPort = protocol === "https" ? 8443 : defaultPort;
const url = `${protocol}://${apiHost}:${sslPort}${path}`;
return url;
2025-09-12 14:42:00 -05:00
} else {
+4
2026-02-12 22:28:13 -06:00
return getBasePath() + path;
2025-09-12 14:42:00 -05:00
}
}
+2
2026-07-26 18:47:27 -05:00
// ============================================================================
// PER-HOST ORIGIN ROUTING (Electron desktop only)
// ============================================================================
//
2026-10-01 16:20:16 -05:00
// hostApi above always points at the embedded local backend. When a host's
// connection origin resolves to "remote" (see src/ui/lib/connection-origin.ts),
// the backend that holds that host's live session is the connected remote
// server instead, so calls for that host must follow it there.
+2
2026-07-26 18:47:27 -05:00
//
2026-10-01 16:20:16 -05:00
// These instances look up the linked server and its session on every
// request, so linking or unlinking needs no reload.
+2
2026-07-26 18:47:27 -05:00
2026-10-01 16:20:16 -05:00
export function createRemoteOriginApiInstance(path: string): AxiosInstance {
+2
2026-07-26 18:47:27 -05:00
const instance = axios.create({
headers: { "Content-Type": "application/json" },
timeout: 30000,
});
+5
2026-08-19 14:12:06 -05:00
instance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
2026-10-01 16:20:16 -05:00
const { getLinkedSession } = await import("@/lib/linked-server");
const linked = await getLinkedSession();
config.baseURL = linked
? `${linked.serverUrl}${path}`
+5
2026-08-19 14:12:06 -05:00
: "http://no-server-configured";
+2
2026-07-26 18:47:27 -05:00
+5
2026-08-19 14:12:06 -05:00
if (config.headers.set) {
config.headers.set("X-Electron-App", "true");
2026-10-01 16:20:16 -05:00
if (linked)
config.headers.set("Authorization", `Bearer ${linked.token}`);
+5
2026-08-19 14:12:06 -05:00
} else {
config.headers["X-Electron-App"] = "true";
2026-10-01 16:20:16 -05:00
if (linked) config.headers["Authorization"] = `Bearer ${linked.token}`;
+5
2026-08-19 14:12:06 -05:00
}
+2
2026-07-26 18:47:27 -05:00
+5
2026-08-19 14:12:06 -05:00
return config;
},
);
+2
2026-07-26 18:47:27 -05:00
return instance;
}
2026-10-01 16:20:16 -05:00
let remoteCoreApi: AxiosInstance | null = null;
+2
2026-07-26 18:47:27 -05:00
2026-10-01 16:20:16 -05:00
/** The linked server's core routes, unprefixed. */
export function getRemoteCoreApi(): AxiosInstance {
if (!remoteCoreApi) {
remoteCoreApi = createRemoteOriginApiInstance("");
+2
2026-07-26 18:47:27 -05:00
}
2026-10-01 16:20:16 -05:00
return remoteCoreApi;
+2
2026-07-26 18:47:27 -05:00
}
2025-09-12 14:42:00 -05:00
function initializeApiInstances() {
2026-10-01 16:20:16 -05:00
// Host Management API (port 30001) - SSH and plugin protocols
2026-03-14 20:05:05 -05:00
hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
sshHostApi = hostApi;
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
// Authentication API (port 30001)
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
+7
2025-11-05 10:36:16 -06:00
+1
2025-12-31 22:20:12 -06:00
// RBAC API (port 30001)
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
2025-09-12 14:42:00 -05:00
}
2025-08-24 01:27:41 -05:00
2026-10-01 16:20:16 -05:00
// Host Management API (port 30001) - SSH and plugin protocols
2026-03-14 20:05:05 -05:00
export let hostApi: AxiosInstance;
// Backward compatibility
2025-09-12 14:42:00 -05:00
export let sshHostApi: AxiosInstance;
2025-08-24 01:27:41 -05:00
2025-10-01 15:40:10 -05:00
// Authentication API (port 30001)
2025-09-12 14:42:00 -05:00
export let authApi: AxiosInstance;
+1
2025-12-31 22:20:12 -06:00
// RBAC API (port 30001)
export let rbacApi: AxiosInstance;
+9
2026-04-22 16:55:23 -05:00
// Pre-initialize with default values to avoid undefined errors during early mounting
initializeApiInstances();
2026-05-28 22:29:20 -05:00
let _resolveAppReady!: () => void;
export const appReadyPromise: Promise<void> = new Promise((resolve) => {
_resolveAppReady = resolve;
});
+7
2025-11-05 10:36:16 -06:00
function initializeApp() {
+2
2026-07-26 18:47:27 -05:00
initializeApiInstances();
_resolveAppReady();
+7
2025-11-05 10:36:16 -06:00
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeApp);
2025-10-01 15:40:10 -05:00
} else {
+7
2025-11-05 10:36:16 -06:00
initializeApp();
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
2025-08-24 01:27:41 -05:00
// ============================================================================
// ERROR HANDLING
// ============================================================================
class ApiError extends Error {
2025-09-12 14:42:00 -05:00
constructor(
message: string,
public status?: number,
public code?: string,
) {
super(message);
this.name = "ApiError";
}
2025-08-24 01:27:41 -05:00
}
2026-10-10 14:58:27 -05:00
/**
* Requests that sign someone in. A 401 from one of these means wrong
* credentials or a wrong code, not an expired session.
*/
export function isLoginRequestUrl(url: string | undefined): boolean {
if (!url) return false;
return (
url.includes("/users/login") ||
url.includes("/users/totp/verify-login") ||
/\/users\/auth\/(second-factor\/)?[^/?]+\/verify(\?|$)/.test(url)
);
}
2026-06-04 15:16:53 -04:00
export function handleApiError(error: unknown, operation: string): never {
2025-09-12 14:42:00 -05:00
const context: LogContext = {
operation: "error_handling",
errorOperation: operation,
};
if (axios.isAxiosError(error)) {
const status = error.response?.status;
+7
2025-11-05 10:36:16 -06:00
const message =
error.response?.data?.message ||
error.response?.data?.error ||
error.message;
const code = error.response?.data?.code || error.response?.data?.error;
2025-09-12 14:42:00 -05:00
const url = error.config?.url;
const method = error.config?.method?.toUpperCase();
const errorContext: LogContext = {
...context,
method,
url,
status,
errorCode: code,
errorMessage: message,
};
if (status === 401) {
authLogger.warn(
`Auth failed: ${method} ${url} - ${message}`,
errorContext,
);
2025-10-01 15:40:10 -05:00
2026-10-10 14:58:27 -05:00
const errorMessage = isLoginRequestUrl(url)
2025-10-01 15:40:10 -05:00
? message
: "Authentication required. Please log in again.";
2026-05-06 15:12:07 -05:00
throw new ApiError(errorMessage, 401, code || "AUTH_REQUIRED");
2025-09-12 14:42:00 -05:00
} else if (status === 403) {
authLogger.warn(`Access denied: ${method} ${url}`, errorContext);
+7
2025-11-05 10:36:16 -06:00
const apiError = new ApiError(
code === "TOTP_REQUIRED"
? message
: "Access denied. You do not have permission to perform this action.",
2025-09-12 14:42:00 -05:00
403,
+7
2025-11-05 10:36:16 -06:00
code || "ACCESS_DENIED",
2025-09-12 14:42:00 -05:00
);
+7
2025-11-05 10:36:16 -06:00
(apiError as ApiError & { response?: unknown }).response = error.response;
throw apiError;
2025-09-12 14:42:00 -05:00
} else if (status === 404) {
apiLogger.warn(`Not found: ${method} ${url}`, errorContext);
throw new ApiError(
"Resource not found. The requested item may have been deleted.",
404,
"NOT_FOUND",
);
} else if (status === 409) {
apiLogger.warn(`Conflict: ${method} ${url}`, errorContext);
throw new ApiError(
"Conflict. The resource already exists or is in use.",
409,
"CONFLICT",
);
} else if (status === 422) {
apiLogger.warn(
`Validation error: ${method} ${url} - ${message}`,
errorContext,
);
throw new ApiError(
"Validation error. Please check your input and try again.",
422,
"VALIDATION_ERROR",
);
} else if (status && status >= 500) {
apiLogger.error(
`Server error: ${method} ${url} - ${message}`,
error,
errorContext,
);
throw new ApiError(
"Server error occurred. Please try again later.",
status,
"SERVER_ERROR",
);
} else if (status === 0) {
if (url.includes("no-server-configured")) {
apiLogger.error(
`No server configured: ${method} ${url}`,
error,
errorContext,
);
throw new ApiError(
"No server configured. Please configure a Termix server first.",
0,
"NO_SERVER_CONFIGURED",
);
}
apiLogger.error(
`Network error: ${method} ${url} - ${message}`,
error,
errorContext,
);
throw new ApiError(
"Network error. Please check your connection and try again.",
0,
"NETWORK_ERROR",
);
} else {
apiLogger.error(
`Request failed: ${method} ${url} - ${message}`,
error,
errorContext,
);
throw new ApiError(message || `Failed to ${operation}`, status, code);
2025-08-24 01:27:41 -05:00
}
2025-09-12 14:42:00 -05:00
}
if (error instanceof ApiError) {
throw error;
}
+5
2026-08-19 14:12:06 -05:00
const errorMessage = getErrorMessage(error);
2025-09-12 14:42:00 -05:00
apiLogger.error(
`Unexpected error during ${operation}: ${errorMessage}`,
error,
context,
);
throw new ApiError(
`Unexpected error during ${operation}: ${errorMessage}`,
undefined,
"UNKNOWN_ERROR",
);
2025-08-24 01:27:41 -05:00
}
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getSSHHosts,
createSSHHost,
updateSSHHost,
bulkImportSSHHosts,
2026-06-21 15:55:41 -05:00
importSSHConfigHosts,
2026-06-04 15:16:53 -04:00
bulkUpdateSSHHosts,
+5
2026-08-19 14:12:06 -05:00
reorderSSHHosts,
2026-06-04 15:16:53 -04:00
deleteSSHHost,
getSSHHostById,
exportSSHHostWithCredentials,
exportAllSSHHosts,
testProxyConnection,
} from "@/api/ssh-host-management-api";
2025-10-01 15:40:10 -05:00
2026-06-04 15:16:53 -04:00
export {
getAllServerStatuses,
getServerStatusById,
refreshServerPolling,
2026-10-01 16:20:16 -05:00
getStatusCheckSettings,
updateStatusCheckSettings,
} from "@/api/host-status-api";
+5
2026-08-19 14:12:06 -05:00
export {
getHostSidebarPreferences,
saveHostSidebarPreferences,
} from "@/api/host-sidebar-preferences-api";
export {
getCredentialSidebarPreferences,
saveCredentialSidebarPreferences,
} from "@/api/credential-sidebar-preferences-api";
export { getUiPreferences, saveUiPreferences } from "@/api/ui-preferences-api";
2026-06-04 15:16:53 -04:00
export {
getLogLevel,
updateLogLevel,
getSessionTimeout,
updateSessionTimeout,
} from "@/api/settings-api";
2026-03-14 20:05:05 -05:00
2025-08-24 01:27:41 -05:00
// ============================================================================
// AUTHENTICATION
// ============================================================================
2025-09-12 14:42:00 -05:00
export async function registerUser(
username: string,
password: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/create", {
username,
password,
});
return response.data;
} catch (error) {
handleApiError(error, "register user");
}
2025-08-21 22:47:38 -05:00
}
2026-06-21 15:55:41 -05:00
export async function adminCreateUser(
username: string,
password: string,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/users/admin-create", {
username,
password,
});
return response.data;
} catch (error) {
handleApiError(error, "admin create user");
}
}
2025-09-12 14:42:00 -05:00
export async function loginUser(
username: string,
password: string,
+5
2026-03-08 18:02:14 -05:00
rememberMe: boolean = false,
2025-09-12 14:42:00 -05:00
): Promise<AuthResponse> {
try {
+5
2026-03-08 18:02:14 -05:00
const response = await authApi.post("/users/login", {
username,
password,
rememberMe,
});
2025-10-01 15:40:10 -05:00
2026-05-28 22:29:20 -05:00
if (response.data.token) {
localStorage.setItem("jwt", response.data.token);
}
2026-05-06 15:12:07 -05:00
if (response.data.success && !response.data.requires_totp) {
markUserAuthenticated();
}
2025-10-01 15:40:10 -05:00
return {
success: response.data.success,
is_admin: response.data.is_admin,
username: response.data.username,
requires_totp: response.data.requires_totp,
temp_token: response.data.temp_token,
+5
2026-03-08 18:02:14 -05:00
rememberMe: response.data.rememberMe,
2026-10-10 14:58:27 -05:00
is_external: response.data.is_external,
2026-01-24 19:49:42 -06:00
totp_enabled: response.data.totp_enabled,
2026-06-04 15:16:53 -04:00
token: response.data.token,
2025-10-01 15:40:10 -05:00
};
2025-09-12 14:42:00 -05:00
} catch (error) {
2026-01-24 19:49:42 -06:00
throw handleApiError(error, "login user");
2025-09-12 14:42:00 -05:00
}
2025-08-21 22:47:38 -05:00
}
+5
2026-08-19 14:12:06 -05:00
export async function requestTrustedProxyLogin(): Promise<{
enabled: boolean;
success?: boolean;
username?: string;
userId?: string;
is_admin?: boolean;
token?: string;
}> {
const response = await authApi.post("/users/proxy-login");
if (response.data.token) localStorage.setItem("jwt", response.data.token);
if (response.data.success) markUserAuthenticated();
return response.data;
}
2025-10-01 15:40:10 -05:00
export async function logoutUser(): Promise<{
success: boolean;
message: string;
}> {
try {
const response = await authApi.post("/users/logout");
+17
2026-09-20 14:56:39 -05:00
clearClientAuthState();
userWasAuthenticated = false;
authInvalidationHandled = false;
2025-10-01 15:40:10 -05:00
return response.data;
} catch (error) {
+17
2026-09-20 14:56:39 -05:00
clearClientAuthState();
userWasAuthenticated = false;
authInvalidationHandled = false;
2025-10-01 15:40:10 -05:00
handleApiError(error, "logout user");
}
}
2025-08-21 22:47:38 -05:00
export async function getUserInfo(): Promise<UserInfo> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/me");
2026-05-06 15:12:07 -05:00
markUserAuthenticated();
2025-09-12 14:42:00 -05:00
return response.data;
} catch (error) {
handleApiError(error, "fetch user info");
}
2025-08-21 22:47:38 -05:00
}
2026-05-12 21:55:14 -05:00
export async function getCurrentToken(): Promise<string | null> {
try {
const response = await authApi.get("/users/me/token");
return response.data?.token ?? null;
} catch {
return null;
}
}
2025-10-01 15:40:10 -05:00
export async function unlockUserData(
password: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await authApi.post("/users/unlock-data", { password });
return response.data;
} catch (error) {
handleApiError(error, "unlock user data");
}
}
2025-08-21 22:47:38 -05:00
export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/registration-allowed");
return response.data;
} catch (error) {
handleApiError(error, "check registration status");
}
2025-08-21 22:47:38 -05:00
}
2026-10-01 16:20:16 -05:00
export async function getPasswordLoginAllowed(): Promise<{
allowed: boolean;
/** Turned off by an admin but kept on because nothing else can sign in. */
forced?: boolean;
}> {
+7
2025-11-05 10:36:16 -06:00
try {
const response = await authApi.get("/users/password-login-allowed");
return response.data;
} catch (error) {
handleApiError(error, "check password login status");
}
}
2025-10-01 15:40:10 -05:00
export async function getSetupRequired(): Promise<{ setup_required: boolean }> {
try {
const response = await authApi.get("/users/setup-required");
return response.data;
} catch (error) {
handleApiError(error, "check setup status");
}
}
+2
2026-07-26 18:47:27 -05:00
// Module-level (not per-component) so concurrent/duplicate mounts -- e.g.
// React StrictMode's intentional double-invoke of effects in dev, or any
// other double-call -- share one in-flight request instead of each minting
// its own session. Minting more than one session here is not just wasted
// work: the backend sets a fresh `jwt` cookie on every call, and since the
// auth middleware prefers the cookie over the Authorization header, a
// second mint silently invalidates whichever token the app already started
// using, surfacing as a spurious "session expired/revoked" on the very
// next request.
let desktopAutoSessionRequest: Promise<DesktopAutoSessionOutcome> | null = null;
// A 403 from the auto-session endpoint means the backend positively
// evaluated the request and declined (not loopback, or not exactly one
// local user) -- that verdict won't change by retrying. Any other failure
// (connection refused, timeout, 5xx) most likely means the embedded
// backend process hasn't finished booting yet, which is routine on a cold
// first launch, so it's worth retrying rather than treated as final.
export type DesktopAutoSessionOutcome =
| { kind: "success"; data: AuthResponse }
| { kind: "declined" }
| { kind: "retry" };
/**
* Electron-only, non-iframed local login: exchanges the embedded backend's
+17
2026-09-20 14:56:39 -05:00
* auto-provisioned local user for a session without ever showing a login
* form. If the local database contains multiple users, the backend
* deterministically chooses the admin account, or the earliest registered
* account if no admin exists.
+2
2026-07-26 18:47:27 -05:00
*/
export async function requestDesktopAutoSession(): Promise<DesktopAutoSessionOutcome> {
if (desktopAutoSessionRequest) return desktopAutoSessionRequest;
desktopAutoSessionRequest = (async () => {
try {
const response = await authApi.post("/users/internal/auto-session");
if (response.data?.token) {
localStorage.setItem("jwt", response.data.token);
}
if (response.data?.success) {
markUserAuthenticated();
return { kind: "success" as const, data: response.data };
}
return { kind: "declined" as const };
} catch (err) {
const status =
(err as { status?: number; response?: { status?: number } })?.status ??
(err as { response?: { status?: number } })?.response?.status;
if (status === 403) {
return { kind: "declined" as const };
}
return { kind: "retry" as const };
}
})();
try {
return await desktopAutoSessionRequest;
} finally {
desktopAutoSessionRequest = null;
}
}
2025-08-21 22:47:38 -05:00
export async function getUserCount(): Promise<UserCount> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.get("/users/count");
return response.data;
} catch (error) {
handleApiError(error, "fetch user count");
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function initiatePasswordReset(
username: string,
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/initiate-reset", { username });
return response.data;
} catch (error) {
handleApiError(error, "initiate password reset");
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export async function verifyPasswordResetCode(
username: string,
resetCode: string,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/verify-reset-code", {
username,
resetCode,
});
return response.data;
} catch (error) {
handleApiError(error, "verify reset code");
}
2025-08-21 22:47:38 -05:00
}
2025-09-12 14:42:00 -05:00
export async function completePasswordReset(
username: string,
tempToken: string,
newPassword: string,
+8
2026-07-19 12:29:52 -05:00
confirmDataWipe = false,
+7
2025-11-05 10:36:16 -06:00
): Promise<Record<string, unknown>> {
2025-09-12 14:42:00 -05:00
try {
const response = await authApi.post("/users/complete-reset", {
username,
tempToken,
newPassword,
+8
2026-07-19 12:29:52 -05:00
confirmDataWipe,
2025-09-12 14:42:00 -05:00
});
return response.data;
} catch (error) {
handleApiError(error, "complete password reset");
}
2025-08-21 22:47:38 -05:00
}
+7
2025-11-05 10:36:16 -06:00
export async function changePassword(oldPassword: string, newPassword: string) {
try {
const response = await authApi.post("/users/change-password", {
oldPassword,
newPassword,
});
return response.data;
} catch (error) {
handleApiError(error, "change password");
}
}
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getUserList,
getSessions,
revokeSession,
revokeAllUserSessions,
createApiKey,
getApiKeys,
deleteApiKey,
makeUserAdmin,
removeAdminStatus,
deleteUser,
deleteAccount,
updateRegistrationAllowed,
2026-10-01 16:20:16 -05:00
getExternalAutoProvision,
updateExternalAutoProvision,
getSecondFactorAfterExternalLogin,
updateSecondFactorAfterExternalLogin,
2026-06-04 15:16:53 -04:00
updatePasswordLoginAllowed,
getPasswordResetAllowed,
updatePasswordResetAllowed,
+8
2026-07-19 12:29:52 -05:00
adminResetUserPassword,
adminExportUserData,
2026-06-04 15:16:53 -04:00
type ApiKey,
type CreatedApiKey,
} from "@/api/user-management-api";
+8
2026-07-19 12:29:52 -05:00
// ADMIN USER DATA MANAGEMENT
// ============================================================================
export {
adminGetUserHosts,
adminCreateUserHost,
adminUpdateUserHost,
adminDeleteUserHost,
adminGetHostPassword,
adminGetUserCredentials,
adminGetUserCredentialDetails,
adminCreateUserCredential,
adminUpdateUserCredential,
adminDeleteUserCredential,
} from "@/api/admin-user-data-api";
2026-06-04 15:16:53 -04:00
export {
getReleasesRSS,
getVersionInfo,
+5
2026-08-19 14:12:06 -05:00
releaseUrlFrom,
2026-06-04 15:16:53 -04:00
getDatabaseHealth,
} from "@/api/system-status-api";
2025-09-12 14:42:00 -05:00
// SSH CREDENTIALS MANAGEMENT
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getCredentials,
getCredentialDetails,
createCredential,
updateCredential,
2026-08-22 19:47:40 -05:00
duplicateCredential,
2026-06-04 15:16:53 -04:00
deleteCredential,
getCredentialHosts,
getCredentialFolders,
getSSHHostWithCredentials,
getHostPassword,
applyCredentialToHost,
removeCredentialFromHost,
migrateHostToCredential,
getFoldersWithStats,
renameFolder,
getSSHFolders,
updateFolderMetadata,
+5
2026-08-19 14:12:06 -05:00
reorderFolders,
2026-06-04 15:16:53 -04:00
deleteAllHostsInFolder,
renameCredentialFolder,
+5
2026-08-19 14:12:06 -05:00
reorderCredentials,
2026-06-04 15:16:53 -04:00
detectKeyType,
detectPublicKeyType,
validateKeyPair,
generatePublicKeyFromPrivate,
generateKeyPair,
deployCredentialToHost,
} from "@/api/credentials-api";
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
2026-10-01 16:20:16 -05:00
export type { UptimeInfo, RecentActivityItem } from "@/api/dashboard-api";
2026-06-04 15:16:53 -04:00
export {
getUptime,
getRecentActivity,
logActivity,
resetRecentActivity,
} from "@/api/dashboard-api";
2026-01-24 19:49:42 -06:00
+7
2025-11-05 10:36:16 -06:00
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
2026-10-01 16:20:16 -05:00
linkExternalToPasswordAccount,
unlinkExternalFromPasswordAccount,
} from "@/api/external-account-api";
2026-05-28 22:29:20 -05:00
+1
2025-12-31 22:20:12 -06:00
// ============================================================================
// RBAC MANAGEMENT
// ============================================================================
2026-06-04 15:16:53 -04:00
export {
getRoles,
createRole,
updateRole,
deleteRole,
getUserRoles,
assignRoleToUser,
removeRoleFromUser,
shareHost,
+2
2026-07-26 18:47:27 -05:00
shareFolder,
+8
2026-07-19 12:29:52 -05:00
updateHostAccess,
2026-06-04 15:16:53 -04:00
getHostAccess,
revokeHostAccess,
+3
2026-08-06 14:41:39 -05:00
getHostAuthOverride,
setHostAuthOverride,
+8
2026-07-19 12:29:52 -05:00
getPermissionsCatalog,
getSharedHosts,
2026-06-04 15:16:53 -04:00
} from "@/api/rbac-api";
+8
2026-07-19 12:29:52 -05:00
export type {
SharePermissionLevel,
ShareTarget,
PermissionCatalogEntry,
2026-10-01 16:20:16 -05:00
PermissionCatalogItem,
+8
2026-07-19 12:29:52 -05:00
} from "@/api/rbac-api";
+9
2026-04-22 16:55:23 -05:00
2026-06-04 15:16:53 -04:00
export {
getOpenTabs,
syncOpenTabs,
deleteOpenTab,
patchOpenTab,
addOpenTab,
getActiveSessions,
getUserPreferences,
saveUserPreferences,
type OpenTabRecord,
type OpenTabSyncPayload,
type OpenTabUpsertPayload,
type ActiveSessionInfo,
type UserPreferences,
} from "@/api/open-tabs-api";