fix: Fixed various issues with the dashboard, tab bar, and database issues

This commit is contained in:
LukeGus
2025-10-21 22:09:05 -05:00
parent 21d8cf9b2c
commit 217af1e286
23 changed files with 757 additions and 254 deletions

View File

@@ -4,7 +4,7 @@ import cookieParser from "cookie-parser";
import { getDb } from "./database/db/index.js";
import { recentActivity, sshData } from "./database/db/schema.js";
import { eq, and, desc } from "drizzle-orm";
import { homepageLogger } from "./utils/logger.js";
import { dashboardLogger } from "./utils/logger.js";
import { SimpleDBOps } from "./utils/simple-db-ops.js";
import { AuthManager } from "./utils/auth-manager.js";
import type { AuthenticatedRequest } from "../types/index.js";
@@ -71,7 +71,7 @@ app.get("/uptime", async (req, res) => {
formatted: `${days}d ${hours}h ${minutes}m`,
});
} catch (err) {
homepageLogger.error("Failed to get uptime", err);
dashboardLogger.error("Failed to get uptime", err);
res.status(500).json({ error: "Failed to get uptime" });
}
});
@@ -103,7 +103,7 @@ app.get("/activity/recent", async (req, res) => {
res.json(activities);
} catch (err) {
homepageLogger.error("Failed to get recent activity", err);
dashboardLogger.error("Failed to get recent activity", err);
res.status(500).json({ error: "Failed to get recent activity" });
}
});
@@ -211,7 +211,7 @@ app.post("/activity/log", async (req, res) => {
res.json({ message: "Activity logged", id: result.id });
} catch (err) {
homepageLogger.error("Failed to log activity", err);
dashboardLogger.error("Failed to log activity", err);
res.status(500).json({ error: "Failed to log activity" });
}
});
@@ -229,22 +229,20 @@ app.delete("/activity/reset", async (req, res) => {
}
// Delete all activities for the user
const activities = await SimpleDBOps.select(
getDb()
.select()
.from(recentActivity)
.where(eq(recentActivity.userId, userId)),
await SimpleDBOps.delete(
recentActivity,
"recent_activity",
userId,
eq(recentActivity.userId, userId),
);
for (const activity of activities) {
await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
}
dashboardLogger.success("Recent activity cleared", {
operation: "reset_recent_activity",
userId,
});
res.json({ message: "Recent activity cleared" });
} catch (err) {
homepageLogger.error("Failed to reset activity", err);
dashboardLogger.error("Failed to reset activity", err);
res.status(500).json({ error: "Failed to reset activity" });
}
});
@@ -254,7 +252,7 @@ app.listen(PORT, async () => {
try {
await authManager.initialize();
} catch (err) {
homepageLogger.error("Failed to initialize AuthManager", err, {
dashboardLogger.error("Failed to initialize AuthManager", err, {
operation: "auth_init_error",
});
}

View File

@@ -678,7 +678,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.authType,
decrypted.password || null,
decrypted.key || null,
decrypted.key_password || null,
decrypted.keyPassword || decrypted.key_password || null,
decrypted.keyType || null,
decrypted.autostartPassword || null,
decrypted.autostartKey || null,
@@ -721,9 +721,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username,
decrypted.password || null,
decrypted.key || null,
decrypted.private_key || null,
decrypted.public_key || null,
decrypted.key_password || null,
decrypted.privateKey || decrypted.private_key || null,
decrypted.publicKey || decrypted.public_key || null,
decrypted.keyPassword || decrypted.key_password || null,
decrypted.keyType || null,
decrypted.detectedKeyType || null,
decrypted.usageCount || 0,

View File

@@ -287,6 +287,24 @@ async function initializeCompleteDatabase(): Promise<void> {
error: e,
});
}
try {
const row = sqlite
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
.get();
if (!row) {
sqlite
.prepare(
"INSERT INTO settings (key, value) VALUES ('allow_password_login', 'true')",
)
.run();
}
} catch (e) {
databaseLogger.warn("Could not initialize allow_password_login setting", {
operation: "db_init",
error: e,
});
}
}
const addColumnIfNotExists = (

View File

@@ -61,27 +61,27 @@ async function verifyOIDCToken(
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
}
let jwks: Record<string, unknown> | null = null;
let jwks: Record<string, unknown> | null = null;
for (const url of jwksUrls) {
try {
const response = await fetch(url);
if (response.ok) {
const jwksData = (await response.json()) as Record<string, unknown>;;
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
jwks = jwksData;
break;
} else {
authLogger.error(
`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`,
);
}
for (const url of jwksUrls) {
try {
const response = await fetch(url);
if (response.ok) {
const jwksData = (await response.json()) as Record<string, unknown>;
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
jwks = jwksData;
break;
} else {
authLogger.error(
`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`,
);
}
} catch {
continue;
} else {
}
} catch {
continue;
}
}
if (!jwks) {
throw new Error("Failed to fetch JWKS from any URL");
@@ -917,8 +917,7 @@ router.post("/login", async (req, res) => {
if (kekSalt.length === 0) {
await authManager.registerUser(userRecord.id, password);
}
} catch {
}
} catch {}
const dataUnlocked = await authManager.authenticateUser(
userRecord.id,
@@ -1153,7 +1152,7 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
}
db.$client
.prepare(
"UPDATE settings SET value = ? WHERE key = 'allow_password_login'",
"INSERT OR REPLACE INTO settings (key, value) VALUES ('allow_password_login', ?)",
)
.run(allowed ? "true" : "false");
res.json({ allowed });

View File

@@ -375,7 +375,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
scheduleSessionCleanup(sessionId);
res.json({ status: "success", message: "SSH connection established" });
// Log activity to homepage API
// Log activity to dashboard API
if (hostId && userId) {
(async () => {
try {
@@ -446,6 +446,8 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
cleanupSession(sessionId);
});
let keyboardInteractiveResponded = false;
client.on(
"keyboard-interactive",
(
@@ -455,11 +457,14 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void,
) => {
const promptTexts = prompts.map((p) => p.prompt);
fileLogger.info("Keyboard-interactive authentication requested", {
operation: "file_keyboard_interactive",
hostId,
sessionId,
promptsCount: prompts.length,
prompts: promptTexts,
alreadyResponded: keyboardInteractiveResponded,
});
const totpPromptIndex = prompts.findIndex((p) =>
@@ -469,7 +474,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
);
if (totpPromptIndex !== -1) {
if (responseSent) return;
// TOTP prompt detected - need user input
if (responseSent) {
fileLogger.warn("Response already sent, ignoring TOTP prompt", {
operation: "file_keyboard_interactive",
hostId,
sessionId,
});
return;
}
responseSent = true;
if (pendingTOTPSessions[sessionId]) {
@@ -481,11 +494,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
sessionId,
},
);
// Don't respond to duplicate keyboard-interactive events
// The first one is still being processed
return;
}
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = {
client,
finish,
@@ -516,14 +529,40 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
prompt: prompts[totpPromptIndex].prompt,
});
} else {
if (resolvedCredentials.password) {
const responses = prompts.map(
() => resolvedCredentials.password || "",
// Non-TOTP prompts (password, etc.) - respond automatically
if (keyboardInteractiveResponded) {
fileLogger.warn(
"Already responded to keyboard-interactive, ignoring subsequent prompt",
{
operation: "file_keyboard_interactive",
hostId,
sessionId,
prompts: promptTexts,
},
);
finish(responses);
} else {
finish(prompts.map(() => ""));
return;
}
keyboardInteractiveResponded = true;
const responses = prompts.map((p) => {
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
return resolvedCredentials.password;
}
return "";
});
fileLogger.info("Auto-responding to keyboard-interactive prompts", {
operation: "file_keyboard_interactive_response",
hostId,
sessionId,
hasPassword: !!resolvedCredentials.password,
responsesProvided: responses.filter((r) => r !== "").length,
totalPrompts: prompts.length,
prompts: promptTexts,
});
finish(responses);
}
},
);
@@ -640,7 +679,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
message: "TOTP verified, SSH connection established",
});
// Log activity to homepage API
// Log activity to dashboard API
if (session.hostId && session.userId) {
(async () => {
try {

View File

@@ -196,6 +196,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
let pingInterval: NodeJS.Timeout | null = null;
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
let totpPromptSent = false;
let keyboardInteractiveResponded = false;
ws.on("close", () => {
const userWs = userConnections.get(userId);
@@ -482,7 +483,29 @@ wss.on("connection", async (ws: WebSocket, req) => {
// Small delay to let connection stabilize after keyboard-interactive auth
// This helps prevent "No response from server" errors with TOTP
setTimeout(() => {
sshConn!.shell(
// Check if connection still exists (might have been cleaned up)
if (!sshConn) {
sshLogger.warn(
"SSH connection was cleaned up before shell could be created",
{
operation: "ssh_shell",
hostId: id,
ip,
port,
username,
},
);
ws.send(
JSON.stringify({
type: "error",
message:
"SSH connection was closed before terminal could be created",
}),
);
return;
}
sshConn.shell(
{
rows: data.rows,
cols: data.cols,
@@ -570,7 +593,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
JSON.stringify({ type: "connected", message: "SSH connected" }),
);
// Log activity to homepage API
// Log activity to dashboard API
if (id && hostConfig.userId) {
(async () => {
try {
@@ -714,8 +737,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
);
if (totpPromptIndex !== -1) {
if (totpPromptSent) return;
// TOTP prompt detected - need user input
if (totpPromptSent) {
sshLogger.warn("TOTP prompt already sent, ignoring duplicate", {
operation: "ssh_keyboard_interactive",
hostId: id,
});
return;
}
totpPromptSent = true;
keyboardInteractiveResponded = true;
keyboardInteractiveFinish = (totpResponses: string[]) => {
const totpCode = (totpResponses[0] || "").trim();
@@ -748,6 +779,20 @@ wss.on("connection", async (ws: WebSocket, req) => {
}),
);
} else {
// Non-TOTP prompts (password, etc.) - respond automatically
if (keyboardInteractiveResponded) {
sshLogger.warn(
"Already responded to keyboard-interactive, ignoring subsequent prompt",
{
operation: "ssh_keyboard_interactive",
hostId: id,
prompts: promptTexts,
},
);
return;
}
keyboardInteractiveResponded = true;
const responses = prompts.map((p) => {
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
return resolvedCredentials.password;
@@ -761,6 +806,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
hasPassword: !!resolvedCredentials.password,
responsesProvided: responses.filter((r) => r !== "").length,
totalPrompts: prompts.length,
prompts: promptTexts,
});
console.log(
@@ -948,6 +994,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
totpPromptSent = false;
keyboardInteractiveResponded = false;
keyboardInteractiveFinish = null;
}

View File

@@ -104,7 +104,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await import("./ssh/tunnel.js");
await import("./ssh/file-manager.js");
await import("./ssh/server-stats.js");
await import("./homepage.js");
await import("./dashboard.js");
process.on("SIGINT", () => {
systemLogger.info(

View File

@@ -125,13 +125,17 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE ssh_data
SET password = ?, key = ?, key_password = ?, updated_at = CURRENT_TIMESTAMP
SET password = ?, key = ?, key_password = ?, key_type = ?, autostart_password = ?, autostart_key = ?, autostart_key_password = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
updatedRecord.key_password || null,
updatedRecord.key_password || updatedRecord.keyPassword || null,
updatedRecord.keyType || null,
updatedRecord.autostartPassword || null,
updatedRecord.autostartKey || null,
updatedRecord.autostartKeyPassword || null,
record.id,
);
@@ -160,15 +164,16 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE ssh_credentials
SET password = ?, key = ?, key_password = ?, private_key = ?, public_key = ?, updated_at = CURRENT_TIMESTAMP
SET password = ?, key = ?, key_password = ?, private_key = ?, public_key = ?, key_type = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
updatedRecord.key_password || null,
updatedRecord.private_key || null,
updatedRecord.public_key || null,
updatedRecord.key_password || updatedRecord.keyPassword || null,
updatedRecord.private_key || updatedRecord.privateKey || null,
updatedRecord.public_key || updatedRecord.publicKey || null,
updatedRecord.keyType || null,
record.id,
);
@@ -197,12 +202,18 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE users
SET totp_secret = ?, totp_backup_codes = ?
SET totp_secret = ?, totp_backup_codes = ?, client_secret = ?, oidc_identifier = ?
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.totp_secret || null,
updatedRecord.totp_backup_codes || null,
updatedRecord.totp_secret || updatedRecord.totpSecret || null,
updatedRecord.totp_backup_codes ||
updatedRecord.totpBackupCodes ||
null,
updatedRecord.client_secret || updatedRecord.clientSecret || null,
updatedRecord.oidc_identifier ||
updatedRecord.oidcIdentifier ||
null,
userId,
);
@@ -249,24 +260,44 @@ class DataCrypto {
try {
const tablesToReencrypt = [
{ table: "ssh_data", fields: ["password", "key", "key_password"] },
{
table: "ssh_data",
fields: [
"password",
"key",
"key_password",
"keyPassword",
"keyType",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
],
},
{
table: "ssh_credentials",
fields: [
"password",
"private_key",
"privateKey",
"key_password",
"keyPassword",
"key",
"public_key",
"publicKey",
"keyType",
],
},
{
table: "users",
fields: [
"client_secret",
"clientSecret",
"totp_secret",
"totpSecret",
"totp_backup_codes",
"totpBackupCodes",
"oidc_identifier",
"oidcIdentifier",
],
},
];

View File

@@ -17,19 +17,36 @@ class FieldCrypto {
private static readonly ENCRYPTED_FIELDS = {
users: new Set([
"password_hash",
"passwordHash",
"client_secret",
"clientSecret",
"totp_secret",
"totpSecret",
"totp_backup_codes",
"totpBackupCodes",
"oidc_identifier",
"oidcIdentifier",
]),
ssh_data: new Set(["password", "key", "key_password", "keyPassword"]),
ssh_data: new Set([
"password",
"key",
"key_password",
"keyPassword",
"keyType",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
]),
ssh_credentials: new Set([
"password",
"private_key",
"privateKey",
"key_password",
"keyPassword",
"key",
"public_key",
"publicKey",
"keyType",
]),
};

View File

@@ -253,6 +253,6 @@ export const apiLogger = new Logger("API", "🌐", "#3b82f6");
export const authLogger = new Logger("AUTH", "🔐", "#ef4444");
export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6");
export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6");
export const homepageLogger = new Logger("HOMEPAGE", "🏠", "#ec4899");
export const dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899");
export const logger = systemLogger;