This commit was merged in pull request #335.
This commit is contained in:
Karmaa
2025-10-03 00:02:10 -05:00
committed by GitHub
parent a7fa40393d
commit 937e04fa5c
26 changed files with 877 additions and 186 deletions

View File

@@ -333,14 +333,14 @@ router.get(
if (credential.key) {
(output as any).key = credential.key;
}
if (credential.privateKey) {
(output as any).privateKey = credential.privateKey;
if (credential.private_key) {
(output as any).privateKey = credential.private_key;
}
if (credential.publicKey) {
(output as any).publicKey = credential.publicKey;
if (credential.public_key) {
(output as any).publicKey = credential.public_key;
}
if (credential.keyPassword) {
(output as any).keyPassword = credential.keyPassword;
if (credential.key_password) {
(output as any).keyPassword = credential.key_password;
}
res.json(output);
@@ -605,15 +605,19 @@ router.post(
}
try {
const credentials = await db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, parseInt(credentialId)),
eq(sshCredentials.userId, userId),
const credentials = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, parseInt(credentialId)),
eq(sshCredentials.userId, userId),
),
),
);
"ssh_credentials",
userId,
);
if (credentials.length === 0) {
return res.status(404).json({ error: "Credential not found" });
@@ -626,7 +630,7 @@ router.post(
.set({
credentialId: parseInt(credentialId),
username: credential.username,
authType: credential.authType,
authType: credential.auth_type || credential.authType,
password: null,
key: null,
keyPassword: null,
@@ -715,15 +719,15 @@ function formatCredentialOutput(credential: any): any {
? credential.tags.split(",").filter(Boolean)
: []
: [],
authType: credential.authType,
authType: credential.authType || credential.auth_type,
username: credential.username,
publicKey: credential.publicKey,
keyType: credential.keyType,
detectedKeyType: credential.detectedKeyType,
usageCount: credential.usageCount || 0,
lastUsed: credential.lastUsed,
createdAt: credential.createdAt,
updatedAt: credential.updatedAt,
publicKey: credential.public_key || credential.publicKey,
keyType: credential.key_type || credential.keyType,
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
usageCount: credential.usage_count || credential.usageCount || 0,
lastUsed: credential.last_used || credential.lastUsed,
createdAt: credential.created_at || credential.createdAt,
updatedAt: credential.updated_at || credential.updatedAt,
};
}
@@ -1551,14 +1555,15 @@ router.post(
if (hostCredential && hostCredential.length > 0) {
const cred = hostCredential[0];
hostConfig.authType = cred.authType;
hostConfig.authType = cred.auth_type || cred.authType;
hostConfig.username = cred.username;
if (cred.authType === "password") {
if ((cred.auth_type || cred.authType) === "password") {
hostConfig.password = cred.password;
} else if (cred.authType === "key") {
hostConfig.privateKey = cred.privateKey || cred.key;
hostConfig.keyPassword = cred.keyPassword;
} else if ((cred.auth_type || cred.authType) === "key") {
hostConfig.privateKey =
cred.private_key || cred.privateKey || cred.key;
hostConfig.keyPassword = cred.key_password || cred.keyPassword;
}
} else {
return res.status(400).json({

View File

@@ -472,7 +472,6 @@ router.put(
}
sshDataObj.password = null;
} else {
// For credential auth
sshDataObj.password = null;
sshDataObj.key = null;
sshDataObj.keyPassword = null;
@@ -670,6 +669,83 @@ router.get(
},
);
// Route: Export SSH host with decrypted credentials (requires data access)
// GET /ssh/db/host/:id/export
router.get(
"/db/host/:id/export",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const hostId = req.params.id;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !hostId) {
return res.status(400).json({ error: "Invalid userId or hostId" });
}
try {
const hosts = await SimpleDBOps.select(
db
.select()
.from(sshData)
.where(
and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)),
),
"ssh_data",
userId,
);
if (hosts.length === 0) {
return res.status(404).json({ error: "SSH host not found" });
}
const host = hosts[0];
const resolvedHost = (await resolveHostCredentials(host)) || host;
const exportData = {
name: resolvedHost.name,
ip: resolvedHost.ip,
port: resolvedHost.port,
username: resolvedHost.username,
authType: resolvedHost.authType,
password: resolvedHost.password || null,
key: resolvedHost.key || null,
keyPassword: resolvedHost.keyPassword || null,
keyType: resolvedHost.keyType || null,
folder: resolvedHost.folder,
tags:
typeof resolvedHost.tags === "string"
? resolvedHost.tags.split(",").filter(Boolean)
: resolvedHost.tags || [],
pin: !!resolvedHost.pin,
enableTerminal: !!resolvedHost.enableTerminal,
enableTunnel: !!resolvedHost.enableTunnel,
enableFileManager: !!resolvedHost.enableFileManager,
defaultPath: resolvedHost.defaultPath,
tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections)
: [],
};
sshLogger.success("Host exported with decrypted credentials", {
operation: "host_export",
hostId: parseInt(hostId),
userId,
});
res.json(exportData);
} catch (err) {
sshLogger.error("Failed to export SSH host", err, {
operation: "host_export",
hostId: parseInt(hostId),
userId,
});
res.status(500).json({ error: "Failed to export SSH host" });
}
},
);
// Route: Delete SSH host by id (requires JWT)
// DELETE /ssh/host/:id
router.delete(
@@ -1136,26 +1212,30 @@ router.delete(
async function resolveHostCredentials(host: any): Promise<any> {
try {
if (host.credentialId && host.userId) {
const credentials = await db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, host.credentialId),
eq(sshCredentials.userId, host.userId),
const credentials = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, host.credentialId),
eq(sshCredentials.userId, host.userId),
),
),
);
"ssh_credentials",
host.userId,
);
if (credentials.length > 0) {
const credential = credentials[0];
return {
...host,
username: credential.username,
authType: credential.authType,
authType: credential.auth_type || credential.authType,
password: credential.password,
key: credential.key,
keyPassword: credential.keyPassword,
keyType: credential.keyType,
keyPassword: credential.key_password || credential.keyPassword,
keyType: credential.key_type || credential.keyType,
};
}
}
@@ -1214,7 +1294,6 @@ router.put(
)
.returning();
// Trigger database save after folder rename
DatabaseSaveTrigger.triggerSave("folder_rename");
res.json({

View File

@@ -1317,6 +1317,43 @@ router.post("/complete-reset", async (req, res) => {
.set({ password_hash })
.where(eq(users.username, username));
try {
await authManager.registerUser(userId, newPassword);
authManager.logoutUser(userId);
await db
.update(users)
.set({
totp_enabled: false,
totp_secret: null,
totp_backup_codes: null,
})
.where(eq(users.id, userId));
authLogger.warn(
`Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`,
{
operation: "password_reset_data_inaccessible",
userId,
username,
},
);
} catch (encryptionError) {
authLogger.error(
"Failed to re-encrypt user data after password reset",
encryptionError,
{
operation: "password_reset_encryption_failed",
userId,
username,
},
);
return res.status(500).json({
error:
"Password reset completed but user data encryption failed. Please contact administrator.",
});
}
authLogger.success(`Password successfully reset for user: ${username}`);
db.$client
@@ -1495,6 +1532,22 @@ router.post("/totp/verify-login", async (req, res) => {
"totp_secret",
);
if (!totpSecret) {
await db
.update(users)
.set({
totp_enabled: false,
totp_secret: null,
totp_backup_codes: null,
})
.where(eq(users.id, userRecord.id));
return res.status(400).json({
error:
"TOTP has been disabled due to password reset. Please set up TOTP again.",
});
}
const verified = speakeasy.totp.verify({
secret: totpSecret,
encoding: "base32",