dev-1.7.0 #294
@@ -299,6 +299,7 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
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");
|
||||
|
||||
@@ -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`),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -742,6 +742,48 @@ export async function getSSHHostById(hostId: number): Promise<SSHHost> {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
// ============================================================================
|
||||
@@ -1453,6 +1495,15 @@ export async function getOIDCConfig(): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user