Code cleanup
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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") ||
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user