Code cleanup

This commit is contained in:
LukeGus
2025-09-28 21:36:20 -05:00
parent 58b28cab50
commit 63e776f183
15 changed files with 467 additions and 535 deletions

View File

@@ -3,10 +3,10 @@ const path = require("path");
const fs = require("fs"); const fs = require("fs");
const os = require("os"); const os = require("os");
app.commandLine.appendSwitch('--ignore-certificate-errors'); app.commandLine.appendSwitch("--ignore-certificate-errors");
app.commandLine.appendSwitch('--ignore-ssl-errors'); app.commandLine.appendSwitch("--ignore-ssl-errors");
app.commandLine.appendSwitch('--ignore-certificate-errors-spki-list'); app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
app.commandLine.appendSwitch('--enable-features=NetworkService'); app.commandLine.appendSwitch("--enable-features=NetworkService");
let mainWindow = null; let mainWindow = null;
@@ -141,9 +141,9 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
requestOptions.rejectUnauthorized = false; requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({ requestOptions.agent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
secureProtocol: 'TLSv1_2_method', secureProtocol: "TLSv1_2_method",
checkServerIdentity: () => undefined, checkServerIdentity: () => undefined,
ciphers: 'ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH', ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
honorCipherOrder: true, honorCipherOrder: true,
}); });
} }
@@ -315,9 +315,9 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
requestOptions.rejectUnauthorized = false; requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({ requestOptions.agent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
secureProtocol: 'TLSv1_2_method', secureProtocol: "TLSv1_2_method",
checkServerIdentity: () => undefined, checkServerIdentity: () => undefined,
ciphers: 'ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH', ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
honorCipherOrder: true, honorCipherOrder: true,
}); });
} }

View File

@@ -245,7 +245,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
} catch { } catch {
return null; return null;
} }
} },
]; ];
for (const getVersion of versionSources) { for (const getVersion of versionSources) {

View File

@@ -1128,93 +1128,79 @@ async function deploySSHKeyToHost(
conn.on("ready", async () => { conn.on("ready", async () => {
clearTimeout(connectionTimeout); clearTimeout(connectionTimeout);
authLogger.info("SSH connection established for key deployment", {
host: hostConfig.ip,
username: hostConfig.username,
authType: hostConfig.authType,
});
try { try {
authLogger.info("Ensuring .ssh directory exists", { host: hostConfig.ip });
await new Promise<void>((resolveCmd, rejectCmd) => { await new Promise<void>((resolveCmd, rejectCmd) => {
const cmdTimeout = setTimeout(() => { const cmdTimeout = setTimeout(() => {
rejectCmd(new Error("mkdir command timeout")); rejectCmd(new Error("mkdir command timeout"));
}, 10000); // Reduced to 10 seconds }, 10000);
// Use a more robust command that handles existing directories conn.exec(
conn.exec("test -d ~/.ssh || mkdir -p ~/.ssh; chmod 700 ~/.ssh", (err, stream) => { "test -d ~/.ssh || mkdir -p ~/.ssh; chmod 700 ~/.ssh",
if (err) { (err, stream) => {
clearTimeout(cmdTimeout); if (err) {
authLogger.error("mkdir command error", { host: hostConfig.ip, error: err.message }); clearTimeout(cmdTimeout);
return rejectCmd(err); return rejectCmd(err);
}
stream.on("close", (code) => {
clearTimeout(cmdTimeout);
authLogger.info("mkdir command completed", { host: hostConfig.ip, code });
if (code === 0) {
resolveCmd();
} else {
rejectCmd(new Error(`mkdir command failed with code ${code}`));
} }
});
stream.on("data", (data) => { stream.on("close", (code) => {
authLogger.info("mkdir command output", { host: hostConfig.ip, output: data.toString() }); clearTimeout(cmdTimeout);
}); if (code === 0) {
}); resolveCmd();
} else {
rejectCmd(
new Error(`mkdir command failed with code ${code}`),
);
}
});
stream.on("data", (data) => {});
},
);
}); });
const keyExists = await new Promise<boolean>( const keyExists = await new Promise<boolean>(
(resolveCheck, rejectCheck) => { (resolveCheck, rejectCheck) => {
const checkTimeout = setTimeout(() => { const checkTimeout = setTimeout(() => {
rejectCheck(new Error("Key check timeout")); rejectCheck(new Error("Key check timeout"));
}, 5000); // Reduced to 5 seconds }, 5000);
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey; let actualPublicKey = publicKey;
try { try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey); const parsed = JSON.parse(publicKey);
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
authLogger.info("Parsed public key from JSON format", { host: hostConfig.ip });
} }
} catch (e) { } catch (e) {}
// Not JSON, use as-is
authLogger.info("Using public key as plain text", { host: hostConfig.ip });
}
// Validate public key format
const keyParts = actualPublicKey.trim().split(" "); const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) { if (keyParts.length < 2) {
clearTimeout(checkTimeout); clearTimeout(checkTimeout);
authLogger.error("Invalid public key format", { host: hostConfig.ip, publicKey: actualPublicKey.substring(0, 50) + "..." }); return rejectCheck(
return rejectCheck(new Error("Invalid public key format - must contain at least 2 parts")); new Error(
"Invalid public key format - must contain at least 2 parts",
),
);
} }
const keyPattern = keyParts[1]; const keyPattern = keyParts[1];
authLogger.info("Checking for existing key", { host: hostConfig.ip, keyPattern: keyPattern.substring(0, 20) + "..." });
// Use a simpler approach - just check if the file exists and has content
conn.exec( conn.exec(
`if [ -f ~/.ssh/authorized_keys ]; then grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?; else echo 1; fi`, `if [ -f ~/.ssh/authorized_keys ]; then grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?; else echo 1; fi`,
(err, stream) => { (err, stream) => {
if (err) { if (err) {
clearTimeout(checkTimeout); clearTimeout(checkTimeout);
authLogger.error("Key check error", { host: hostConfig.ip, error: err.message });
return rejectCheck(err); return rejectCheck(err);
} }
let output = ''; let output = "";
stream.on('data', (data) => { stream.on("data", (data) => {
output += data.toString(); output += data.toString();
}); });
stream.on("close", (code) => { stream.on("close", (code) => {
clearTimeout(checkTimeout); clearTimeout(checkTimeout);
const exists = output.trim() === '0'; const exists = output.trim() === "0";
authLogger.info("Key check completed", { host: hostConfig.ip, code, output: output.trim(), exists });
resolveCheck(exists); resolveCheck(exists);
}); });
}, },
@@ -1228,40 +1214,33 @@ async function deploySSHKeyToHost(
return; return;
} }
authLogger.info("Adding SSH key to authorized_keys", { host: hostConfig.ip });
await new Promise<void>((resolveAdd, rejectAdd) => { await new Promise<void>((resolveAdd, rejectAdd) => {
const addTimeout = setTimeout(() => { const addTimeout = setTimeout(() => {
rejectAdd(new Error("Key add timeout")); rejectAdd(new Error("Key add timeout"));
}, 10000); // Reduced to 10 seconds }, 10000);
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey; let actualPublicKey = publicKey;
try { try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey); const parsed = JSON.parse(publicKey);
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
} }
} catch (e) { } catch (e) {}
// Not JSON, use as-is
} const escapedKey = actualPublicKey
.replace(/\\/g, "\\\\")
.replace(/'/g, "'\\''");
// Use printf instead of echo for more reliable key addition
const escapedKey = actualPublicKey.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
authLogger.info("Adding key to authorized_keys", { host: hostConfig.ip, keyLength: actualPublicKey.length });
conn.exec( conn.exec(
`printf '%s\\n' '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, `printf '%s\\n' '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => { (err, stream) => {
if (err) { if (err) {
clearTimeout(addTimeout); clearTimeout(addTimeout);
authLogger.error("Key add error", { host: hostConfig.ip, error: err.message });
return rejectAdd(err); return rejectAdd(err);
} }
stream.on("close", (code) => { stream.on("close", (code) => {
clearTimeout(addTimeout); clearTimeout(addTimeout);
authLogger.info("Key add completed", { host: hostConfig.ip, code });
if (code === 0) { if (code === 0) {
resolveAdd(); resolveAdd();
} else { } else {
@@ -1270,39 +1249,32 @@ async function deploySSHKeyToHost(
); );
} }
}); });
stream.on("data", (data) => {
authLogger.info("Key add output", { host: hostConfig.ip, output: data.toString() });
});
}, },
); );
}); });
authLogger.info("Verifying key deployment", { host: hostConfig.ip });
const verifySuccess = await new Promise<boolean>( const verifySuccess = await new Promise<boolean>(
(resolveVerify, rejectVerify) => { (resolveVerify, rejectVerify) => {
const verifyTimeout = setTimeout(() => { const verifyTimeout = setTimeout(() => {
rejectVerify(new Error("Key verification timeout")); rejectVerify(new Error("Key verification timeout"));
}, 5000); // Reduced to 5 seconds }, 5000);
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey; let actualPublicKey = publicKey;
try { try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey); const parsed = JSON.parse(publicKey);
if (parsed.data) { if (parsed.data) {
actualPublicKey = parsed.data; actualPublicKey = parsed.data;
} }
} catch (e) { } catch (e) {}
// Not JSON, use as-is
}
// Use the same key pattern extraction as above
const keyParts = actualPublicKey.trim().split(" "); const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) { if (keyParts.length < 2) {
clearTimeout(verifyTimeout); clearTimeout(verifyTimeout);
authLogger.error("Invalid public key format for verification", { host: hostConfig.ip, publicKey: actualPublicKey.substring(0, 50) + "..." }); return rejectVerify(
return rejectVerify(new Error("Invalid public key format - must contain at least 2 parts")); new Error(
"Invalid public key format - must contain at least 2 parts",
),
);
} }
const keyPattern = keyParts[1]; const keyPattern = keyParts[1];
@@ -1311,19 +1283,17 @@ async function deploySSHKeyToHost(
(err, stream) => { (err, stream) => {
if (err) { if (err) {
clearTimeout(verifyTimeout); clearTimeout(verifyTimeout);
authLogger.error("Key verification error", { host: hostConfig.ip, error: err.message });
return rejectVerify(err); return rejectVerify(err);
} }
let output = ''; let output = "";
stream.on('data', (data) => { stream.on("data", (data) => {
output += data.toString(); output += data.toString();
}); });
stream.on("close", (code) => { stream.on("close", (code) => {
clearTimeout(verifyTimeout); clearTimeout(verifyTimeout);
const verified = output.trim() === '0'; const verified = output.trim() === "0";
authLogger.info("Key verification completed", { host: hostConfig.ip, code, output: output.trim(), verified });
resolveVerify(verified); resolveVerify(verified);
}); });
}, },
@@ -1353,30 +1323,31 @@ async function deploySSHKeyToHost(
conn.on("error", (err) => { conn.on("error", (err) => {
clearTimeout(connectionTimeout); clearTimeout(connectionTimeout);
let errorMessage = err.message; let errorMessage = err.message;
// Log detailed error information for debugging if (
authLogger.error("SSH connection failed during key deployment", { err.message.includes("All configured authentication methods failed")
host: hostConfig.ip, ) {
username: hostConfig.username, errorMessage =
authType: hostConfig.authType, "Authentication failed. Please check your credentials and ensure the SSH service is running.";
hasPassword: !!hostConfig.password, } else if (
hasPrivateKey: !!hostConfig.privateKey, err.message.includes("ENOTFOUND") ||
error: err.message, err.message.includes("ENOENT")
errorCode: (err as any).code, ) {
});
if (err.message.includes("All configured authentication methods failed")) {
errorMessage = "Authentication failed. Please check your credentials and ensure the SSH service is running.";
} else if (err.message.includes("ENOTFOUND") || err.message.includes("ENOENT")) {
errorMessage = "Could not resolve hostname or connect to server."; errorMessage = "Could not resolve hostname or connect to server.";
} else if (err.message.includes("ECONNREFUSED")) { } else if (err.message.includes("ECONNREFUSED")) {
errorMessage = "Connection refused. The server may not be running or the port may be incorrect."; errorMessage =
"Connection refused. The server may not be running or the port may be incorrect.";
} else if (err.message.includes("ETIMEDOUT")) { } else if (err.message.includes("ETIMEDOUT")) {
errorMessage = "Connection timed out. Check your network connection and server availability."; errorMessage =
} else if (err.message.includes("authentication failed") || err.message.includes("Permission denied")) { "Connection timed out. Check your network connection and server availability.";
errorMessage = "Authentication failed. Please check your username and password/key."; } else if (
err.message.includes("authentication failed") ||
err.message.includes("Permission denied")
) {
errorMessage =
"Authentication failed. Please check your username and password/key.";
} }
resolve({ success: false, error: errorMessage }); resolve({ success: false, error: errorMessage });
}); });
@@ -1462,24 +1433,9 @@ async function deploySSHKeyToHost(
return; return;
} }
// Log connection attempt
authLogger.info("Attempting SSH connection for key deployment", {
host: connectionConfig.host,
port: connectionConfig.port,
username: connectionConfig.username,
authType: hostConfig.authType,
hasPassword: !!connectionConfig.password,
hasPrivateKey: !!connectionConfig.privateKey,
hasPassphrase: !!connectionConfig.passphrase,
});
conn.connect(connectionConfig); conn.connect(connectionConfig);
} catch (error) { } catch (error) {
clearTimeout(connectionTimeout); clearTimeout(connectionTimeout);
authLogger.error("Failed to initiate SSH connection", {
host: hostConfig.ip,
error: error instanceof Error ? error.message : "Unknown error",
});
resolve({ resolve({
success: false, success: false,
error: error instanceof Error ? error.message : "Connection failed", error: error instanceof Error ? error.message : "Connection failed",
@@ -1547,11 +1503,7 @@ router.post(
}); });
} }
const targetHost = await SimpleDBOps.select( const targetHost = await SimpleDBOps.select(
db db.select().from(sshData).where(eq(sshData.id, targetHostId)).limit(1),
.select()
.from(sshData)
.where(eq(sshData.id, targetHostId))
.limit(1),
"ssh_data", "ssh_data",
userId, userId,
); );
@@ -1575,25 +1527,9 @@ router.post(
keyPassword: hostData.keyPassword, keyPassword: hostData.keyPassword,
}; };
authLogger.info("Host configuration for SSH key deployment", {
hostId: targetHostId,
ip: hostConfig.ip,
port: hostConfig.port,
username: hostConfig.username,
authType: hostConfig.authType,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKey,
hasKeyPassword: !!hostConfig.keyPassword,
passwordLength: hostConfig.password ? hostConfig.password.length : 0,
});
if (hostData.authType === "credential" && hostData.credentialId) { if (hostData.authType === "credential" && hostData.credentialId) {
const userId = (req as any).userId; const userId = (req as any).userId;
if (!userId) { if (!userId) {
authLogger.error("Missing userId for credential resolution", {
hostId: targetHostId,
credentialId: hostData.credentialId,
});
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: "Authentication required for credential resolution", error: "Authentication required for credential resolution",
@@ -1624,32 +1560,13 @@ router.post(
hostConfig.privateKey = cred.privateKey || cred.key; hostConfig.privateKey = cred.privateKey || cred.key;
hostConfig.keyPassword = cred.keyPassword; hostConfig.keyPassword = cred.keyPassword;
} }
authLogger.info("Resolved host credentials for SSH key deployment", {
hostId: targetHostId,
credentialId: hostData.credentialId,
authType: hostConfig.authType,
username: hostConfig.username,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKey,
hasKeyPassword: !!hostConfig.keyPassword,
});
} else { } else {
authLogger.error("Host credential not found", {
hostId: targetHostId,
credentialId: hostData.credentialId,
});
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: "Host credential not found", error: "Host credential not found",
}); });
} }
} catch (error) { } catch (error) {
authLogger.error("Failed to resolve host credentials", {
hostId: targetHostId,
credentialId: hostData.credentialId,
error: error instanceof Error ? error.message : "Unknown error",
});
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: "Failed to resolve host credentials", error: "Failed to resolve host credentials",
@@ -1664,31 +1581,17 @@ router.post(
); );
if (deployResult.success) { if (deployResult.success) {
authLogger.success(`SSH key deployed successfully`, {
credentialId,
targetHostId,
operation: "deploy_ssh_key",
});
res.json({ res.json({
success: true, success: true,
message: deployResult.message || "SSH key deployed successfully", message: deployResult.message || "SSH key deployed successfully",
}); });
} else { } else {
authLogger.error(`SSH key deployment failed`, {
credentialId,
targetHostId,
error: deployResult.error,
operation: "deploy_ssh_key",
});
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: deployResult.error || "Deployment failed", error: deployResult.error || "Deployment failed",
}); });
} }
} catch (error) { } catch (error) {
authLogger.error("Failed to deploy SSH key", error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: error:

View File

@@ -926,8 +926,10 @@ router.post("/login", async (req, res) => {
username: userRecord.username, username: userRecord.username,
}; };
const isElectron = req.headers['x-electron-app'] === 'true' || req.headers['X-Electron-App'] === 'true'; const isElectron =
req.headers["x-electron-app"] === "true" ||
req.headers["X-Electron-App"] === "true";
if (isElectron) { if (isElectron) {
response.token = token; response.token = token;
} }
@@ -1507,7 +1509,7 @@ router.post("/totp/verify-login", async (req, res) => {
success: true, success: true,
is_admin: !!userRecord.is_admin, is_admin: !!userRecord.is_admin,
username: userRecord.username, username: userRecord.username,
token: req.headers['x-electron-app'] === 'true' ? token : undefined, token: req.headers["x-electron-app"] === "true" ? token : undefined,
}); });
} catch (err) { } catch (err) {
authLogger.error("TOTP verification failed", err); authLogger.error("TOTP verification failed", err);

View File

@@ -9,10 +9,9 @@ import { fileLogger } 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";
function isExecutableFile(permissions: string, fileName: string): boolean { function isExecutableFile(permissions: string, fileName: string): boolean {
const hasExecutePermission = const hasExecutePermission =
permissions[3] === "x" || permissions[6] === "x" || permissions[9] === "x"; permissions[3] === "x" || permissions[6] === "x" || permissions[9] === "x";
const scriptExtensions = [ const scriptExtensions = [
".sh", ".sh",
@@ -26,59 +25,59 @@ function isExecutableFile(permissions: string, fileName: string): boolean {
".fish", ".fish",
]; ];
const hasScriptExtension = scriptExtensions.some((ext) => const hasScriptExtension = scriptExtensions.some((ext) =>
fileName.toLowerCase().endsWith(ext), fileName.toLowerCase().endsWith(ext),
); );
const executableExtensions = [".bin", ".exe", ".out"]; const executableExtensions = [".bin", ".exe", ".out"];
const hasExecutableExtension = executableExtensions.some((ext) => const hasExecutableExtension = executableExtensions.some((ext) =>
fileName.toLowerCase().endsWith(ext), fileName.toLowerCase().endsWith(ext),
); );
const hasNoExtension = !fileName.includes(".") && hasExecutePermission; const hasNoExtension = !fileName.includes(".") && hasExecutePermission;
return ( return (
hasExecutePermission && hasExecutePermission &&
(hasScriptExtension || hasExecutableExtension || hasNoExtension) (hasScriptExtension || hasExecutableExtension || hasNoExtension)
); );
} }
const app = express(); const app = express();
app.use( app.use(
cors({ cors({
origin: (origin, callback) => { origin: (origin, callback) => {
if (!origin) return callback(null, true); if (!origin) return callback(null, true);
const allowedOrigins = [ const allowedOrigins = [
"http://localhost:5173", "http://localhost:5173",
"http://localhost:3000", "http://localhost:3000",
"http://127.0.0.1:5173", "http://127.0.0.1:5173",
"http://127.0.0.1:3000", "http://127.0.0.1:3000",
]; ];
if (origin.startsWith("https://")) { if (origin.startsWith("https://")) {
return callback(null, true); return callback(null, true);
} }
if (origin.startsWith("http://")) { if (origin.startsWith("http://")) {
return callback(null, true); return callback(null, true);
} }
if (allowedOrigins.includes(origin)) { if (allowedOrigins.includes(origin)) {
return callback(null, true); return callback(null, true);
} }
callback(new Error("Not allowed by CORS")); callback(new Error("Not allowed by CORS"));
}, },
credentials: true, credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [ allowedHeaders: [
"Content-Type", "Content-Type",
"Authorization", "Authorization",
"User-Agent", "User-Agent",
"X-Electron-App", "X-Electron-App",
], ],
}), }),
); );
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json({ limit: "1gb" })); app.use(express.json({ limit: "1gb" }));
@@ -88,8 +87,6 @@ app.use(express.raw({ limit: "5gb", type: "application/octet-stream" }));
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware()); app.use(authManager.createAuthMiddleware());
interface SSHSession { interface SSHSession {
client: SSHClient; client: SSHClient;
isConnected: boolean; isConnected: boolean;
@@ -116,10 +113,10 @@ function scheduleSessionCleanup(sessionId: string) {
if (session.timeout) clearTimeout(session.timeout); if (session.timeout) clearTimeout(session.timeout);
session.timeout = setTimeout( session.timeout = setTimeout(
() => { () => {
cleanupSession(sessionId); cleanupSession(sessionId);
}, },
30 * 60 * 1000, 30 * 60 * 1000,
); );
} }
} }
@@ -144,7 +141,6 @@ function getMimeType(fileName: string): string {
return mimeTypes[ext || ""] || "application/octet-stream"; return mimeTypes[ext || ""] || "application/octet-stream";
} }
app.post("/ssh/file_manager/ssh/connect", async (req, res) => { app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
const { const {
sessionId, sessionId,
@@ -189,17 +185,17 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
if (credentialId && hostId && userId) { if (credentialId && hostId && userId) {
try { try {
const credentials = await SimpleDBOps.select( const credentials = await SimpleDBOps.select(
getDb() getDb()
.select() .select()
.from(sshCredentials) .from(sshCredentials)
.where( .where(
and( and(
eq(sshCredentials.id, credentialId), eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, userId), eq(sshCredentials.userId, userId),
), ),
), ),
"ssh_credentials", "ssh_credentials",
userId, userId,
); );
if (credentials.length > 0) { if (credentials.length > 0) {
@@ -228,13 +224,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
} }
} else if (credentialId && hostId) { } else if (credentialId && hostId) {
fileLogger.warn( fileLogger.warn(
"Missing userId for credential resolution in file manager", "Missing userId for credential resolution in file manager",
{ {
operation: "ssh_credentials", operation: "ssh_credentials",
hostId, hostId,
credentialId, credentialId,
hasUserId: !!userId, hasUserId: !!userId,
}, },
); );
} }
@@ -279,21 +275,29 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
}, },
}; };
if (resolvedCredentials.authType === "password" && resolvedCredentials.password && resolvedCredentials.password.trim()) { if (
resolvedCredentials.authType === "password" &&
resolvedCredentials.password &&
resolvedCredentials.password.trim()
) {
config.password = resolvedCredentials.password; config.password = resolvedCredentials.password;
} else if (resolvedCredentials.authType === "key" && resolvedCredentials.sshKey && resolvedCredentials.sshKey.trim()) { } else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.sshKey &&
resolvedCredentials.sshKey.trim()
) {
try { try {
if ( if (
!resolvedCredentials.sshKey.includes("-----BEGIN") || !resolvedCredentials.sshKey.includes("-----BEGIN") ||
!resolvedCredentials.sshKey.includes("-----END") !resolvedCredentials.sshKey.includes("-----END")
) { ) {
throw new Error("Invalid private key format"); throw new Error("Invalid private key format");
} }
const cleanKey = resolvedCredentials.sshKey const cleanKey = resolvedCredentials.sshKey
.trim() .trim()
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.replace(/\r/g, "\n"); .replace(/\r/g, "\n");
config.privateKey = Buffer.from(cleanKey, "utf8"); config.privateKey = Buffer.from(cleanKey, "utf8");
@@ -309,17 +313,20 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
return res.status(400).json({ error: "Invalid SSH key format" }); return res.status(400).json({ error: "Invalid SSH key format" });
} }
} else { } else {
fileLogger.warn("No valid authentication method provided for file manager", { fileLogger.warn(
operation: "file_connect", "No valid authentication method provided for file manager",
sessionId, {
hostId, operation: "file_connect",
authType: resolvedCredentials.authType, sessionId,
hasPassword: !!resolvedCredentials.password, hostId,
hasKey: !!resolvedCredentials.sshKey, authType: resolvedCredentials.authType,
}); hasPassword: !!resolvedCredentials.password,
hasKey: !!resolvedCredentials.sshKey,
},
);
return res return res
.status(400) .status(400)
.json({ error: "Either password or SSH key must be provided" }); .json({ error: "Either password or SSH key must be provided" });
} }
let responseSent = false; let responseSent = false;
@@ -359,14 +366,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
client.connect(config); client.connect(config);
}); });
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body; const { sessionId } = req.body;
cleanupSession(sessionId); cleanupSession(sessionId);
res.json({ status: "success", message: "SSH connection disconnected" }); res.json({ status: "success", message: "SSH connection disconnected" });
}); });
app.get("/ssh/file_manager/ssh/status", (req, res) => { app.get("/ssh/file_manager/ssh/status", (req, res) => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const isConnected = !!sshSessions[sessionId]?.isConnected; const isConnected = !!sshSessions[sessionId]?.isConnected;
@@ -400,7 +405,6 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
}); });
}); });
app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
@@ -437,7 +441,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
stream.on("close", (code) => { stream.on("close", (code) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
return res.status(500).json({ error: `Command failed: ${errorData}` }); return res.status(500).json({ error: `Command failed: ${errorData}` });
} }
@@ -487,9 +491,9 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
linkTarget, linkTarget,
path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${actualName}`, path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${actualName}`,
executable: executable:
!isDirectory && !isLink !isDirectory && !isLink
? isExecutableFile(permissions, actualName) ? isExecutableFile(permissions, actualName)
: false, : false,
}); });
} }
} }
@@ -499,7 +503,6 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
}); });
}); });
app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
@@ -542,7 +545,7 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
stream.on("close", (code) => { stream.on("close", (code) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH identifySymlink command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH identifySymlink command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
return res.status(500).json({ error: `Command failed: ${errorData}` }); return res.status(500).json({ error: `Command failed: ${errorData}` });
} }
@@ -553,8 +556,8 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
path: linkPath, path: linkPath,
target: target, target: target,
type: fileType.toLowerCase().includes("directory") type: fileType.toLowerCase().includes("directory")
? "directory" ? "directory"
: "file", : "file",
}); });
}); });
@@ -567,7 +570,6 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
}); });
}); });
app.get("/ssh/file_manager/ssh/readFile", (req, res) => { app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
@@ -591,106 +593,105 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const escapedPath = filePath.replace(/'/g, "'\"'\"'"); const escapedPath = filePath.replace(/'/g, "'\"'\"'");
sshConn.client.exec( sshConn.client.exec(
`stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`, `stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`,
(sizeErr, sizeStream) => { (sizeErr, sizeStream) => {
if (sizeErr) { if (sizeErr) {
fileLogger.error("SSH file size check error:", sizeErr); fileLogger.error("SSH file size check error:", sizeErr);
return res.status(500).json({ error: sizeErr.message }); return res.status(500).json({ error: sizeErr.message });
}
let sizeData = "";
let sizeErrorData = "";
sizeStream.on("data", (chunk: Buffer) => {
sizeData += chunk.toString();
});
sizeStream.stderr.on("data", (chunk: Buffer) => {
sizeErrorData += chunk.toString();
});
sizeStream.on("close", (sizeCode) => {
if (sizeCode !== 0) {
const errorLower = sizeErrorData.toLowerCase();
const isFileNotFound =
errorLower.includes("no such file or directory") ||
errorLower.includes("cannot access") ||
errorLower.includes("not found") ||
errorLower.includes("resource not found");
fileLogger.error(`File size check failed: ${sizeErrorData}`);
return res.status(isFileNotFound ? 404 : 500).json({
error: `Cannot check file size: ${sizeErrorData}`,
fileNotFound: isFileNotFound,
});
} }
let sizeData = ""; const fileSize = parseInt(sizeData.trim(), 10);
let sizeErrorData = "";
sizeStream.on("data", (chunk: Buffer) => { if (isNaN(fileSize)) {
sizeData += chunk.toString(); fileLogger.error("Invalid file size response:", sizeData);
}); return res.status(500).json({ error: "Cannot determine file size" });
}
sizeStream.stderr.on("data", (chunk: Buffer) => { if (fileSize > MAX_READ_SIZE) {
sizeErrorData += chunk.toString(); fileLogger.warn("File too large for reading", {
}); operation: "file_read",
sessionId,
filePath,
fileSize,
maxSize: MAX_READ_SIZE,
});
return res.status(400).json({
error: `File too large to open in editor. Maximum size is ${MAX_READ_SIZE / 1024 / 1024}MB, file is ${(fileSize / 1024 / 1024).toFixed(2)}MB. Use download instead.`,
fileSize,
maxSize: MAX_READ_SIZE,
tooLarge: true,
});
}
sizeStream.on("close", (sizeCode) => { sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => {
if (sizeCode !== 0) { if (err) {
const errorLower = sizeErrorData.toLowerCase(); fileLogger.error("SSH readFile error:", err);
const isFileNotFound = return res.status(500).json({ error: err.message });
errorLower.includes("no such file or directory") ||
errorLower.includes("cannot access") ||
errorLower.includes("not found") ||
errorLower.includes("resource not found");
fileLogger.error(`File size check failed: ${sizeErrorData}`);
return res.status(isFileNotFound ? 404 : 500).json({
error: `Cannot check file size: ${sizeErrorData}`,
fileNotFound: isFileNotFound,
});
} }
const fileSize = parseInt(sizeData.trim(), 10); let data = "";
let errorData = "";
if (isNaN(fileSize)) { stream.on("data", (chunk: Buffer) => {
fileLogger.error("Invalid file size response:", sizeData); data += chunk.toString();
return res.status(500).json({ error: "Cannot determine file size" }); });
}
if (fileSize > MAX_READ_SIZE) { stream.stderr.on("data", (chunk: Buffer) => {
fileLogger.warn("File too large for reading", { errorData += chunk.toString();
operation: "file_read", });
sessionId,
filePath,
fileSize,
maxSize: MAX_READ_SIZE,
});
return res.status(400).json({
error: `File too large to open in editor. Maximum size is ${MAX_READ_SIZE / 1024 / 1024}MB, file is ${(fileSize / 1024 / 1024).toFixed(2)}MB. Use download instead.`,
fileSize,
maxSize: MAX_READ_SIZE,
tooLarge: true,
});
}
sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { stream.on("close", (code) => {
if (err) { if (code !== 0) {
fileLogger.error("SSH readFile error:", err); fileLogger.error(
return res.status(500).json({ error: err.message }); `SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
);
const isFileNotFound =
errorData.includes("No such file or directory") ||
errorData.includes("cannot access") ||
errorData.includes("not found");
return res.status(isFileNotFound ? 404 : 500).json({
error: `Command failed: ${errorData}`,
fileNotFound: isFileNotFound,
});
} }
let data = ""; res.json({ content: data, path: filePath });
let errorData = "";
stream.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
stream.stderr.on("data", (chunk: Buffer) => {
errorData += chunk.toString();
});
stream.on("close", (code) => {
if (code !== 0) {
fileLogger.error(
`SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
);
const isFileNotFound =
errorData.includes("No such file or directory") ||
errorData.includes("cannot access") ||
errorData.includes("not found");
return res.status(isFileNotFound ? 404 : 500).json({
error: `Command failed: ${errorData}`,
fileNotFound: isFileNotFound,
});
}
res.json({ content: data, path: filePath });
});
}); });
}); });
}, });
},
); );
}); });
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const { sessionId, path: filePath, content, hostId, userId } = req.body; const { sessionId, path: filePath, content, hostId, userId } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
@@ -718,7 +719,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
sshConn.client.sftp((err, sftp) => { sshConn.client.sftp((err, sftp) => {
if (err) { if (err) {
fileLogger.warn( fileLogger.warn(
`SFTP failed, trying fallback method: ${err.message}`, `SFTP failed, trying fallback method: ${err.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
return; return;
@@ -737,8 +738,8 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
fileLogger.error("Buffer conversion error:", bufferErr); fileLogger.error("Buffer conversion error:", bufferErr);
if (!res.headersSent) { if (!res.headersSent) {
return res return res
.status(500) .status(500)
.json({ error: "Invalid file content format" }); .json({ error: "Invalid file content format" });
} }
return; return;
} }
@@ -752,7 +753,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
if (hasError || hasFinished) return; if (hasError || hasFinished) return;
hasError = true; hasError = true;
fileLogger.warn( fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`, `SFTP write failed, trying fallback method: ${streamErr.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
}); });
@@ -788,14 +789,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
if (hasError || hasFinished) return; if (hasError || hasFinished) return;
hasError = true; hasError = true;
fileLogger.warn( fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${writeErr.message}`, `SFTP write operation failed, trying fallback method: ${writeErr.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
} }
}); });
} catch (sftpErr) { } catch (sftpErr) {
fileLogger.warn( fileLogger.warn(
`SFTP connection error, trying fallback method: ${sftpErr.message}`, `SFTP connection error, trying fallback method: ${sftpErr.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
} }
@@ -845,7 +846,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
} }
} else { } else {
fileLogger.error( fileLogger.error(
`Fallback write failed with code ${code}: ${errorData}`, `Fallback write failed with code ${code}: ${errorData}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
@@ -860,8 +861,8 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
fileLogger.error("Fallback write stream error:", streamErr); fileLogger.error("Fallback write stream error:", streamErr);
if (!res.headersSent) { if (!res.headersSent) {
res res
.status(500) .status(500)
.json({ error: `Write stream error: ${streamErr.message}` }); .json({ error: `Write stream error: ${streamErr.message}` });
} }
}); });
}); });
@@ -869,8 +870,8 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
fileLogger.error("Fallback method failed:", fallbackErr); fileLogger.error("Fallback method failed:", fallbackErr);
if (!res.headersSent) { if (!res.headersSent) {
res res
.status(500) .status(500)
.json({ error: `All write methods failed: ${fallbackErr.message}` }); .json({ error: `All write methods failed: ${fallbackErr.message}` });
} }
} }
}; };
@@ -878,7 +879,6 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
trySFTP(); trySFTP();
}); });
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const { const {
sessionId, sessionId,
@@ -900,27 +900,27 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
if (!filePath || !fileName || content === undefined) { if (!filePath || !fileName || content === undefined) {
return res return res
.status(400) .status(400)
.json({ error: "File path, name, and content are required" }); .json({ error: "File path, name, and content are required" });
} }
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
const contentSize = const contentSize =
typeof content === "string" typeof content === "string"
? Buffer.byteLength(content, "utf8") ? Buffer.byteLength(content, "utf8")
: content.length; : content.length;
const fullPath = filePath.endsWith("/") const fullPath = filePath.endsWith("/")
? filePath + fileName ? filePath + fileName
: filePath + "/" + fileName; : filePath + "/" + fileName;
const trySFTP = () => { const trySFTP = () => {
try { try {
sshConn.client.sftp((err, sftp) => { sshConn.client.sftp((err, sftp) => {
if (err) { if (err) {
fileLogger.warn( fileLogger.warn(
`SFTP failed, trying fallback method: ${err.message}`, `SFTP failed, trying fallback method: ${err.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
return; return;
@@ -939,8 +939,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
fileLogger.error("Buffer conversion error:", bufferErr); fileLogger.error("Buffer conversion error:", bufferErr);
if (!res.headersSent) { if (!res.headersSent) {
return res return res
.status(500) .status(500)
.json({ error: "Invalid file content format" }); .json({ error: "Invalid file content format" });
} }
return; return;
} }
@@ -954,14 +954,14 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
if (hasError || hasFinished) return; if (hasError || hasFinished) return;
hasError = true; hasError = true;
fileLogger.warn( fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`, `SFTP write failed, trying fallback method: ${streamErr.message}`,
{ {
operation: "file_upload", operation: "file_upload",
sessionId, sessionId,
fileName, fileName,
fileSize: contentSize, fileSize: contentSize,
error: streamErr.message, error: streamErr.message,
}, },
); );
tryFallbackMethod(); tryFallbackMethod();
}); });
@@ -997,14 +997,14 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
if (hasError || hasFinished) return; if (hasError || hasFinished) return;
hasError = true; hasError = true;
fileLogger.warn( fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${writeErr.message}`, `SFTP write operation failed, trying fallback method: ${writeErr.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
} }
}); });
} catch (sftpErr) { } catch (sftpErr) {
fileLogger.warn( fileLogger.warn(
`SFTP connection error, trying fallback method: ${sftpErr.message}`, `SFTP connection error, trying fallback method: ${sftpErr.message}`,
); );
tryFallbackMethod(); tryFallbackMethod();
} }
@@ -1032,8 +1032,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
fileLogger.error("Fallback upload command failed:", err); fileLogger.error("Fallback upload command failed:", err);
if (!res.headersSent) { if (!res.headersSent) {
return res return res
.status(500) .status(500)
.json({ error: `Upload failed: ${err.message}` }); .json({ error: `Upload failed: ${err.message}` });
} }
return; return;
} }
@@ -1063,7 +1063,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
} }
} else { } else {
fileLogger.error( fileLogger.error(
`Fallback upload failed with code ${code}: ${errorData}`, `Fallback upload failed with code ${code}: ${errorData}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
@@ -1081,8 +1081,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
fileLogger.error("Fallback upload stream error:", streamErr); fileLogger.error("Fallback upload stream error:", streamErr);
if (!res.headersSent) { if (!res.headersSent) {
res res
.status(500) .status(500)
.json({ error: `Upload stream error: ${streamErr.message}` }); .json({ error: `Upload stream error: ${streamErr.message}` });
} }
}); });
}); });
@@ -1104,8 +1104,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
fileLogger.error("Chunked fallback upload failed:", err); fileLogger.error("Chunked fallback upload failed:", err);
if (!res.headersSent) { if (!res.headersSent) {
return res return res
.status(500) .status(500)
.json({ error: `Chunked upload failed: ${err.message}` }); .json({ error: `Chunked upload failed: ${err.message}` });
} }
return; return;
} }
@@ -1135,7 +1135,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
} }
} else { } else {
fileLogger.error( fileLogger.error(
`Chunked fallback upload failed with code ${code}: ${errorData}`, `Chunked fallback upload failed with code ${code}: ${errorData}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
@@ -1151,8 +1151,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
stream.on("error", (streamErr) => { stream.on("error", (streamErr) => {
fileLogger.error( fileLogger.error(
"Chunked fallback upload stream error:", "Chunked fallback upload stream error:",
streamErr, streamErr,
); );
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
@@ -1166,8 +1166,8 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
fileLogger.error("Fallback method failed:", fallbackErr); fileLogger.error("Fallback method failed:", fallbackErr);
if (!res.headersSent) { if (!res.headersSent) {
res res
.status(500) .status(500)
.json({ error: `All upload methods failed: ${fallbackErr.message}` }); .json({ error: `All upload methods failed: ${fallbackErr.message}` });
} }
} }
}; };
@@ -1175,7 +1175,6 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
trySFTP(); trySFTP();
}); });
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const { const {
sessionId, sessionId,
@@ -1202,8 +1201,8 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
const fullPath = filePath.endsWith("/") const fullPath = filePath.endsWith("/")
? filePath + fileName ? filePath + fileName
: filePath + "/" + fileName; : filePath + "/" + fileName;
const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`; const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`;
@@ -1252,7 +1251,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ return res.status(500).json({
@@ -1284,7 +1283,6 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
}); });
}); });
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const { sessionId, path: folderPath, folderName, hostId, userId } = req.body; const { sessionId, path: folderPath, folderName, hostId, userId } = req.body;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
@@ -1304,8 +1302,8 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
const fullPath = folderPath.endsWith("/") const fullPath = folderPath.endsWith("/")
? folderPath + folderName ? folderPath + folderName
: folderPath + "/" + folderName; : folderPath + "/" + folderName;
const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`; const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
@@ -1354,7 +1352,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ return res.status(500).json({
@@ -1406,8 +1404,8 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const escapedPath = itemPath.replace(/'/g, "'\"'\"'"); const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
const deleteCommand = isDirectory const deleteCommand = isDirectory
? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0` ? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0`
: `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`; : `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`;
sshConn.client.exec(deleteCommand, (err, stream) => { sshConn.client.exec(deleteCommand, (err, stream) => {
if (err) { if (err) {
@@ -1456,7 +1454,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ return res.status(500).json({
@@ -1502,8 +1500,8 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
if (!oldPath || !newName) { if (!oldPath || !newName) {
return res return res
.status(400) .status(400)
.json({ error: "Old path and new name are required" }); .json({ error: "Old path and new name are required" });
} }
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
@@ -1563,7 +1561,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ return res.status(500).json({
@@ -1610,8 +1608,8 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
if (!oldPath || !newPath) { if (!oldPath || !newPath) {
return res return res
.status(400) .status(400)
.json({ error: "Old path and new path are required" }); .json({ error: "Old path and new path are required" });
} }
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
@@ -1687,7 +1685,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
if (code !== 0) { if (code !== 0) {
fileLogger.error( fileLogger.error(
`SSH moveItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, `SSH moveItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
); );
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ return res.status(500).json({
@@ -1721,7 +1719,6 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
}); });
}); });
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
const { sessionId, path: filePath, hostId, userId } = req.body; const { sessionId, path: filePath, hostId, userId } = req.body;
@@ -1742,8 +1739,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
isConnected: sshConn?.isConnected, isConnected: sshConn?.isConnected,
}); });
return res return res
.status(400) .status(400)
.json({ error: "SSH session not found or not connected" }); .json({ error: "SSH session not found or not connected" });
} }
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
@@ -1759,8 +1756,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
if (statErr) { if (statErr) {
fileLogger.error("File stat failed for download:", statErr); fileLogger.error("File stat failed for download:", statErr);
return res return res
.status(500) .status(500)
.json({ error: `Cannot access file: ${statErr.message}` }); .json({ error: `Cannot access file: ${statErr.message}` });
} }
if (!stats.isFile()) { if (!stats.isFile()) {
@@ -1772,8 +1769,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
isDirectory: stats.isDirectory(), isDirectory: stats.isDirectory(),
}); });
return res return res
.status(400) .status(400)
.json({ error: "Cannot download directories or special files" }); .json({ error: "Cannot download directories or special files" });
} }
const MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024; const MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024;
@@ -1794,8 +1791,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
if (readErr) { if (readErr) {
fileLogger.error("File read failed for download:", readErr); fileLogger.error("File read failed for download:", readErr);
return res return res
.status(500) .status(500)
.json({ error: `Failed to read file: ${readErr.message}` }); .json({ error: `Failed to read file: ${readErr.message}` });
} }
const base64Content = data.toString("base64"); const base64Content = data.toString("base64");
@@ -1823,7 +1820,6 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
}); });
}); });
app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const { sessionId, sourcePath, targetDir, hostId, userId } = req.body; const { sessionId, sourcePath, targetDir, hostId, userId } = req.body;
@@ -1834,8 +1830,8 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];
if (!sshConn || !sshConn.isConnected) { if (!sshConn || !sshConn.isConnected) {
return res return res
.status(400) .status(400)
.json({ error: "SSH session not found or not connected" }); .json({ error: "SSH session not found or not connected" });
} }
sshConn.lastActive = Date.now(); sshConn.lastActive = Date.now();
@@ -1895,7 +1891,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
if (code !== 0) { if (code !== 0) {
const fullErrorInfo = const fullErrorInfo =
errorData || stdoutData || "No error message available"; errorData || stdoutData || "No error message available";
fileLogger.error(`SSH copyItem command failed with code ${code}`, { fileLogger.error(`SSH copyItem command failed with code ${code}`, {
operation: "file_copy_failed", operation: "file_copy_failed",
sessionId, sessionId,
@@ -1926,7 +1922,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
} }
const copySuccessful = const copySuccessful =
stdoutData.includes("COPY_SUCCESS") || code === 0; stdoutData.includes("COPY_SUCCESS") || code === 0;
if (copySuccessful) { if (copySuccessful) {
fileLogger.success("Item copied successfully", { fileLogger.success("Item copied successfully", {
@@ -1993,13 +1989,13 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
if (!sshConn || !sshConn.isConnected) { if (!sshConn || !sshConn.isConnected) {
fileLogger.error( fileLogger.error(
"SSH connection not found or not connected for executeFile", "SSH connection not found or not connected for executeFile",
{ {
operation: "execute_file", operation: "execute_file",
sessionId, sessionId,
hasConnection: !!sshConn, hasConnection: !!sshConn,
isConnected: sshConn?.isConnected, isConnected: sshConn?.isConnected,
}, },
); );
return res.status(400).json({ error: "SSH connection not available" }); return res.status(400).json({ error: "SSH connection not available" });
} }
@@ -2016,8 +2012,8 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
if (checkErr) { if (checkErr) {
fileLogger.error("SSH executeFile check error:", checkErr); fileLogger.error("SSH executeFile check error:", checkErr);
return res return res
.status(500) .status(500)
.json({ error: "Failed to check file executability" }); .json({ error: "Failed to check file executability" });
} }
let checkResult = ""; let checkResult = "";
@@ -2052,8 +2048,8 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
stream.on("close", (code) => { stream.on("close", (code) => {
const exitCodeMatch = output.match(/EXIT_CODE:(\d+)$/); const exitCodeMatch = output.match(/EXIT_CODE:(\d+)$/);
const actualExitCode = exitCodeMatch const actualExitCode = exitCodeMatch
? parseInt(exitCodeMatch[1]) ? parseInt(exitCodeMatch[1])
: code; : code;
const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim(); const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim();
fileLogger.info("File execution completed", { fileLogger.info("File execution completed", {
@@ -2085,7 +2081,6 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
}); });
}); });
process.on("SIGINT", () => { process.on("SIGINT", () => {
Object.keys(sshSessions).forEach(cleanupSession); Object.keys(sshSessions).forEach(cleanupSession);
process.exit(0); process.exit(0);
@@ -2096,10 +2091,8 @@ process.on("SIGTERM", () => {
process.exit(0); process.exit(0);
}); });
const PORT = 30004; const PORT = 30004;
try { try {
const server = app.listen(PORT, async () => { const server = app.listen(PORT, async () => {
try { try {
@@ -2111,7 +2104,7 @@ try {
} }
}); });
server.on('error', (err) => { server.on("error", (err) => {
fileLogger.error("File Manager server error", err, { fileLogger.error("File Manager server error", err, {
operation: "file_manager_server_error", operation: "file_manager_server_error",
port: PORT, port: PORT,

View File

@@ -588,9 +588,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
compress: ["none", "zlib@openssh.com", "zlib"], compress: ["none", "zlib@openssh.com", "zlib"],
}, },
}; };
if (resolvedCredentials.authType === "password" && resolvedCredentials.password) { if (
resolvedCredentials.authType === "password" &&
resolvedCredentials.password
) {
connectConfig.password = resolvedCredentials.password; connectConfig.password = resolvedCredentials.password;
} else if (resolvedCredentials.authType === "key" && resolvedCredentials.key) { } else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.key
) {
try { try {
if ( if (
!resolvedCredentials.key.includes("-----BEGIN") || !resolvedCredentials.key.includes("-----BEGIN") ||

View File

@@ -24,13 +24,15 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
} catch {} } catch {}
let version = "unknown"; let version = "unknown";
const versionSources = [ const versionSources = [
() => process.env.VERSION, () => process.env.VERSION,
() => { () => {
try { try {
const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const packageJson = JSON.parse(
readFileSync(packageJsonPath, "utf-8"),
);
return packageJson.version; return packageJson.version;
} catch { } catch {
return null; return null;
@@ -43,7 +45,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
path.dirname(__filename), path.dirname(__filename),
"../../../package.json", "../../../package.json",
); );
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const packageJson = JSON.parse(
readFileSync(packageJsonPath, "utf-8"),
);
return packageJson.version; return packageJson.version;
} catch { } catch {
return null; return null;
@@ -52,12 +56,14 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
() => { () => {
try { try {
const packageJsonPath = path.join("/app", "package.json"); const packageJsonPath = path.join("/app", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const packageJson = JSON.parse(
readFileSync(packageJsonPath, "utf-8"),
);
return packageJson.version; return packageJson.version;
} catch { } catch {
return null; return null;
} }
} },
]; ];
for (const getVersion of versionSources) { for (const getVersion of versionSources) {

View File

@@ -37,13 +37,13 @@ export class AutoSSLSetup {
try { try {
await fs.access(this.CERT_FILE); await fs.access(this.CERT_FILE);
await fs.access(this.KEY_FILE); await fs.access(this.KEY_FILE);
systemLogger.info("SSL certificates found from entrypoint script", { systemLogger.info("SSL certificates found from entrypoint script", {
operation: "ssl_cert_found_entrypoint", operation: "ssl_cert_found_entrypoint",
cert_path: this.CERT_FILE, cert_path: this.CERT_FILE,
key_path: this.KEY_FILE, key_path: this.KEY_FILE,
}); });
await this.logCertificateInfo(); await this.logCertificateInfo();
await this.setupEnvironmentVariables(); await this.setupEnvironmentVariables();
return; return;

View File

@@ -3,45 +3,53 @@ import { Toaster as Sonner, type ToasterProps, toast } from "sonner";
import { useRef } from "react"; import { useRef } from "react";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme(); const { theme = "system" } = useTheme();
const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null); const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null);
const originalToast = toast; const originalToast = toast;
const rateLimitedToast = (message: string, options?: any) => { const rateLimitedToast = (message: string, options?: any) => {
const now = Date.now(); const now = Date.now();
const lastToast = lastToastRef.current; const lastToast = lastToastRef.current;
if (lastToast && lastToast.text === message && (now - lastToast.timestamp) < 1000) { if (
return; lastToast &&
} lastToast.text === message &&
now - lastToast.timestamp < 1000
) {
return;
}
lastToastRef.current = { text: message, timestamp: now }; lastToastRef.current = { text: message, timestamp: now };
return originalToast(message, options); return originalToast(message, options);
}; };
Object.assign(toast, { Object.assign(toast, {
success: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'success' }), success: (message: string, options?: any) =>
error: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'error' }), rateLimitedToast(message, { ...options, type: "success" }),
warning: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'warning' }), error: (message: string, options?: any) =>
info: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'info' }), rateLimitedToast(message, { ...options, type: "error" }),
message: rateLimitedToast, warning: (message: string, options?: any) =>
}); rateLimitedToast(message, { ...options, type: "warning" }),
info: (message: string, options?: any) =>
rateLimitedToast(message, { ...options, type: "info" }),
message: rateLimitedToast,
});
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
style={ style={
{ {
"--normal-bg": "var(--popover)", "--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)", "--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)", "--normal-border": "var(--border)",
} as React.CSSProperties } as React.CSSProperties
} }
{...props} {...props}
/> />
); );
}; };
export { Toaster }; export { Toaster };

View File

@@ -24,10 +24,7 @@ interface VersionAlertProps {
onDownload?: () => void; onDownload?: () => void;
} }
export function VersionAlert({ export function VersionAlert({ updateInfo, onDownload }: VersionAlertProps) {
updateInfo,
onDownload,
}: VersionAlertProps) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!updateInfo.success) { if (!updateInfo.success) {

View File

@@ -11,7 +11,11 @@ interface VersionCheckModalProps {
isAuthenticated?: boolean; isAuthenticated?: boolean;
} }
export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = false }: VersionCheckModalProps) { export function VersionCheckModal({
onDismiss,
onContinue,
isAuthenticated = false,
}: VersionCheckModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [versionInfo, setVersionInfo] = useState<any>(null); const [versionInfo, setVersionInfo] = useState<any>(null);
const [versionChecking, setVersionChecking] = useState(false); const [versionChecking, setVersionChecking] = useState(false);
@@ -30,7 +34,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
try { try {
const updateInfo = await checkElectronUpdate(); const updateInfo = await checkElectronUpdate();
setVersionInfo(updateInfo); setVersionInfo(updateInfo);
if (updateInfo?.status === "up_to_date") { if (updateInfo?.status === "up_to_date") {
onContinue(); onContinue();
return; return;
@@ -65,7 +69,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
return ( return (
<div className="fixed inset-0 flex items-center justify-center z-50"> <div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && ( {!isAuthenticated && (
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
backgroundImage: `linear-gradient( backgroundImage: `linear-gradient(
@@ -93,12 +97,11 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
); );
} }
if (!versionInfo || versionDismissed) { if (!versionInfo || versionDismissed) {
return ( return (
<div className="fixed inset-0 flex items-center justify-center z-50"> <div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && ( {!isAuthenticated && (
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
backgroundImage: `linear-gradient( backgroundImage: `linear-gradient(
@@ -120,7 +123,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
{t("versionCheck.checkUpdates")} {t("versionCheck.checkUpdates")}
</h2> </h2>
</div> </div>
{versionInfo && !versionDismissed && ( {versionInfo && !versionDismissed && (
<div className="mb-4"> <div className="mb-4">
<VersionAlert <VersionAlert
@@ -131,10 +134,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button onClick={handleContinue} className="flex-1 h-10">
onClick={handleContinue}
className="flex-1 h-10"
>
{t("common.continue")} {t("common.continue")}
</Button> </Button>
</div> </div>
@@ -146,7 +146,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
return ( return (
<div className="fixed inset-0 flex items-center justify-center z-50"> <div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && ( {!isAuthenticated && (
<div <div
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
backgroundImage: `linear-gradient( backgroundImage: `linear-gradient(
@@ -168,7 +168,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
{t("versionCheck.updateRequired")} {t("versionCheck.updateRequired")}
</h2> </h2>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<VersionAlert <VersionAlert
updateInfo={versionInfo} updateInfo={versionInfo}
@@ -177,10 +177,7 @@ export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = fal
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button onClick={handleContinue} className="flex-1 h-10">
onClick={handleContinue}
className="flex-1 h-10"
>
{t("common.continue")} {t("common.continue")}
</Button> </Button>
</div> </div>

View File

@@ -107,7 +107,10 @@ export function CredentialsManager({
setHostSearchQuery(""); setHostSearchQuery("");
setSelectedHostId(""); setSelectedHostId("");
setTimeout(() => { setTimeout(() => {
if (document.activeElement && (document.activeElement as HTMLElement).blur) { if (
document.activeElement &&
(document.activeElement as HTMLElement).blur
) {
(document.activeElement as HTMLElement).blur(); (document.activeElement as HTMLElement).blur();
} }
}, 50); }, 50);

View File

@@ -222,7 +222,8 @@ export function HomepageAuth({
setTotpCode(""); setTotpCode("");
setTotpTempToken(""); setTotpTempToken("");
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.unknownError"); const errorMessage =
err?.response?.data?.error || err?.message || t("errors.unknownError");
toast.error(errorMessage); toast.error(errorMessage);
setInternalLoggedIn(false); setInternalLoggedIn(false);
setLoggedIn(false); setLoggedIn(false);
@@ -370,7 +371,10 @@ export function HomepageAuth({
setTotpTempToken(""); setTotpTempToken("");
toast.success(t("messages.loginSuccess")); toast.success(t("messages.loginSuccess"));
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.invalidTotpCode"); const errorMessage =
err?.response?.data?.error ||
err?.message ||
t("errors.invalidTotpCode");
toast.error(errorMessage); toast.error(errorMessage);
} finally { } finally {
setTotpLoading(false); setTotpLoading(false);
@@ -390,7 +394,10 @@ export function HomepageAuth({
window.location.replace(authUrl); window.location.replace(authUrl);
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.failedOidcLogin"); const errorMessage =
err?.response?.data?.error ||
err?.message ||
t("errors.failedOidcLogin");
toast.error(errorMessage); toast.error(errorMessage);
setOidcLoading(false); setOidcLoading(false);
} }

View File

@@ -204,7 +204,8 @@ export function HomepageAuth({
setTotpCode(""); setTotpCode("");
setTotpTempToken(""); setTotpTempToken("");
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.unknownError"); const errorMessage =
err?.response?.data?.error || err?.message || t("errors.unknownError");
toast.error(errorMessage); toast.error(errorMessage);
setInternalLoggedIn(false); setInternalLoggedIn(false);
setLoggedIn(false); setLoggedIn(false);
@@ -346,7 +347,10 @@ export function HomepageAuth({
setTotpTempToken(""); setTotpTempToken("");
toast.success(t("messages.loginSuccess")); toast.success(t("messages.loginSuccess"));
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.invalidTotpCode"); const errorMessage =
err?.response?.data?.error ||
err?.message ||
t("errors.invalidTotpCode");
toast.error(errorMessage); toast.error(errorMessage);
} finally { } finally {
setTotpLoading(false); setTotpLoading(false);
@@ -366,7 +370,10 @@ export function HomepageAuth({
window.location.replace(authUrl); window.location.replace(authUrl);
} catch (err: any) { } catch (err: any) {
const errorMessage = err?.response?.data?.error || err?.message || t("errors.failedOidcLogin"); const errorMessage =
err?.response?.data?.error ||
err?.message ||
t("errors.failedOidcLogin");
toast.error(errorMessage); toast.error(errorMessage);
setOidcLoading(false); setOidcLoading(false);
} }

View File

@@ -168,10 +168,10 @@ function createApiInstance(
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
logger.requestStart(method, fullUrl, context); logger.requestStart(method, fullUrl, context);
} }
if (isElectron()) { if (isElectron()) {
config.headers["X-Electron-App"] = "true"; config.headers["X-Electron-App"] = "true";
const token = localStorage.getItem("jwt"); const token = localStorage.getItem("jwt");
if (token) { if (token) {
config.headers["Authorization"] = `Bearer ${token}`; config.headers["Authorization"] = `Bearer ${token}`;
@@ -304,7 +304,7 @@ function isDev(): boolean {
if (isElectron()) { if (isElectron()) {
return false; return false;
} }
return ( return (
process.env.NODE_ENV === "development" && process.env.NODE_ENV === "development" &&
(window.location.port === "3000" || (window.location.port === "3000" ||
@@ -463,16 +463,21 @@ export let statsApi: AxiosInstance;
export let authApi: AxiosInstance; export let authApi: AxiosInstance;
if (isElectron()) { if (isElectron()) {
getServerConfig().then((config) => { getServerConfig()
if (config?.serverUrl) { .then((config) => {
configuredServerUrl = config.serverUrl; if (config?.serverUrl) {
(window as any).configuredServerUrl = configuredServerUrl; configuredServerUrl = config.serverUrl;
} (window as any).configuredServerUrl = configuredServerUrl;
initializeApiInstances(); }
}).catch((error) => { initializeApiInstances();
console.error("Failed to load server config, initializing with default:", error); })
initializeApiInstances(); .catch((error) => {
}); console.error(
"Failed to load server config, initializing with default:",
error,
);
initializeApiInstances();
});
} else { } else {
initializeApiInstances(); initializeApiInstances();
} }
@@ -535,15 +540,13 @@ function handleApiError(error: unknown, operation: string): never {
`Auth failed: ${method} ${url} - ${message}`, `Auth failed: ${method} ${url} - ${message}`,
errorContext, errorContext,
); );
const isLoginEndpoint = url?.includes('/users/login'); const isLoginEndpoint = url?.includes("/users/login");
const errorMessage = isLoginEndpoint ? message : "Authentication required. Please log in again."; const errorMessage = isLoginEndpoint
? message
throw new ApiError( : "Authentication required. Please log in again.";
errorMessage,
401, throw new ApiError(errorMessage, 401, "AUTH_REQUIRED");
"AUTH_REQUIRED",
);
} else if (status === 403) { } else if (status === 403) {
authLogger.warn(`Access denied: ${method} ${url}`, errorContext); authLogger.warn(`Access denied: ${method} ${url}`, errorContext);
throw new ApiError( throw new ApiError(
@@ -1527,11 +1530,11 @@ export async function loginUser(
): Promise<AuthResponse> { ): Promise<AuthResponse> {
try { try {
const response = await authApi.post("/users/login", { username, password }); const response = await authApi.post("/users/login", { username, password });
if (isElectron() && response.data.token) { if (isElectron() && response.data.token) {
localStorage.setItem("jwt", response.data.token); localStorage.setItem("jwt", response.data.token);
} }
return { return {
token: response.data.token || "cookie-based", token: response.data.token || "cookie-based",
success: response.data.success, success: response.data.success,