2026-06-04 15:16:53 -04:00
|
|
|
import type {
|
|
|
|
|
AuthenticatedRequest,
|
|
|
|
|
CredentialBackend,
|
|
|
|
|
} from "../../../types/index.js";
|
|
|
|
|
import type { Request, RequestHandler, Response, Router } from "express";
|
|
|
|
|
import { eq } from "drizzle-orm";
|
|
|
|
|
import ssh2Pkg from "ssh2";
|
|
|
|
|
import { db } from "../db/index.js";
|
|
|
|
|
import { hosts, sshCredentials } from "../db/schema.js";
|
2026-06-29 13:28:26 -05:00
|
|
|
import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js";
|
|
|
|
|
import { applyAgentAuth } from "../../ssh/terminal-auth-helpers.js";
|
2026-06-04 15:16:53 -04:00
|
|
|
|
|
|
|
|
const { Client } = ssh2Pkg;
|
|
|
|
|
|
|
|
|
|
async function deploySSHKeyToHost(
|
|
|
|
|
hostConfig: Record<string, unknown>,
|
|
|
|
|
credData: CredentialBackend,
|
|
|
|
|
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
|
|
|
const publicKey = credData.publicKey as string;
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const conn = new Client();
|
|
|
|
|
|
|
|
|
|
const connectionTimeout = setTimeout(() => {
|
|
|
|
|
conn.destroy();
|
|
|
|
|
resolve({ success: false, error: "Connection timeout" });
|
|
|
|
|
}, 120000);
|
|
|
|
|
|
|
|
|
|
conn.on("ready", async () => {
|
|
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await new Promise<void>((resolveCmd, rejectCmd) => {
|
|
|
|
|
const cmdTimeout = setTimeout(() => {
|
|
|
|
|
rejectCmd(new Error("mkdir command timeout"));
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
|
|
|
|
conn.exec(
|
|
|
|
|
"test -d ~/.ssh || mkdir -p ~/.ssh; chmod 700 ~/.ssh",
|
|
|
|
|
(err, stream) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
clearTimeout(cmdTimeout);
|
|
|
|
|
return rejectCmd(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stream.on("close", (code) => {
|
|
|
|
|
clearTimeout(cmdTimeout);
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
resolveCmd();
|
|
|
|
|
} else {
|
|
|
|
|
rejectCmd(
|
|
|
|
|
new Error(`mkdir command failed with code ${code}`),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("data", () => {
|
|
|
|
|
// Ignore output
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const keyExists = await new Promise<boolean>(
|
|
|
|
|
(resolveCheck, rejectCheck) => {
|
|
|
|
|
const checkTimeout = setTimeout(() => {
|
|
|
|
|
rejectCheck(new Error("Key check timeout"));
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
let actualPublicKey = publicKey;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(publicKey);
|
|
|
|
|
if (parsed.data) {
|
|
|
|
|
actualPublicKey = parsed.data;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyParts = actualPublicKey.trim().split(" ");
|
|
|
|
|
if (keyParts.length < 2) {
|
|
|
|
|
clearTimeout(checkTimeout);
|
|
|
|
|
return rejectCheck(
|
|
|
|
|
new Error(
|
|
|
|
|
"Invalid public key format - must contain at least 2 parts",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyPattern = keyParts[1];
|
|
|
|
|
|
|
|
|
|
conn.exec(
|
|
|
|
|
`if [ -f ~/.ssh/authorized_keys ]; then grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?; else echo 1; fi`,
|
|
|
|
|
(err, stream) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
clearTimeout(checkTimeout);
|
|
|
|
|
return rejectCheck(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let output = "";
|
|
|
|
|
stream.on("data", (data) => {
|
|
|
|
|
output += data.toString();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("close", () => {
|
|
|
|
|
clearTimeout(checkTimeout);
|
|
|
|
|
const exists = output.trim() === "0";
|
|
|
|
|
resolveCheck(exists);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (keyExists) {
|
|
|
|
|
conn.end();
|
|
|
|
|
resolve({ success: true, message: "SSH key already deployed" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await new Promise<void>((resolveAdd, rejectAdd) => {
|
|
|
|
|
const addTimeout = setTimeout(() => {
|
|
|
|
|
rejectAdd(new Error("Key add timeout"));
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
let actualPublicKey = publicKey;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(publicKey);
|
|
|
|
|
if (parsed.data) {
|
|
|
|
|
actualPublicKey = parsed.data;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const escapedKey = actualPublicKey
|
|
|
|
|
.replace(/\\/g, "\\\\")
|
|
|
|
|
.replace(/'/g, "'\\''");
|
|
|
|
|
const escapedName = credData.name
|
|
|
|
|
.replace(/\\/g, "\\\\")
|
|
|
|
|
.replace(/'/g, "'\\''");
|
|
|
|
|
|
|
|
|
|
conn.exec(
|
|
|
|
|
`printf '%s\n' '${escapedKey} ${escapedName}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
|
|
|
|
|
(err, stream) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
clearTimeout(addTimeout);
|
|
|
|
|
return rejectAdd(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stream.on("data", () => {
|
|
|
|
|
// Consume output
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("close", (code) => {
|
|
|
|
|
clearTimeout(addTimeout);
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
resolveAdd();
|
|
|
|
|
} else {
|
|
|
|
|
rejectAdd(
|
|
|
|
|
new Error(`Key deployment failed with code ${code}`),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const verifySuccess = await new Promise<boolean>(
|
|
|
|
|
(resolveVerify, rejectVerify) => {
|
|
|
|
|
const verifyTimeout = setTimeout(() => {
|
|
|
|
|
rejectVerify(new Error("Key verification timeout"));
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
let actualPublicKey = publicKey;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(publicKey);
|
|
|
|
|
if (parsed.data) {
|
|
|
|
|
actualPublicKey = parsed.data;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyParts = actualPublicKey.trim().split(" ");
|
|
|
|
|
if (keyParts.length < 2) {
|
|
|
|
|
clearTimeout(verifyTimeout);
|
|
|
|
|
return rejectVerify(
|
|
|
|
|
new Error(
|
|
|
|
|
"Invalid public key format - must contain at least 2 parts",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyPattern = keyParts[1];
|
|
|
|
|
conn.exec(
|
|
|
|
|
`grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?`,
|
|
|
|
|
(err, stream) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
clearTimeout(verifyTimeout);
|
|
|
|
|
return rejectVerify(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let output = "";
|
|
|
|
|
stream.on("data", (data) => {
|
|
|
|
|
output += data.toString();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("close", () => {
|
|
|
|
|
clearTimeout(verifyTimeout);
|
|
|
|
|
const verified = output.trim() === "0";
|
|
|
|
|
resolveVerify(verified);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
conn.end();
|
|
|
|
|
|
|
|
|
|
if (verifySuccess) {
|
|
|
|
|
resolve({ success: true, message: "SSH key deployed successfully" });
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Key deployment verification failed",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
conn.end();
|
|
|
|
|
resolve({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : "Deployment failed",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
conn.on("error", (err) => {
|
|
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
let errorMessage = err.message;
|
|
|
|
|
|
|
|
|
|
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.";
|
|
|
|
|
} else if (err.message.includes("ECONNREFUSED")) {
|
|
|
|
|
errorMessage =
|
|
|
|
|
"Connection refused. The server may not be running or the port may be incorrect.";
|
|
|
|
|
} else if (err.message.includes("ETIMEDOUT")) {
|
|
|
|
|
errorMessage =
|
|
|
|
|
"Connection timed out. Check your network connection and server availability.";
|
|
|
|
|
} 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 });
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-29 13:28:26 -05:00
|
|
|
void (async () => {
|
|
|
|
|
try {
|
|
|
|
|
const connectionConfig: Record<string, unknown> = {
|
|
|
|
|
host: hostConfig.ip,
|
|
|
|
|
port: hostConfig.port || 22,
|
|
|
|
|
username: hostConfig.username,
|
|
|
|
|
readyTimeout: 60000,
|
|
|
|
|
keepaliveInterval: 30000,
|
|
|
|
|
keepaliveCountMax: 3,
|
|
|
|
|
tcpKeepAlive: true,
|
|
|
|
|
tcpKeepAliveInitialDelay: 30000,
|
|
|
|
|
algorithms: {
|
|
|
|
|
kex: [
|
|
|
|
|
"diffie-hellman-group14-sha256",
|
|
|
|
|
"diffie-hellman-group14-sha1",
|
|
|
|
|
"diffie-hellman-group1-sha1",
|
|
|
|
|
"diffie-hellman-group-exchange-sha256",
|
|
|
|
|
"diffie-hellman-group-exchange-sha1",
|
|
|
|
|
"ecdh-sha2-nistp256",
|
|
|
|
|
"ecdh-sha2-nistp384",
|
|
|
|
|
"ecdh-sha2-nistp521",
|
|
|
|
|
],
|
|
|
|
|
cipher: [
|
|
|
|
|
"aes128-ctr",
|
|
|
|
|
"aes192-ctr",
|
|
|
|
|
"aes256-ctr",
|
|
|
|
|
"aes128-gcm@openssh.com",
|
|
|
|
|
"aes256-gcm@openssh.com",
|
|
|
|
|
"aes128-cbc",
|
|
|
|
|
"aes192-cbc",
|
|
|
|
|
"aes256-cbc",
|
|
|
|
|
"3des-cbc",
|
|
|
|
|
],
|
|
|
|
|
hmac: [
|
|
|
|
|
"hmac-sha2-256-etm@openssh.com",
|
|
|
|
|
"hmac-sha2-512-etm@openssh.com",
|
|
|
|
|
"hmac-sha2-256",
|
|
|
|
|
"hmac-sha2-512",
|
|
|
|
|
"hmac-sha1",
|
|
|
|
|
"hmac-md5",
|
|
|
|
|
],
|
|
|
|
|
compress: ["none", "zlib@openssh.com", "zlib"],
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-06-04 15:16:53 -04:00
|
|
|
|
2026-06-29 13:28:26 -05:00
|
|
|
if (hostConfig.authType === "password" && hostConfig.password) {
|
|
|
|
|
connectionConfig.password = hostConfig.password;
|
|
|
|
|
} else if (hostConfig.authType === "key" && hostConfig.privateKey) {
|
|
|
|
|
try {
|
|
|
|
|
const privateKey = hostConfig.privateKey as string;
|
|
|
|
|
connectionConfig.privateKey = preparePrivateKeyForSSH2(
|
|
|
|
|
privateKey,
|
|
|
|
|
hostConfig.keyPassword as string | undefined,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (hostConfig.keyPassword) {
|
|
|
|
|
connectionConfig.passphrase = hostConfig.keyPassword;
|
|
|
|
|
}
|
|
|
|
|
} catch (keyError) {
|
|
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
resolve({
|
|
|
|
|
success: false,
|
|
|
|
|
error: `Invalid SSH key format: ${keyError instanceof Error ? keyError.message : "Unknown error"}`,
|
|
|
|
|
});
|
|
|
|
|
return;
|
2026-06-04 15:16:53 -04:00
|
|
|
}
|
2026-06-29 13:28:26 -05:00
|
|
|
} else if (hostConfig.authType === "agent") {
|
|
|
|
|
const result = await applyAgentAuth(
|
|
|
|
|
connectionConfig,
|
|
|
|
|
hostConfig.terminalConfig as Record<string, unknown> | undefined,
|
|
|
|
|
);
|
|
|
|
|
if ("error" in result) {
|
|
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
resolve({ success: false, error: result.error });
|
|
|
|
|
return;
|
2026-06-04 15:16:53 -04:00
|
|
|
}
|
2026-06-29 13:28:26 -05:00
|
|
|
} else {
|
2026-06-04 15:16:53 -04:00
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
resolve({
|
|
|
|
|
success: false,
|
2026-06-29 13:28:26 -05:00
|
|
|
error: `Invalid authentication configuration. Auth type: ${hostConfig.authType}, has password: ${!!hostConfig.password}, has key: ${!!hostConfig.privateKey}`,
|
2026-06-04 15:16:53 -04:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-29 13:28:26 -05:00
|
|
|
|
|
|
|
|
conn.connect(connectionConfig);
|
|
|
|
|
} catch (error) {
|
2026-06-04 15:16:53 -04:00
|
|
|
clearTimeout(connectionTimeout);
|
|
|
|
|
resolve({
|
|
|
|
|
success: false,
|
2026-06-29 13:28:26 -05:00
|
|
|
error: error instanceof Error ? error.message : "Connection failed",
|
2026-06-04 15:16:53 -04:00
|
|
|
});
|
|
|
|
|
}
|
2026-06-29 13:28:26 -05:00
|
|
|
})();
|
2026-06-04 15:16:53 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function registerCredentialDeployRoutes(
|
|
|
|
|
router: Router,
|
|
|
|
|
authenticateJWT: RequestHandler,
|
|
|
|
|
): void {
|
|
|
|
|
/**
|
|
|
|
|
* @openapi
|
|
|
|
|
* /credentials/{id}/deploy-to-host:
|
|
|
|
|
* post:
|
|
|
|
|
* summary: Deploy SSH key to a host
|
|
|
|
|
* description: Deploys an SSH public key to a target host's authorized_keys file.
|
|
|
|
|
* tags:
|
|
|
|
|
* - Credentials
|
|
|
|
|
* parameters:
|
|
|
|
|
* - in: path
|
|
|
|
|
* name: id
|
|
|
|
|
* required: true
|
|
|
|
|
* schema:
|
|
|
|
|
* type: integer
|
|
|
|
|
* requestBody:
|
|
|
|
|
* required: true
|
|
|
|
|
* content:
|
|
|
|
|
* application/json:
|
|
|
|
|
* schema:
|
|
|
|
|
* type: object
|
|
|
|
|
* properties:
|
|
|
|
|
* targetHostId:
|
|
|
|
|
* type: integer
|
|
|
|
|
* responses:
|
|
|
|
|
* 200:
|
|
|
|
|
* description: SSH key deployed successfully.
|
|
|
|
|
* 400:
|
|
|
|
|
* description: Credential ID and target host ID are required.
|
|
|
|
|
* 401:
|
|
|
|
|
* description: Authentication required.
|
|
|
|
|
* 404:
|
|
|
|
|
* description: Credential or target host not found.
|
|
|
|
|
* 500:
|
|
|
|
|
* description: Failed to deploy SSH key.
|
|
|
|
|
*/
|
|
|
|
|
router.post(
|
|
|
|
|
"/:id/deploy-to-host",
|
|
|
|
|
authenticateJWT,
|
|
|
|
|
async (req: Request, res: Response) => {
|
|
|
|
|
const id = Array.isArray(req.params.id)
|
|
|
|
|
? req.params.id[0]
|
|
|
|
|
: req.params.id;
|
|
|
|
|
const credentialId = parseInt(id);
|
|
|
|
|
const { targetHostId } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!credentialId || !targetHostId) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Credential ID and target host ID are required",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
|
|
|
if (!userId) {
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Authentication required",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
|
|
|
|
|
const credential = await SimpleDBOps.select(
|
|
|
|
|
db
|
|
|
|
|
.select()
|
|
|
|
|
.from(sshCredentials)
|
|
|
|
|
.where(eq(sshCredentials.id, credentialId))
|
|
|
|
|
.limit(1),
|
|
|
|
|
"ssh_credentials",
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!credential || credential.length === 0) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Credential not found",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const credData = credential[0] as unknown as CredentialBackend;
|
|
|
|
|
|
|
|
|
|
if (credData.authType !== "key") {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Only SSH key-based credentials can be deployed",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const publicKey = credData.publicKey;
|
|
|
|
|
if (!publicKey) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Public key is required for deployment",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const targetHost = await SimpleDBOps.select(
|
|
|
|
|
db.select().from(hosts).where(eq(hosts.id, targetHostId)).limit(1),
|
|
|
|
|
"ssh_data",
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!targetHost || targetHost.length === 0) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Target host not found",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hostData = targetHost[0];
|
|
|
|
|
|
|
|
|
|
const hostConfig = {
|
|
|
|
|
ip: hostData.ip,
|
|
|
|
|
port: hostData.port,
|
|
|
|
|
username: hostData.username,
|
|
|
|
|
authType: hostData.authType,
|
|
|
|
|
password: hostData.password,
|
|
|
|
|
privateKey: hostData.key,
|
|
|
|
|
keyPassword: hostData.keyPassword,
|
2026-06-29 13:28:26 -05:00
|
|
|
terminalConfig: hostData.terminalConfig,
|
2026-06-04 15:16:53 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (hostData.authType === "credential" && hostData.credentialId) {
|
|
|
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
|
|
|
if (!userId) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Authentication required for credential resolution",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { SimpleDBOps } =
|
|
|
|
|
await import("../../utils/simple-db-ops.js");
|
|
|
|
|
const hostCredential = await SimpleDBOps.select(
|
|
|
|
|
db
|
|
|
|
|
.select()
|
|
|
|
|
.from(sshCredentials)
|
|
|
|
|
.where(eq(sshCredentials.id, hostData.credentialId as number))
|
|
|
|
|
.limit(1),
|
|
|
|
|
"ssh_credentials",
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (hostCredential && hostCredential.length > 0) {
|
|
|
|
|
const cred = hostCredential[0];
|
|
|
|
|
|
|
|
|
|
hostConfig.authType = cred.authType;
|
|
|
|
|
hostConfig.username = cred.username;
|
|
|
|
|
|
|
|
|
|
if (cred.authType === "password") {
|
|
|
|
|
hostConfig.password = cred.password;
|
|
|
|
|
} else if (cred.authType === "key") {
|
|
|
|
|
hostConfig.privateKey = cred.privateKey || cred.key;
|
|
|
|
|
hostConfig.keyPassword = cred.keyPassword;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Host credential not found",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Failed to resolve host credentials",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deployResult = await deploySSHKeyToHost(hostConfig, credData);
|
|
|
|
|
|
|
|
|
|
if (deployResult.success) {
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: deployResult.message || "SSH key deployed successfully",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: deployResult.error || "Deployment failed",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to deploy SSH key",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|