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
This commit is contained in:
ZacharyZcR
2025-09-24 01:57:17 +08:00
parent cf6fed8d77
commit 8c004dfcfe
11 changed files with 479 additions and 49 deletions

View File

@@ -299,6 +299,7 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (host_id) REFERENCES ssh_data (id), FOREIGN KEY (host_id) REFERENCES ssh_data (id),
FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (user_id) REFERENCES users (id)
); );
`); `);
// Run schema migrations // Run schema migrations
@@ -435,6 +436,11 @@ const migrateSchema = () => {
"INTEGER REFERENCES ssh_credentials(id)", "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 // SSH credentials table migrations for encryption support
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");

View File

@@ -49,6 +49,11 @@ export const sshData = sqliteTable("ssh_data", {
keyPassword: text("key_password"), keyPassword: text("key_password"),
keyType: text("key_type"), 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), credentialId: integer("credential_id").references(() => sshCredentials.id),
enableTerminal: integer("enable_terminal", { mode: "boolean" }) enableTerminal: integer("enable_terminal", { mode: "boolean" })
.notNull() .notNull()
@@ -168,3 +173,4 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", {
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),
}); });

View File

@@ -8,13 +8,15 @@ import {
fileManagerPinned, fileManagerPinned,
fileManagerShortcuts, fileManagerShortcuts,
} from "../db/schema.js"; } 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 type { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import multer from "multer"; import multer from "multer";
import { sshLogger } from "../../utils/logger.js"; import { sshLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js";
import { AuthManager } from "../../utils/auth-manager.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(); 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"; 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) => { router.get("/db/host/internal", async (req: Request, res: Response) => {
if (!isLocalhost(req) && req.headers["x-internal-request"] !== "1") { try {
sshLogger.warn("Unauthorized attempt to access internal SSH host endpoint"); // 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" }); 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 { try {
// Internal endpoint - returns encrypted data (autostart will need user unlock) // Query sshData directly for hosts that have autostart plaintext fields populated
const data = await SimpleDBOps.selectEncrypted( const autostartHosts = await db.select()
db.select().from(sshData), .from(sshData)
"ssh_data", .where(
// Check if any autostart fields are populated (meaning autostart is enabled)
or(
isNotNull(sshData.autostartPassword),
isNotNull(sshData.autostartKey)
)
); );
const result = data.map((row: any) => {
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 { return {
...row, id: host.id,
tags: userId: host.userId,
typeof row.tags === "string" name: host.name || `autostart-${host.id}`,
? row.tags ip: host.ip,
? row.tags.split(",").filter(Boolean) port: host.port,
: [] username: host.username,
: [], password: host.autostartPassword,
pin: !!row.pin, key: host.autostartKey,
enableTerminal: !!row.enableTerminal, keyPassword: host.autostartKeyPassword,
enableTunnel: !!row.enableTunnel, authType: host.authType,
tunnelConnections: row.tunnelConnections enableTunnel: true,
? JSON.parse(row.tunnelConnections) tunnelConnections: tunnelConnections.filter((tunnel: any) => tunnel.autoStart),
: [], pin: false,
enableFileManager: !!row.enableFileManager, enableTerminal: false,
enableFileManager: false,
tags: ["autostart"],
}; };
}); });
res.json(result); res.json(result);
} catch (err) { } catch (err) {
sshLogger.error("Failed to fetch SSH data (internal)", err); sshLogger.error("Failed to fetch autostart SSH data", err);
res.status(500).json({ error: "Failed to fetch SSH data" }); 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; export default router;

View File

@@ -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 // GET /users/oidc-config
router.get("/oidc-config", authenticateJWT, async (req, res) => { router.get("/oidc-config", async (req, res) => {
try { try {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'") .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 // GET /users/count
router.get("/count", authenticateJWT, async (req, res) => { router.get("/count", authenticateJWT, async (req, res) => {
const userId = (req as any).userId;
try { 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 const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .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 // GET /users/registration-allowed
router.get("/registration-allowed", authenticateJWT, async (req, res) => { router.get("/registration-allowed", async (req, res) => {
try { try {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .prepare("SELECT value FROM settings WHERE key = 'allow_registration'")

View File

@@ -15,6 +15,7 @@ import type {
} from "../../types/index.js"; } from "../../types/index.js";
import { CONNECTION_STATES } from "../../types/index.js"; import { CONNECTION_STATES } from "../../types/index.js";
import { tunnelLogger } from "../utils/logger.js"; import { tunnelLogger } from "../utils/logger.js";
import { SystemCrypto } from "../utils/system-crypto.js";
const app = express(); const app = express();
app.use( app.use(
@@ -1023,12 +1024,16 @@ app.post("/ssh/tunnel/cancel", (req, res) => {
async function initializeAutoStartTunnels(): Promise<void> { async function initializeAutoStartTunnels(): Promise<void> {
try { try {
// Get internal auth token from SystemCrypto
const systemCrypto = SystemCrypto.getInstance();
const internalAuthToken = await systemCrypto.getInternalAuthToken();
const response = await axios.get( const response = await axios.get(
"http://localhost:8081/ssh/db/host/internal", "http://localhost:8081/ssh/db/host/internal",
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Internal-Request": "1", "X-Internal-Auth-Token": internalAuthToken,
}, },
}, },
); );

View File

@@ -8,6 +8,7 @@ import path from "path";
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js"; import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
import { AuthManager } from "./utils/auth-manager.js"; import { AuthManager } from "./utils/auth-manager.js";
import { DataCrypto } from "./utils/data-crypto.js"; import { DataCrypto } from "./utils/data-crypto.js";
import { SystemCrypto } from "./utils/system-crypto.js";
import { systemLogger, versionLogger } from "./utils/logger.js"; import { systemLogger, versionLogger } from "./utils/logger.js";
(async () => { (async () => {
@@ -74,6 +75,15 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
securityIssues.push("DATABASE_KEY should be at least 64 characters in production"); 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 // Check database file encryption
if (process.env.DB_FILE_ENCRYPTION === 'false') { if (process.env.DB_FILE_ENCRYPTION === 'false') {
securityIssues.push("Database file encryption should be enabled in production"); 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(); const authManager = AuthManager.getInstance();
await authManager.initialize(); await authManager.initialize();
DataCrypto.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", operation: "security_init",
}); });

View File

@@ -16,6 +16,7 @@ class SystemCrypto {
private static instance: SystemCrypto; private static instance: SystemCrypto;
private jwtSecret: string | null = null; private jwtSecret: string | null = null;
private databaseKey: Buffer | null = null; private databaseKey: Buffer | null = null;
private internalAuthToken: string | null = null;
private constructor() {} private constructor() {}
@@ -109,6 +110,47 @@ class SystemCrypto {
return this.databaseKey!; 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 * 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"
});
}

View File

@@ -30,6 +30,8 @@ import {
getCredentials, getCredentials,
getSSHHosts, getSSHHosts,
updateSSHHost, updateSSHHost,
enableAutoStart,
disableAutoStart,
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx"; import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
@@ -436,20 +438,45 @@ export function HostManagerEditor({
submitData.keyType = data.keyType; submitData.keyType = data.keyType;
} }
let savedHost;
if (editingHost && editingHost.id) { 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 })); toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name }));
} else {
savedHost = await createSSHHost(submitData);
toast.success(t("hosts.hostAddedSuccessfully", { name: data.name }));
}
if (onFormSubmit) { // Handle AutoStart plaintext cache management
onFormSubmit(updatedHost); 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 { } else {
const newHost = await createSSHHost(submitData); // User has disabled autoStart on all tunnels
toast.success(t("hosts.hostAddedSuccessfully", { name: data.name })); // 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) { if (onFormSubmit) {
onFormSubmit(newHost); onFormSubmit(savedHost);
}
} }
window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));

View File

@@ -13,7 +13,7 @@ import {
getUserInfo, getUserInfo,
getRegistrationAllowed, getRegistrationAllowed,
getOIDCConfig, getOIDCConfig,
getUserCount, getSetupRequired,
initiatePasswordReset, initiatePasswordReset,
verifyPasswordResetCode, verifyPasswordResetCode,
completePasswordReset, completePasswordReset,
@@ -124,9 +124,9 @@ export function HomepageAuth({
}, []); }, []);
useEffect(() => { useEffect(() => {
getUserCount() getSetupRequired()
.then((res) => { .then((res) => {
if (res.count === 0) { if (res.setup_required) {
setFirstUser(true); setFirstUser(true);
setTab("signup"); setTab("signup");
} else { } else {

View File

@@ -12,7 +12,7 @@ import {
getUserInfo, getUserInfo,
getRegistrationAllowed, getRegistrationAllowed,
getOIDCConfig, getOIDCConfig,
getUserCount, getSetupRequired,
initiatePasswordReset, initiatePasswordReset,
verifyPasswordResetCode, verifyPasswordResetCode,
completePasswordReset, completePasswordReset,
@@ -111,9 +111,9 @@ export function HomepageAuth({
}, []); }, []);
useEffect(() => { useEffect(() => {
getUserCount() getSetupRequired()
.then((res) => { .then((res) => {
if (res.count === 0) { if (res.setup_required) {
setFirstUser(true); setFirstUser(true);
setTab("signup"); setTab("signup");
} else { } else {

View File

@@ -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 // 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> { export async function getUserCount(): Promise<UserCount> {
try { try {
const response = await authApi.get("/users/count"); const response = await authApi.get("/users/count");