From 8c004dfcfe749978dc0b9cfaf0f9984300fb22f4 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 24 Sep 2025 01:57:17 +0800 Subject: [PATCH] feat: Simplify AutoStart and fix critical security vulnerability Major architectural improvements: - Remove complex plaintext cache system, use direct database fields - Replace IP-based authentication with secure token-based auth - Integrate INTERNAL_AUTH_TOKEN with unified auto-generation system Security fixes: - Fix Docker nginx proxy authentication bypass vulnerability in /ssh/db/host/internal - Replace req.ip detection with X-Internal-Auth-Token header validation - Add production environment security checks for internal auth token AutoStart simplification: - Add autostart_{password,key,key_password} columns directly to ssh_data table - Remove redundant autostartPlaintextCache table and AutoStartPlaintextManager - Implement enable/disable/status endpoints for autostart management - Update frontend to handle autostart cache lifecycle automatically Environment variable improvements: - Integrate INTERNAL_AUTH_TOKEN into SystemCrypto auto-generation - Unified .env file management for all security keys (JWT, Database, Internal Auth) - Auto-generate secure tokens with proper entropy (256-bit) API improvements: - Make /users/oidc-config and /users/registration-allowed public for login page - Add /users/setup-required endpoint replacing problematic getUserCount usage - Restrict /users/count to admin-only access for security Database schema: - Add autostart plaintext columns to ssh_data table with proper migrations - Remove complex cache table structure for simplified data model --- src/backend/database/db/index.ts | 6 + src/backend/database/db/schema.ts | 6 + src/backend/database/routes/ssh.ts | 285 ++++++++++++++++-- src/backend/database/routes/users.ts | 36 ++- src/backend/ssh/tunnel.ts | 7 +- src/backend/starter.ts | 19 +- src/backend/utils/system-crypto.ts | 63 ++++ .../Apps/Host Manager/HostManagerEditor.tsx | 43 ++- src/ui/Desktop/Homepage/HomepageAuth.tsx | 6 +- src/ui/Mobile/Homepage/HomepageAuth.tsx | 6 +- src/ui/main-axios.ts | 51 ++++ 11 files changed, 479 insertions(+), 49 deletions(-) diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index ad90b28b..52949e72 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -299,6 +299,7 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (host_id) REFERENCES ssh_data (id), FOREIGN KEY (user_id) REFERENCES users (id) ); + `); // Run schema migrations @@ -435,6 +436,11 @@ const migrateSchema = () => { "INTEGER REFERENCES ssh_credentials(id)", ); + // AutoStart plaintext columns + addColumnIfNotExists("ssh_data", "autostart_password", "TEXT"); + addColumnIfNotExists("ssh_data", "autostart_key", "TEXT"); + addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT"); + // SSH credentials table migrations for encryption support addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 7e64d754..8e0f8e79 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -49,6 +49,11 @@ export const sshData = sqliteTable("ssh_data", { keyPassword: text("key_password"), keyType: text("key_type"), + // AutoStart plaintext fields (populated only when autoStart is enabled) + autostartPassword: text("autostart_password"), + autostartKey: text("autostart_key", { length: 8192 }), + autostartKeyPassword: text("autostart_key_password"), + credentialId: integer("credential_id").references(() => sshCredentials.id), enableTerminal: integer("enable_terminal", { mode: "boolean" }) .notNull() @@ -168,3 +173,4 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", { .notNull() .default(sql`CURRENT_TIMESTAMP`), }); + diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index f502db93..5d6a94be 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -8,13 +8,15 @@ import { fileManagerPinned, fileManagerShortcuts, } from "../db/schema.js"; -import { eq, and, desc } from "drizzle-orm"; +import { eq, and, desc, isNotNull, or } from "drizzle-orm"; import type { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; import multer from "multer"; import { sshLogger } from "../../utils/logger.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js"; import { AuthManager } from "../../utils/auth-manager.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; +import { SystemCrypto } from "../../utils/system-crypto.js"; const router = express.Router(); @@ -42,40 +44,76 @@ function isLocalhost(req: Request) { return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; } -// Internal-only endpoint for autostart (no JWT) +// Internal-only endpoint for autostart - requires internal auth token router.get("/db/host/internal", async (req: Request, res: Response) => { - if (!isLocalhost(req) && req.headers["x-internal-request"] !== "1") { - sshLogger.warn("Unauthorized attempt to access internal SSH host endpoint"); - return res.status(403).json({ error: "Forbidden" }); - } try { - // Internal endpoint - returns encrypted data (autostart will need user unlock) - const data = await SimpleDBOps.selectEncrypted( - db.select().from(sshData), - "ssh_data", - ); - const result = data.map((row: any) => { + // Check for internal authentication token using SystemCrypto + const internalToken = req.headers["x-internal-auth-token"]; + const systemCrypto = SystemCrypto.getInstance(); + const expectedToken = await systemCrypto.getInternalAuthToken(); + + if (internalToken !== expectedToken) { + sshLogger.warn("Unauthorized attempt to access internal SSH host endpoint", { + source: req.ip, + userAgent: req.headers["user-agent"], + providedToken: internalToken ? "present" : "missing" + }); + return res.status(403).json({ error: "Forbidden" }); + } + } catch (error) { + sshLogger.error("Failed to validate internal auth token", error); + return res.status(500).json({ error: "Internal server error" }); + } + + try { + // Query sshData directly for hosts that have autostart plaintext fields populated + const autostartHosts = await db.select() + .from(sshData) + .where( + // Check if any autostart fields are populated (meaning autostart is enabled) + or( + isNotNull(sshData.autostartPassword), + isNotNull(sshData.autostartKey) + ) + ); + + sshLogger.info("Internal autostart endpoint accessed", { + operation: "autostart_internal_access", + configCount: autostartHosts.length, + source: req.ip, + userAgent: req.headers["user-agent"] + }); + + // Transform to expected format for tunnel service + const result = autostartHosts.map((host) => { + const tunnelConnections = host.tunnelConnections + ? JSON.parse(host.tunnelConnections) + : []; + return { - ...row, - tags: - typeof row.tags === "string" - ? row.tags - ? row.tags.split(",").filter(Boolean) - : [] - : [], - pin: !!row.pin, - enableTerminal: !!row.enableTerminal, - enableTunnel: !!row.enableTunnel, - tunnelConnections: row.tunnelConnections - ? JSON.parse(row.tunnelConnections) - : [], - enableFileManager: !!row.enableFileManager, + id: host.id, + userId: host.userId, + name: host.name || `autostart-${host.id}`, + ip: host.ip, + port: host.port, + username: host.username, + password: host.autostartPassword, + key: host.autostartKey, + keyPassword: host.autostartKeyPassword, + authType: host.authType, + enableTunnel: true, + tunnelConnections: tunnelConnections.filter((tunnel: any) => tunnel.autoStart), + pin: false, + enableTerminal: false, + enableFileManager: false, + tags: ["autostart"], }; }); + res.json(result); } catch (err) { - sshLogger.error("Failed to fetch SSH data (internal)", err); - res.status(500).json({ error: "Failed to fetch SSH data" }); + sshLogger.error("Failed to fetch autostart SSH data", err); + res.status(500).json({ error: "Failed to fetch autostart SSH data" }); } }); @@ -1261,4 +1299,195 @@ router.post( }, ); +// Route: Enable autostart for SSH configuration (requires JWT) +// POST /ssh/autostart/enable +router.post( + "/autostart/enable", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { sshConfigId } = req.body; + + if (!sshConfigId || typeof sshConfigId !== "number") { + sshLogger.warn("Missing or invalid sshConfigId in autostart enable request", { + operation: "autostart_enable", + userId, + sshConfigId + }); + return res.status(400).json({ error: "Valid sshConfigId is required" }); + } + + try { + // Validate user has access to decrypt the data + const userDataKey = DataCrypto.getUserDataKey(userId); + if (!userDataKey) { + sshLogger.warn("User attempted to enable autostart without unlocked data", { + operation: "autostart_enable_failed", + userId, + sshConfigId, + reason: "data_locked" + }); + return res.status(400).json({ + error: "Failed to enable autostart. Ensure user data is unlocked." + }); + } + + // Get and decrypt SSH configuration + const sshConfig = await db.select() + .from(sshData) + .where(and( + eq(sshData.id, sshConfigId), + eq(sshData.userId, userId) + )); + + if (sshConfig.length === 0) { + sshLogger.warn("SSH config not found for autostart enable", { + operation: "autostart_enable_failed", + userId, + sshConfigId, + reason: "config_not_found" + }); + return res.status(404).json({ + error: "SSH configuration not found" + }); + } + + const config = sshConfig[0]; + + // Decrypt sensitive fields + const decryptedConfig = DataCrypto.decryptRecord("ssh_data", config, userId, userDataKey); + + // Update the SSH config with plaintext autostart fields + await db.update(sshData) + .set({ + autostartPassword: decryptedConfig.password || null, + autostartKey: decryptedConfig.key || null, + autostartKeyPassword: decryptedConfig.keyPassword || null, + }) + .where(eq(sshData.id, sshConfigId)); + + sshLogger.success("AutoStart enabled successfully", { + operation: "autostart_enabled", + userId, + sshConfigId, + host: config.ip + }); + + res.json({ + message: "AutoStart enabled successfully", + sshConfigId + }); + } catch (error) { + sshLogger.error("Error enabling autostart", error, { + operation: "autostart_enable_error", + userId, + sshConfigId + }); + res.status(500).json({ error: "Internal server error" }); + } + } +); + +// Route: Disable autostart for SSH configuration (requires JWT) +// DELETE /ssh/autostart/disable +router.delete( + "/autostart/disable", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { sshConfigId } = req.body; + + if (!sshConfigId || typeof sshConfigId !== "number") { + sshLogger.warn("Missing or invalid sshConfigId in autostart disable request", { + operation: "autostart_disable", + userId, + sshConfigId + }); + return res.status(400).json({ error: "Valid sshConfigId is required" }); + } + + try { + // Clear the autostart plaintext fields for this SSH config + const result = await db.update(sshData) + .set({ + autostartPassword: null, + autostartKey: null, + autostartKeyPassword: null, + }) + .where(and( + eq(sshData.id, sshConfigId), + eq(sshData.userId, userId) + )); + + sshLogger.info("AutoStart disabled successfully", { + operation: "autostart_disabled", + userId, + sshConfigId + }); + + res.json({ + message: "AutoStart disabled successfully", + sshConfigId + }); + } catch (error) { + sshLogger.error("Error disabling autostart", error, { + operation: "autostart_disable_error", + userId, + sshConfigId + }); + res.status(500).json({ error: "Internal server error" }); + } + } +); + +// Route: Get autostart status for user's SSH configurations (requires JWT) +// GET /ssh/autostart/status +router.get( + "/autostart/status", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + + try { + // Query user's SSH configs that have autostart enabled + const autostartConfigs = await db.select() + .from(sshData) + .where(and( + eq(sshData.userId, userId), + or( + isNotNull(sshData.autostartPassword), + isNotNull(sshData.autostartKey) + ) + )); + + // Map to just the basic info needed for status + const statusList = autostartConfigs.map(config => ({ + sshConfigId: config.id, + host: config.ip, + port: config.port, + username: config.username, + authType: config.authType + })); + + sshLogger.info("AutoStart status retrieved", { + operation: "autostart_status", + userId, + configCount: statusList.length + }); + + res.json({ + autostart_configs: statusList, + total_count: statusList.length + }); + } catch (error) { + sshLogger.error("Error getting autostart status", error, { + operation: "autostart_status_error", + userId + }); + res.status(500).json({ error: "Internal server error" }); + } + } +); + export default router; diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 22f47303..742120db 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -412,9 +412,9 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => { } }); -// Route: Get OIDC configuration +// Route: Get OIDC configuration (public - needed for login page) // GET /users/oidc-config -router.get("/oidc-config", authenticateJWT, async (req, res) => { +router.get("/oidc-config", async (req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") @@ -955,10 +955,36 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => { } }); -// Route: Count users +// Route: Check if system requires initial setup (public - for first-time setup detection) +// GET /users/setup-required +router.get("/setup-required", async (req, res) => { + try { + const countResult = db.$client + .prepare("SELECT COUNT(*) as count FROM users") + .get(); + const count = (countResult as any)?.count || 0; + + res.json({ + setup_required: count === 0, + // 不暴露具体用户数量,只返回是否需要初始化 + }); + } catch (err) { + authLogger.error("Failed to check setup status", err); + res.status(500).json({ error: "Failed to check setup status" }); + } +}); + +// Route: Count users (admin only - for dashboard statistics) // GET /users/count router.get("/count", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; try { + // 只有管理员可以查看用户统计 + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user[0] || !user[0].is_admin) { + return res.status(403).json({ error: "Admin access required" }); + } + const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") .get(); @@ -982,9 +1008,9 @@ router.get("/db-health", requireAdmin, async (req, res) => { } }); -// Route: Get registration allowed status +// Route: Get registration allowed status (public - needed for login page) // GET /users/registration-allowed -router.get("/registration-allowed", authenticateJWT, async (req, res) => { +router.get("/registration-allowed", async (req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 944a1186..89e7a946 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -15,6 +15,7 @@ import type { } from "../../types/index.js"; import { CONNECTION_STATES } from "../../types/index.js"; import { tunnelLogger } from "../utils/logger.js"; +import { SystemCrypto } from "../utils/system-crypto.js"; const app = express(); app.use( @@ -1023,12 +1024,16 @@ app.post("/ssh/tunnel/cancel", (req, res) => { async function initializeAutoStartTunnels(): Promise { try { + // Get internal auth token from SystemCrypto + const systemCrypto = SystemCrypto.getInstance(); + const internalAuthToken = await systemCrypto.getInternalAuthToken(); + const response = await axios.get( "http://localhost:8081/ssh/db/host/internal", { headers: { "Content-Type": "application/json", - "X-Internal-Request": "1", + "X-Internal-Auth-Token": internalAuthToken, }, }, ); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 9e25739d..bdcd3002 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -8,6 +8,7 @@ import path from "path"; import { AutoSSLSetup } from "./utils/auto-ssl-setup.js"; import { AuthManager } from "./utils/auth-manager.js"; import { DataCrypto } from "./utils/data-crypto.js"; +import { SystemCrypto } from "./utils/system-crypto.js"; import { systemLogger, versionLogger } from "./utils/logger.js"; (async () => { @@ -74,6 +75,15 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; securityIssues.push("DATABASE_KEY should be at least 64 characters in production"); } + if (!process.env.INTERNAL_AUTH_TOKEN) { + systemLogger.warn("INTERNAL_AUTH_TOKEN not set - using auto-generated token (consider setting for production)", { + operation: "security_warning", + note: "Auto-generated tokens are secure but not persistent across deployments" + }); + } else if (process.env.INTERNAL_AUTH_TOKEN.length < 32) { + securityIssues.push("INTERNAL_AUTH_TOKEN should be at least 32 characters in production"); + } + // Check database file encryption if (process.env.DB_FILE_ENCRYPTION === 'false') { securityIssues.push("Database file encryption should be enabled in production"); @@ -114,7 +124,14 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; const authManager = AuthManager.getInstance(); await authManager.initialize(); DataCrypto.initialize(); - systemLogger.info("Security system initialized (KEK-DEK architecture)", { + + // Initialize system crypto keys (JWT, Database, Internal Auth) + const systemCrypto = SystemCrypto.getInstance(); + await systemCrypto.initializeJWTSecret(); + await systemCrypto.initializeDatabaseKey(); + await systemCrypto.initializeInternalAuthToken(); + + systemLogger.info("Security system initialized (KEK-DEK architecture + SystemCrypto)", { operation: "security_init", }); diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index eea4b66f..2135d003 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -16,6 +16,7 @@ class SystemCrypto { private static instance: SystemCrypto; private jwtSecret: string | null = null; private databaseKey: Buffer | null = null; + private internalAuthToken: string | null = null; private constructor() {} @@ -109,6 +110,47 @@ class SystemCrypto { return this.databaseKey!; } + /** + * Initialize internal auth token - environment variable only + */ + async initializeInternalAuthToken(): Promise { + try { + databaseLogger.info("Initializing internal auth token", { + operation: "internal_auth_init", + }); + + // Check environment variable + const envToken = process.env.INTERNAL_AUTH_TOKEN; + if (envToken && envToken.length >= 32) { + this.internalAuthToken = envToken; + databaseLogger.info("✅ Using internal auth token from environment variable", { + operation: "internal_auth_env_loaded", + source: "environment" + }); + return; + } + + // No environment variable - generate and guide user + await this.generateAndGuideInternalAuthToken(); + + } catch (error) { + databaseLogger.error("Failed to initialize internal auth token", error, { + operation: "internal_auth_init_failed", + }); + throw new Error("Internal auth token initialization failed"); + } + } + + /** + * Get internal auth token + */ + async getInternalAuthToken(): Promise { + if (!this.internalAuthToken) { + await this.initializeInternalAuthToken(); + } + return this.internalAuthToken!; + } + /** * Generate and auto-save to .env file */ @@ -155,6 +197,27 @@ class SystemCrypto { }); } + /** + * Generate and auto-save internal auth token to .env file + */ + private async generateAndGuideInternalAuthToken(): Promise { + const newToken = crypto.randomBytes(32).toString('hex'); // 256-bit token for security + const instanceId = crypto.randomBytes(8).toString('hex'); + + // Set in memory for current session + this.internalAuthToken = newToken; + + // Auto-save to .env file + await this.updateEnvFile("INTERNAL_AUTH_TOKEN", newToken); + + databaseLogger.success("🔑 Internal auth token auto-generated and saved to .env", { + operation: "internal_auth_auto_generated", + instanceId, + envVarName: "INTERNAL_AUTH_TOKEN", + note: "Ready for use - no restart required" + }); + } + diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx index 687aff3f..abcd83b9 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx @@ -30,6 +30,8 @@ import { getCredentials, getSSHHosts, updateSSHHost, + enableAutoStart, + disableAutoStart, } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx"; @@ -436,22 +438,47 @@ export function HostManagerEditor({ submitData.keyType = data.keyType; } + let savedHost; if (editingHost && editingHost.id) { - const updatedHost = await updateSSHHost(editingHost.id, submitData); + savedHost = await updateSSHHost(editingHost.id, submitData); toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name })); - - if (onFormSubmit) { - onFormSubmit(updatedHost); - } } else { - const newHost = await createSSHHost(submitData); + savedHost = await createSSHHost(submitData); toast.success(t("hosts.hostAddedSuccessfully", { name: data.name })); + } - if (onFormSubmit) { - onFormSubmit(newHost); + // Handle AutoStart plaintext cache management + if (savedHost && savedHost.id && data.tunnelConnections) { + const hasAutoStartTunnels = data.tunnelConnections.some(tunnel => tunnel.autoStart); + + if (hasAutoStartTunnels) { + // User has enabled autoStart on some tunnels + // Need to ensure plaintext cache exists for this host + try { + await enableAutoStart(savedHost.id); + console.log(`AutoStart plaintext cache enabled for SSH host ${savedHost.id}`); + } catch (error) { + console.warn(`Failed to enable AutoStart plaintext cache for SSH host ${savedHost.id}:`, error); + // Don't fail the whole operation if cache setup fails + toast.warning(t("hosts.autoStartEnableFailed", { name: data.name })); + } + } else { + // User has disabled autoStart on all tunnels + // Clean up plaintext cache for this host + try { + await disableAutoStart(savedHost.id); + console.log(`AutoStart plaintext cache disabled for SSH host ${savedHost.id}`); + } catch (error) { + console.warn(`Failed to disable AutoStart plaintext cache for SSH host ${savedHost.id}:`, error); + // Don't fail the whole operation + } } } + if (onFormSubmit) { + onFormSubmit(savedHost); + } + window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); form.reset(); diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index 92ddf8d6..0b580dbd 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -13,7 +13,7 @@ import { getUserInfo, getRegistrationAllowed, getOIDCConfig, - getUserCount, + getSetupRequired, initiatePasswordReset, verifyPasswordResetCode, completePasswordReset, @@ -124,9 +124,9 @@ export function HomepageAuth({ }, []); useEffect(() => { - getUserCount() + getSetupRequired() .then((res) => { - if (res.count === 0) { + if (res.setup_required) { setFirstUser(true); setTab("signup"); } else { diff --git a/src/ui/Mobile/Homepage/HomepageAuth.tsx b/src/ui/Mobile/Homepage/HomepageAuth.tsx index 2c01c9a8..ae91956c 100644 --- a/src/ui/Mobile/Homepage/HomepageAuth.tsx +++ b/src/ui/Mobile/Homepage/HomepageAuth.tsx @@ -12,7 +12,7 @@ import { getUserInfo, getRegistrationAllowed, getOIDCConfig, - getUserCount, + getSetupRequired, initiatePasswordReset, verifyPasswordResetCode, completePasswordReset, @@ -111,9 +111,9 @@ export function HomepageAuth({ }, []); useEffect(() => { - getUserCount() + getSetupRequired() .then((res) => { - if (res.count === 0) { + if (res.setup_required) { setFirstUser(true); setTab("signup"); } else { diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 355b73b1..cbf6e90d 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -742,6 +742,48 @@ export async function getSSHHostById(hostId: number): Promise { } } +// ============================================================================ +// SSH AUTOSTART MANAGEMENT +// ============================================================================ + +export async function enableAutoStart(sshConfigId: number): Promise { + 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 { + 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 // ============================================================================ @@ -1453,6 +1495,15 @@ export async function getOIDCConfig(): Promise { } } +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 { try { const response = await authApi.get("/users/count");