fix: Several bug fixes for terminals, server stats, and general feature improvements
This commit is contained in:
@@ -456,6 +456,11 @@ const migrateSchema = () => {
|
|||||||
"credential_id",
|
"credential_id",
|
||||||
"INTEGER REFERENCES ssh_credentials(id)",
|
"INTEGER REFERENCES ssh_credentials(id)",
|
||||||
);
|
);
|
||||||
|
addColumnIfNotExists(
|
||||||
|
"ssh_data",
|
||||||
|
"override_credential_username",
|
||||||
|
"INTEGER",
|
||||||
|
);
|
||||||
|
|
||||||
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
|
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
|
||||||
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
|
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
autostartKeyPassword: text("autostart_key_password"),
|
autostartKeyPassword: text("autostart_key_password"),
|
||||||
|
|
||||||
credentialId: integer("credential_id").references(() => sshCredentials.id),
|
credentialId: integer("credential_id").references(() => sshCredentials.id),
|
||||||
|
overrideCredentialUsername: integer("override_credential_username", {
|
||||||
|
mode: "boolean",
|
||||||
|
}),
|
||||||
enableTerminal: integer("enable_terminal", { mode: "boolean" })
|
enableTerminal: integer("enable_terminal", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
fileManagerRecent,
|
fileManagerRecent,
|
||||||
fileManagerPinned,
|
fileManagerPinned,
|
||||||
fileManagerShortcuts,
|
fileManagerShortcuts,
|
||||||
|
recentActivity,
|
||||||
} from "../db/schema.js";
|
} from "../db/schema.js";
|
||||||
import { eq, and, desc, isNotNull, or } from "drizzle-orm";
|
import { eq, and, desc, isNotNull, or } from "drizzle-orm";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
@@ -225,6 +226,7 @@ router.post(
|
|||||||
authMethod,
|
authMethod,
|
||||||
authType,
|
authType,
|
||||||
credentialId,
|
credentialId,
|
||||||
|
overrideCredentialUsername,
|
||||||
key,
|
key,
|
||||||
keyPassword,
|
keyPassword,
|
||||||
keyType,
|
keyType,
|
||||||
@@ -264,6 +266,7 @@ router.post(
|
|||||||
username,
|
username,
|
||||||
authType: effectiveAuthType,
|
authType: effectiveAuthType,
|
||||||
credentialId: credentialId || null,
|
credentialId: credentialId || null,
|
||||||
|
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||||
pin: pin ? 1 : 0,
|
pin: pin ? 1 : 0,
|
||||||
enableTerminal: enableTerminal ? 1 : 0,
|
enableTerminal: enableTerminal ? 1 : 0,
|
||||||
enableTunnel: enableTunnel ? 1 : 0,
|
enableTunnel: enableTunnel ? 1 : 0,
|
||||||
@@ -323,6 +326,7 @@ router.post(
|
|||||||
: []
|
: []
|
||||||
: [],
|
: [],
|
||||||
pin: !!createdHost.pin,
|
pin: !!createdHost.pin,
|
||||||
|
overrideCredentialUsername: !!createdHost.overrideCredentialUsername,
|
||||||
enableTerminal: !!createdHost.enableTerminal,
|
enableTerminal: !!createdHost.enableTerminal,
|
||||||
enableTunnel: !!createdHost.enableTunnel,
|
enableTunnel: !!createdHost.enableTunnel,
|
||||||
tunnelConnections: createdHost.tunnelConnections
|
tunnelConnections: createdHost.tunnelConnections
|
||||||
@@ -349,6 +353,27 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetch = (await import("node-fetch")).default;
|
||||||
|
const token =
|
||||||
|
req.cookies?.jwt || req.headers.authorization?.replace("Bearer ", "");
|
||||||
|
await fetch("http://localhost:30005/refresh", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(token && { Cookie: `jwt=${token}` }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (refreshError) {
|
||||||
|
sshLogger.warn("Failed to refresh server stats polling", {
|
||||||
|
operation: "stats_refresh_after_create",
|
||||||
|
error:
|
||||||
|
refreshError instanceof Error
|
||||||
|
? refreshError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json(resolvedHost);
|
res.json(resolvedHost);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error("Failed to save SSH host to database", err, {
|
sshLogger.error("Failed to save SSH host to database", err, {
|
||||||
@@ -415,6 +440,7 @@ router.put(
|
|||||||
authMethod,
|
authMethod,
|
||||||
authType,
|
authType,
|
||||||
credentialId,
|
credentialId,
|
||||||
|
overrideCredentialUsername,
|
||||||
key,
|
key,
|
||||||
keyPassword,
|
keyPassword,
|
||||||
keyType,
|
keyType,
|
||||||
@@ -455,6 +481,7 @@ router.put(
|
|||||||
username,
|
username,
|
||||||
authType: effectiveAuthType,
|
authType: effectiveAuthType,
|
||||||
credentialId: credentialId || null,
|
credentialId: credentialId || null,
|
||||||
|
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||||
pin: pin ? 1 : 0,
|
pin: pin ? 1 : 0,
|
||||||
enableTerminal: enableTerminal ? 1 : 0,
|
enableTerminal: enableTerminal ? 1 : 0,
|
||||||
enableTunnel: enableTunnel ? 1 : 0,
|
enableTunnel: enableTunnel ? 1 : 0,
|
||||||
@@ -532,6 +559,7 @@ router.put(
|
|||||||
: []
|
: []
|
||||||
: [],
|
: [],
|
||||||
pin: !!updatedHost.pin,
|
pin: !!updatedHost.pin,
|
||||||
|
overrideCredentialUsername: !!updatedHost.overrideCredentialUsername,
|
||||||
enableTerminal: !!updatedHost.enableTerminal,
|
enableTerminal: !!updatedHost.enableTerminal,
|
||||||
enableTunnel: !!updatedHost.enableTunnel,
|
enableTunnel: !!updatedHost.enableTunnel,
|
||||||
tunnelConnections: updatedHost.tunnelConnections
|
tunnelConnections: updatedHost.tunnelConnections
|
||||||
@@ -558,6 +586,27 @@ router.put(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetch = (await import("node-fetch")).default;
|
||||||
|
const token =
|
||||||
|
req.cookies?.jwt || req.headers.authorization?.replace("Bearer ", "");
|
||||||
|
await fetch("http://localhost:30005/refresh", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(token && { Cookie: `jwt=${token}` }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (refreshError) {
|
||||||
|
sshLogger.warn("Failed to refresh server stats polling", {
|
||||||
|
operation: "stats_refresh_after_update",
|
||||||
|
error:
|
||||||
|
refreshError instanceof Error
|
||||||
|
? refreshError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json(resolvedHost);
|
res.json(resolvedHost);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error("Failed to update SSH host in database", err, {
|
sshLogger.error("Failed to update SSH host in database", err, {
|
||||||
@@ -585,6 +634,18 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
return res.status(400).json({ error: "Invalid userId" });
|
return res.status(400).json({ error: "Invalid userId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
sshLogger.warn("User data not unlocked for SSH host fetch", {
|
||||||
|
operation: "host_fetch",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await SimpleDBOps.select(
|
const data = await SimpleDBOps.select(
|
||||||
db.select().from(sshData).where(eq(sshData.userId, userId)),
|
db.select().from(sshData).where(eq(sshData.userId, userId)),
|
||||||
@@ -603,6 +664,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
: []
|
: []
|
||||||
: [],
|
: [],
|
||||||
pin: !!row.pin,
|
pin: !!row.pin,
|
||||||
|
overrideCredentialUsername: !!row.overrideCredentialUsername,
|
||||||
enableTerminal: !!row.enableTerminal,
|
enableTerminal: !!row.enableTerminal,
|
||||||
enableTunnel: !!row.enableTunnel,
|
enableTunnel: !!row.enableTunnel,
|
||||||
tunnelConnections: row.tunnelConnections
|
tunnelConnections: row.tunnelConnections
|
||||||
@@ -649,6 +711,19 @@ router.get(
|
|||||||
});
|
});
|
||||||
return res.status(400).json({ error: "Invalid userId or hostId" });
|
return res.status(400).json({ error: "Invalid userId or hostId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
sshLogger.warn("User data not unlocked for SSH host fetch by ID", {
|
||||||
|
operation: "host_fetch_by_id",
|
||||||
|
hostId: parseInt(hostId),
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await db
|
const data = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -674,6 +749,7 @@ router.get(
|
|||||||
: []
|
: []
|
||||||
: [],
|
: [],
|
||||||
pin: !!host.pin,
|
pin: !!host.pin,
|
||||||
|
overrideCredentialUsername: !!host.overrideCredentialUsername,
|
||||||
enableTerminal: !!host.enableTerminal,
|
enableTerminal: !!host.enableTerminal,
|
||||||
enableTunnel: !!host.enableTunnel,
|
enableTunnel: !!host.enableTunnel,
|
||||||
tunnelConnections: host.tunnelConnections
|
tunnelConnections: host.tunnelConnections
|
||||||
@@ -848,6 +924,15 @@ router.delete(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(recentActivity)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(recentActivity.userId, userId),
|
||||||
|
eq(recentActivity.hostId, numericHostId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.delete(sshData)
|
.delete(sshData)
|
||||||
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
|
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
|
||||||
@@ -1267,7 +1352,9 @@ async function resolveHostCredentials(
|
|||||||
const credential = credentials[0];
|
const credential = credentials[0];
|
||||||
return {
|
return {
|
||||||
...host,
|
...host,
|
||||||
username: credential.username,
|
username: host.overrideCredentialUsername
|
||||||
|
? host.username
|
||||||
|
: credential.username,
|
||||||
authType: credential.auth_type || credential.authType,
|
authType: credential.auth_type || credential.authType,
|
||||||
password: credential.password,
|
password: credential.password,
|
||||||
key: credential.key,
|
key: credential.key,
|
||||||
@@ -1446,8 +1533,10 @@ router.post(
|
|||||||
username: hostData.username,
|
username: hostData.username,
|
||||||
password: hostData.authType === "password" ? hostData.password : null,
|
password: hostData.authType === "password" ? hostData.password : null,
|
||||||
authType: hostData.authType,
|
authType: hostData.authType,
|
||||||
credentialId:
|
credentialId: hostData.credentialId || null,
|
||||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
overrideCredentialUsername: hostData.overrideCredentialUsername
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
key: hostData.authType === "key" ? hostData.key : null,
|
key: hostData.authType === "key" ? hostData.key : null,
|
||||||
keyPassword:
|
keyPassword:
|
||||||
hostData.authType === "key"
|
hostData.authType === "key"
|
||||||
|
|||||||
@@ -226,6 +226,16 @@ router.post("/create", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
|
||||||
|
await saveMemoryDatabaseToFile();
|
||||||
|
} catch (saveError) {
|
||||||
|
authLogger.error("Failed to persist user to disk", saveError, {
|
||||||
|
operation: "user_create_save_failed",
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.success(
|
||||||
`Traditional user created: ${username} (is_admin: ${isFirstUser})`,
|
`Traditional user created: ${username} (is_admin: ${isFirstUser})`,
|
||||||
{
|
{
|
||||||
@@ -785,6 +795,16 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
|
||||||
|
await saveMemoryDatabaseToFile();
|
||||||
|
} catch (saveError) {
|
||||||
|
authLogger.error("Failed to persist OIDC user to disk", saveError, {
|
||||||
|
operation: "oidc_user_create_save_failed",
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
user = await db.select().from(users).where(eq(users.id, id));
|
user = await db.select().from(users).where(eq(users.id, id));
|
||||||
} else {
|
} else {
|
||||||
await db
|
await db
|
||||||
@@ -836,6 +856,9 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
? 30 * 24 * 60 * 60 * 1000
|
? 30 * 24 * 60 * 60 * 1000
|
||||||
: 7 * 24 * 60 * 60 * 1000;
|
: 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Clear any existing JWT cookie first to prevent conflicts
|
||||||
|
res.clearCookie("jwt", authManager.getSecureCookieOptions(req));
|
||||||
|
|
||||||
return res
|
return res
|
||||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
|
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
|
||||||
.redirect(redirectUrl.toString());
|
.redirect(redirectUrl.toString());
|
||||||
@@ -1653,6 +1676,16 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
|
|||||||
.set({ is_admin: true })
|
.set({ is_admin: true })
|
||||||
.where(eq(users.username, username));
|
.where(eq(users.username, username));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
|
||||||
|
await saveMemoryDatabaseToFile();
|
||||||
|
} catch (saveError) {
|
||||||
|
authLogger.error("Failed to persist admin promotion to disk", saveError, {
|
||||||
|
operation: "make_admin_save_failed",
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.success(
|
||||||
`User ${username} made admin by ${adminUser[0].username}`,
|
`User ${username} made admin by ${adminUser[0].username}`,
|
||||||
);
|
);
|
||||||
@@ -1702,6 +1735,16 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
|
|||||||
.set({ is_admin: false })
|
.set({ is_admin: false })
|
||||||
.where(eq(users.username, username));
|
.where(eq(users.username, username));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
|
||||||
|
await saveMemoryDatabaseToFile();
|
||||||
|
} catch (saveError) {
|
||||||
|
authLogger.error("Failed to persist admin removal to disk", saveError, {
|
||||||
|
operation: "remove_admin_save_failed",
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
authLogger.success(
|
authLogger.success(
|
||||||
`Admin status removed from ${username} by ${adminUser[0].username}`,
|
`Admin status removed from ${username} by ${adminUser[0].username}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -845,7 +845,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
|
|
||||||
const escapedPath = sshPath.replace(/'/g, "'\"'\"'");
|
const escapedPath = sshPath.replace(/'/g, "'\"'\"'");
|
||||||
sshConn.client.exec(`ls -la '${escapedPath}'`, (err, stream) => {
|
sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
fileLogger.error("SSH listFiles error:", err);
|
fileLogger.error("SSH listFiles error:", err);
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: err.message });
|
||||||
|
|||||||
@@ -455,6 +455,13 @@ class PollingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!statsConfig.statusCheckEnabled && !statsConfig.metricsEnabled) {
|
||||||
|
this.pollingConfigs.delete(host.id);
|
||||||
|
this.statusStore.delete(host.id);
|
||||||
|
this.metricsStore.delete(host.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const config: HostPollingConfig = {
|
const config: HostPollingConfig = {
|
||||||
host,
|
host,
|
||||||
statsConfig,
|
statsConfig,
|
||||||
@@ -514,7 +521,7 @@ class PollingManager {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopPollingForHost(hostId: number): void {
|
stopPollingForHost(hostId: number, clearData = true): void {
|
||||||
const config = this.pollingConfigs.get(hostId);
|
const config = this.pollingConfigs.get(hostId);
|
||||||
if (config) {
|
if (config) {
|
||||||
if (config.statusTimer) {
|
if (config.statusTimer) {
|
||||||
@@ -524,8 +531,10 @@ class PollingManager {
|
|||||||
clearInterval(config.metricsTimer);
|
clearInterval(config.metricsTimer);
|
||||||
}
|
}
|
||||||
this.pollingConfigs.delete(hostId);
|
this.pollingConfigs.delete(hostId);
|
||||||
this.statusStore.delete(hostId);
|
if (clearData) {
|
||||||
this.metricsStore.delete(hostId);
|
this.statusStore.delete(hostId);
|
||||||
|
this.metricsStore.delete(hostId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,11 +563,23 @@ class PollingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshHostPolling(userId: string): Promise<void> {
|
async refreshHostPolling(userId: string): Promise<void> {
|
||||||
|
const hosts = await fetchAllHosts(userId);
|
||||||
|
const currentHostIds = new Set(hosts.map((h) => h.id));
|
||||||
|
|
||||||
for (const hostId of this.pollingConfigs.keys()) {
|
for (const hostId of this.pollingConfigs.keys()) {
|
||||||
this.stopPollingForHost(hostId);
|
this.stopPollingForHost(hostId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.initializePolling(userId);
|
for (const hostId of this.statusStore.keys()) {
|
||||||
|
if (!currentHostIds.has(hostId)) {
|
||||||
|
this.statusStore.delete(hostId);
|
||||||
|
this.metricsStore.delete(hostId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
await this.startPollingForHost(host);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
let totpPromptSent = false;
|
let totpPromptSent = false;
|
||||||
let isKeyboardInteractive = false;
|
let isKeyboardInteractive = false;
|
||||||
let keyboardInteractiveResponded = false;
|
let keyboardInteractiveResponded = false;
|
||||||
|
let isConnecting = false;
|
||||||
|
let isConnected = false;
|
||||||
|
let isCleaningUp = false;
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
const userWs = userConnections.get(userId);
|
const userWs = userConnections.get(userId);
|
||||||
@@ -417,10 +420,21 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isConnecting || isConnected) {
|
||||||
|
sshLogger.warn("Connection already in progress or established", {
|
||||||
|
operation: "ssh_connect",
|
||||||
|
hostId: id,
|
||||||
|
isConnecting,
|
||||||
|
isConnected,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnecting = true;
|
||||||
sshConn = new Client();
|
sshConn = new Client();
|
||||||
|
|
||||||
const connectionTimeout = setTimeout(() => {
|
const connectionTimeout = setTimeout(() => {
|
||||||
if (sshConn) {
|
if (sshConn && isConnecting && !isConnected) {
|
||||||
sshLogger.error("SSH connection timeout", undefined, {
|
sshLogger.error("SSH connection timeout", undefined, {
|
||||||
operation: "ssh_connect",
|
operation: "ssh_connect",
|
||||||
hostId: id,
|
hostId: id,
|
||||||
@@ -433,7 +447,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
);
|
);
|
||||||
cleanupSSH(connectionTimeout);
|
cleanupSSH(connectionTimeout);
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 120000);
|
||||||
|
|
||||||
let resolvedCredentials = { password, key, keyPassword, keyType, authType };
|
let resolvedCredentials = { password, key, keyPassword, keyType, authType };
|
||||||
let authMethodNotAvailable = false;
|
let authMethodNotAvailable = false;
|
||||||
@@ -498,7 +512,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sshConn.on("ready", () => {
|
sshConn.on("ready", () => {
|
||||||
clearTimeout(connectionTimeout);
|
clearTimeout(connectionTimeout);
|
||||||
|
|
||||||
if (!sshConn) {
|
const conn = sshConn;
|
||||||
|
|
||||||
|
if (!conn || isCleaningUp) {
|
||||||
sshLogger.warn(
|
sshLogger.warn(
|
||||||
"SSH connection was cleaned up before shell could be created",
|
"SSH connection was cleaned up before shell could be created",
|
||||||
{
|
{
|
||||||
@@ -507,6 +523,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
ip,
|
ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username,
|
||||||
|
isCleaningUp,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -519,7 +536,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sshConn.shell(
|
isConnecting = false;
|
||||||
|
isConnected = true;
|
||||||
|
|
||||||
|
conn.shell(
|
||||||
{
|
{
|
||||||
rows: data.rows,
|
rows: data.rows,
|
||||||
cols: data.cols,
|
cols: data.cols,
|
||||||
@@ -836,9 +856,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
keepaliveInterval: 30000,
|
keepaliveInterval: 30000,
|
||||||
keepaliveCountMax: 3,
|
keepaliveCountMax: 3,
|
||||||
readyTimeout: 60000,
|
readyTimeout: 120000,
|
||||||
tcpKeepAlive: true,
|
tcpKeepAlive: true,
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
tcpKeepAliveInitialDelay: 30000,
|
||||||
|
timeout: 120000,
|
||||||
env: {
|
env: {
|
||||||
TERM: "xterm-256color",
|
TERM: "xterm-256color",
|
||||||
LANG: "en_US.UTF-8",
|
LANG: "en_US.UTF-8",
|
||||||
@@ -982,6 +1003,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanupSSH(timeoutId?: NodeJS.Timeout) {
|
function cleanupSSH(timeoutId?: NodeJS.Timeout) {
|
||||||
|
if (isCleaningUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isCleaningUp = true;
|
||||||
|
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1045,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
isKeyboardInteractive = false;
|
isKeyboardInteractive = false;
|
||||||
keyboardInteractiveResponded = false;
|
keyboardInteractiveResponded = false;
|
||||||
keyboardInteractiveFinish = null;
|
keyboardInteractiveFinish = null;
|
||||||
|
isConnecting = false;
|
||||||
|
isConnected = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isCleaningUp = false;
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPingInterval() {
|
function setupPingInterval() {
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
"orCreateNewFolder": "Or create new folder",
|
"orCreateNewFolder": "Or create new folder",
|
||||||
"addTag": "Add tag",
|
"addTag": "Add tag",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
|
"credentialId": "Credential ID",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
@@ -782,7 +783,9 @@
|
|||||||
"noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
|
"noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
|
||||||
"noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
|
"noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
|
||||||
"forceKeyboardInteractive": "Force Keyboard-Interactive",
|
"forceKeyboardInteractive": "Force Keyboard-Interactive",
|
||||||
"forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is often required for servers that use Two-Factor Authentication (TOTP/2FA)."
|
"forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is often required for servers that use Two-Factor Authentication (TOTP/2FA).",
|
||||||
|
"overrideCredentialUsername": "Override Credential Username",
|
||||||
|
"overrideCredentialUsernameDesc": "Use a different username than the one stored in the credential. This allows you to use the same credential with different usernames."
|
||||||
},
|
},
|
||||||
"terminal": {
|
"terminal": {
|
||||||
"title": "Terminal",
|
"title": "Terminal",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface SSHHost {
|
|||||||
autostartKeyPassword?: string;
|
autostartKeyPassword?: string;
|
||||||
|
|
||||||
credentialId?: number;
|
credentialId?: number;
|
||||||
|
overrideCredentialUsername?: boolean;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
enableTerminal: boolean;
|
enableTerminal: boolean;
|
||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
@@ -52,6 +53,7 @@ export interface SSHHostData {
|
|||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
keyType?: string;
|
keyType?: string;
|
||||||
credentialId?: number | null;
|
credentialId?: number | null;
|
||||||
|
overrideCredentialUsername?: boolean;
|
||||||
enableTerminal?: boolean;
|
enableTerminal?: boolean;
|
||||||
enableTunnel?: boolean;
|
enableTunnel?: boolean;
|
||||||
enableFileManager?: boolean;
|
enableFileManager?: boolean;
|
||||||
|
|||||||
@@ -640,6 +640,9 @@ export function CredentialsManager({
|
|||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{credential.username}
|
{credential.username}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
ID: {credential.id}
|
||||||
|
</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{credential.authType === "password"
|
{credential.authType === "password"
|
||||||
? t("credentials.password")
|
? t("credentials.password")
|
||||||
|
|||||||
@@ -527,41 +527,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onerror = () => reject(reader.error);
|
reader.onerror = () => reject(reader.error);
|
||||||
|
|
||||||
const isTextFile =
|
reader.onload = () => {
|
||||||
file.type.startsWith("text/") ||
|
if (reader.result instanceof ArrayBuffer) {
|
||||||
file.type === "application/json" ||
|
const bytes = new Uint8Array(reader.result);
|
||||||
file.type === "application/javascript" ||
|
let binary = "";
|
||||||
file.type === "application/xml" ||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
file.type === "image/svg+xml" ||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
file.name.match(
|
|
||||||
/\.(txt|json|js|ts|jsx|tsx|css|scss|less|html|htm|xml|svg|yaml|yml|md|markdown|mdown|mkdn|mdx|py|java|c|cpp|h|sh|bash|zsh|bat|ps1|toml|ini|conf|config|sql|vue|svelte)$/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTextFile) {
|
|
||||||
reader.onload = () => {
|
|
||||||
if (reader.result) {
|
|
||||||
resolve(reader.result as string);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to read text file content"));
|
|
||||||
}
|
}
|
||||||
};
|
const base64 = btoa(binary);
|
||||||
reader.readAsText(file);
|
resolve(base64);
|
||||||
} else {
|
} else {
|
||||||
reader.onload = () => {
|
reject(new Error("Failed to read file"));
|
||||||
if (reader.result instanceof ArrayBuffer) {
|
}
|
||||||
const bytes = new Uint8Array(reader.result);
|
};
|
||||||
let binary = "";
|
reader.readAsArrayBuffer(file);
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
const base64 = btoa(binary);
|
|
||||||
resolve(base64);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to read binary file"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await uploadSSHFile(
|
await uploadSSHFile(
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ export function HostManagerEditor({
|
|||||||
pin: z.boolean().default(false),
|
pin: z.boolean().default(false),
|
||||||
authType: z.enum(["password", "key", "credential", "none"]),
|
authType: z.enum(["password", "key", "credential", "none"]),
|
||||||
credentialId: z.number().optional().nullable(),
|
credentialId: z.number().optional().nullable(),
|
||||||
|
overrideCredentialUsername: z.boolean().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
key: z.any().optional().nullable(),
|
key: z.any().optional().nullable(),
|
||||||
keyPassword: z.string().optional(),
|
keyPassword: z.string().optional(),
|
||||||
@@ -389,6 +390,7 @@ export function HostManagerEditor({
|
|||||||
pin: false,
|
pin: false,
|
||||||
authType: "password" as const,
|
authType: "password" as const,
|
||||||
credentialId: null,
|
credentialId: null,
|
||||||
|
overrideCredentialUsername: false,
|
||||||
password: "",
|
password: "",
|
||||||
key: null,
|
key: null,
|
||||||
keyPassword: "",
|
keyPassword: "",
|
||||||
@@ -407,7 +409,8 @@ export function HostManagerEditor({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authTab === "credential") {
|
if (authTab === "credential") {
|
||||||
const currentCredentialId = form.getValues("credentialId");
|
const currentCredentialId = form.getValues("credentialId");
|
||||||
if (currentCredentialId) {
|
const overrideUsername = form.getValues("overrideCredentialUsername");
|
||||||
|
if (currentCredentialId && !overrideUsername) {
|
||||||
const selectedCredential = credentials.find(
|
const selectedCredential = credentials.find(
|
||||||
(c) => c.id === currentCredentialId,
|
(c) => c.id === currentCredentialId,
|
||||||
);
|
);
|
||||||
@@ -464,6 +467,9 @@ export function HostManagerEditor({
|
|||||||
pin: Boolean(cleanedHost.pin),
|
pin: Boolean(cleanedHost.pin),
|
||||||
authType: defaultAuthType as "password" | "key" | "credential" | "none",
|
authType: defaultAuthType as "password" | "key" | "credential" | "none",
|
||||||
credentialId: null,
|
credentialId: null,
|
||||||
|
overrideCredentialUsername: Boolean(
|
||||||
|
cleanedHost.overrideCredentialUsername,
|
||||||
|
),
|
||||||
password: "",
|
password: "",
|
||||||
key: null,
|
key: null,
|
||||||
keyPassword: "",
|
keyPassword: "",
|
||||||
@@ -512,6 +518,7 @@ export function HostManagerEditor({
|
|||||||
pin: false,
|
pin: false,
|
||||||
authType: "password" as const,
|
authType: "password" as const,
|
||||||
credentialId: null,
|
credentialId: null,
|
||||||
|
overrideCredentialUsername: false,
|
||||||
password: "",
|
password: "",
|
||||||
key: null,
|
key: null,
|
||||||
keyPassword: "",
|
keyPassword: "",
|
||||||
@@ -574,6 +581,7 @@ export function HostManagerEditor({
|
|||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
pin: Boolean(data.pin),
|
pin: Boolean(data.pin),
|
||||||
authType: data.authType,
|
authType: data.authType,
|
||||||
|
overrideCredentialUsername: Boolean(data.overrideCredentialUsername),
|
||||||
enableTerminal: Boolean(data.enableTerminal),
|
enableTerminal: Boolean(data.enableTerminal),
|
||||||
enableTunnel: Boolean(data.enableTunnel),
|
enableTunnel: Boolean(data.enableTunnel),
|
||||||
enableFileManager: Boolean(data.enableFileManager),
|
enableFileManager: Boolean(data.enableFileManager),
|
||||||
@@ -882,17 +890,28 @@ export function HostManagerEditor({
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem className="col-span-6">
|
const isCredentialAuth = authTab === "credential";
|
||||||
<FormLabel>{t("hosts.username")}</FormLabel>
|
const hasCredential = !!form.watch("credentialId");
|
||||||
<FormControl>
|
const overrideEnabled = !!form.watch(
|
||||||
<Input
|
"overrideCredentialUsername",
|
||||||
placeholder={t("placeholders.username")}
|
);
|
||||||
{...field}
|
const shouldDisable =
|
||||||
/>
|
isCredentialAuth && hasCredential && !overrideEnabled;
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
return (
|
||||||
)}
|
<FormItem className="col-span-6">
|
||||||
|
<FormLabel>{t("hosts.username")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t("placeholders.username")}
|
||||||
|
disabled={shouldDisable}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormLabel className="mb-3 mt-3 font-bold">
|
<FormLabel className="mb-3 mt-3 font-bold">
|
||||||
@@ -1263,29 +1282,60 @@ export function HostManagerEditor({
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="credential">
|
<TabsContent value="credential">
|
||||||
<FormField
|
<div className="space-y-4">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="credentialId"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="credentialId"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<CredentialSelector
|
<FormItem>
|
||||||
value={field.value}
|
<CredentialSelector
|
||||||
onValueChange={field.onChange}
|
value={field.value}
|
||||||
onCredentialSelect={(credential) => {
|
onValueChange={field.onChange}
|
||||||
if (credential) {
|
onCredentialSelect={(credential) => {
|
||||||
form.setValue(
|
if (
|
||||||
"username",
|
credential &&
|
||||||
credential.username,
|
!form.getValues(
|
||||||
);
|
"overrideCredentialUsername",
|
||||||
}
|
)
|
||||||
}}
|
) {
|
||||||
/>
|
form.setValue(
|
||||||
<FormDescription>
|
"username",
|
||||||
{t("hosts.credentialDescription")}
|
credential.username,
|
||||||
</FormDescription>
|
);
|
||||||
</FormItem>
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormDescription>
|
||||||
|
{t("hosts.credentialDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{form.watch("credentialId") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="overrideCredentialUsername"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.overrideCredentialUsername")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("hosts.overrideCredentialUsernameDesc")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="none">
|
<TabsContent value="none">
|
||||||
<Alert className="mt-2">
|
<Alert className="mt-2">
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const [keyboardInteractiveDetected, setKeyboardInteractiveDetected] =
|
const [keyboardInteractiveDetected, setKeyboardInteractiveDetected] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const isVisibleRef = useRef<boolean>(false);
|
const isVisibleRef = useRef<boolean>(false);
|
||||||
|
const isReadyRef = useRef<boolean>(false);
|
||||||
const isFittingRef = useRef(false);
|
const isFittingRef = useRef(false);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const reconnectAttempts = useRef(0);
|
const reconnectAttempts = useRef(0);
|
||||||
@@ -157,6 +158,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
isVisibleRef.current = isVisible;
|
isVisibleRef.current = isVisible;
|
||||||
}, [isVisible]);
|
}, [isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isReadyRef.current = isReady;
|
||||||
|
}, [isReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = () => {
|
const checkAuth = () => {
|
||||||
const jwtToken = getCookie("jwt");
|
const jwtToken = getCookie("jwt");
|
||||||
@@ -507,6 +512,9 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
terminal.onData((data) => {
|
terminal.onData((data) => {
|
||||||
|
if (data === "\x00" || data === "\u0000") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ws.send(JSON.stringify({ type: "input", data }));
|
ws.send(JSON.stringify({ type: "input", data }));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -915,15 +923,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
|
|
||||||
element?.addEventListener("keydown", handleMacKeyboard, true);
|
element?.addEventListener("keydown", handleMacKeyboard, true);
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const handleResize = () => {
|
||||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
resizeTimeout.current = setTimeout(() => {
|
resizeTimeout.current = setTimeout(() => {
|
||||||
if (!isVisibleRef.current || !isReady) return;
|
if (!isVisibleRef.current || !isReadyRef.current) return;
|
||||||
performFit();
|
performFit();
|
||||||
}, 50);
|
}, 100);
|
||||||
});
|
};
|
||||||
|
|
||||||
resizeObserver.observe(xtermRef.current);
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
|
||||||
|
if (xtermRef.current) {
|
||||||
|
resizeObserver.observe(xtermRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
|
||||||
@@ -936,6 +950,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
setIsReady(false);
|
setIsReady(false);
|
||||||
isFittingRef.current = false;
|
isFittingRef.current = false;
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
element?.removeEventListener("contextmenu", handleContextMenu);
|
element?.removeEventListener("contextmenu", handleContextMenu);
|
||||||
element?.removeEventListener("keydown", handleMacKeyboard, true);
|
element?.removeEventListener("keydown", handleMacKeyboard, true);
|
||||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
|
|||||||
@@ -558,65 +558,61 @@ export function Auth({
|
|||||||
if (success) {
|
if (success) {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
|
|
||||||
getUserInfo()
|
// Clear the success parameter first to prevent re-processing
|
||||||
.then((meRes) => {
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
if (isInElectronWebView()) {
|
|
||||||
const token = getCookie("jwt") || localStorage.getItem("jwt");
|
setTimeout(() => {
|
||||||
if (token) {
|
getUserInfo()
|
||||||
try {
|
.then((meRes) => {
|
||||||
window.parent.postMessage(
|
if (isInElectronWebView()) {
|
||||||
{
|
const token = getCookie("jwt") || localStorage.getItem("jwt");
|
||||||
type: "AUTH_SUCCESS",
|
if (token) {
|
||||||
token: token,
|
try {
|
||||||
source: "oidc_callback",
|
window.parent.postMessage(
|
||||||
platform: "desktop",
|
{
|
||||||
timestamp: Date.now(),
|
type: "AUTH_SUCCESS",
|
||||||
},
|
token: token,
|
||||||
"*",
|
source: "oidc_callback",
|
||||||
);
|
platform: "desktop",
|
||||||
setWebviewAuthSuccess(true);
|
timestamp: Date.now(),
|
||||||
setTimeout(() => window.location.reload(), 100);
|
},
|
||||||
setOidcLoading(false);
|
"*",
|
||||||
return;
|
);
|
||||||
} catch (e) {
|
setWebviewAuthSuccess(true);
|
||||||
console.error("Error posting auth success message:", e);
|
setTimeout(() => window.location.reload(), 100);
|
||||||
|
setOidcLoading(false);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error posting auth success message:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setIsAdmin(!!meRes.is_admin);
|
setIsAdmin(!!meRes.is_admin);
|
||||||
setUsername(meRes.username || null);
|
setUsername(meRes.username || null);
|
||||||
setUserId(meRes.userId || null);
|
setUserId(meRes.userId || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
onAuthSuccess({
|
onAuthSuccess({
|
||||||
isAdmin: !!meRes.is_admin,
|
isAdmin: !!meRes.is_admin,
|
||||||
username: meRes.username || null,
|
username: meRes.username || null,
|
||||||
userId: meRes.userId || null,
|
userId: meRes.userId || null,
|
||||||
|
});
|
||||||
|
setInternalLoggedIn(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to get user info after OIDC callback:", err);
|
||||||
|
setInternalLoggedIn(false);
|
||||||
|
setLoggedIn(false);
|
||||||
|
setIsAdmin(false);
|
||||||
|
setUsername(null);
|
||||||
|
setUserId(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setOidcLoading(false);
|
||||||
});
|
});
|
||||||
setInternalLoggedIn(true);
|
}, 200);
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setInternalLoggedIn(false);
|
|
||||||
setLoggedIn(false);
|
|
||||||
setIsAdmin(false);
|
|
||||||
setUsername(null);
|
|
||||||
setUserId(null);
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setOidcLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onAuthSuccess,
|
onAuthSuccess,
|
||||||
|
|||||||
@@ -318,34 +318,37 @@ function createApiInstance(
|
|||||||
const errorMessage = (error.response?.data as Record<string, unknown>)
|
const errorMessage = (error.response?.data as Record<string, unknown>)
|
||||||
?.error;
|
?.error;
|
||||||
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
||||||
|
const isSessionNotFound = errorCode === "SESSION_NOT_FOUND";
|
||||||
const isInvalidToken =
|
const isInvalidToken =
|
||||||
errorCode === "AUTH_REQUIRED" ||
|
errorCode === "AUTH_REQUIRED" ||
|
||||||
errorMessage === "Invalid token" ||
|
errorMessage === "Invalid token" ||
|
||||||
errorMessage === "Authentication required";
|
errorMessage === "Authentication required";
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isSessionExpired || isSessionNotFound) {
|
||||||
localStorage.removeItem("jwt");
|
if (isElectron()) {
|
||||||
} else {
|
localStorage.removeItem("jwt");
|
||||||
localStorage.removeItem("jwt");
|
} else {
|
||||||
}
|
localStorage.removeItem("jwt");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (typeof window !== "undefined") {
|
||||||
(isSessionExpired || isInvalidToken) &&
|
console.warn("Session expired or not found - please log in again");
|
||||||
typeof window !== "undefined"
|
|
||||||
) {
|
document.cookie =
|
||||||
|
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
|
|
||||||
|
import("sonner").then(({ toast }) => {
|
||||||
|
toast.warning("Session expired. Please log in again.");
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
}
|
||||||
|
} else if (isInvalidToken && typeof window !== "undefined") {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Session expired or invalid token - please log in again",
|
"Authentication error - token may be invalid",
|
||||||
|
errorMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
document.cookie =
|
|
||||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
||||||
|
|
||||||
import("sonner").then(({ toast }) => {
|
|
||||||
toast.warning("Session expired. Please log in again.");
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,6 +795,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
||||||
credentialId:
|
credentialId:
|
||||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
hostData.authType === "credential" ? hostData.credentialId : null,
|
||||||
|
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
enableTerminal: Boolean(hostData.enableTerminal),
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
enableTunnel: Boolean(hostData.enableTunnel),
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
enableFileManager: Boolean(hostData.enableFileManager),
|
||||||
@@ -855,6 +859,7 @@ export async function updateSSHHost(
|
|||||||
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
keyType: hostData.authType === "key" ? hostData.keyType : null,
|
||||||
credentialId:
|
credentialId:
|
||||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
hostData.authType === "credential" ? hostData.credentialId : null,
|
||||||
|
overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
|
||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
enableTerminal: Boolean(hostData.enableTerminal),
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
enableTunnel: Boolean(hostData.enableTunnel),
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
enableFileManager: Boolean(hostData.enableFileManager),
|
||||||
|
|||||||
@@ -504,55 +504,45 @@ export function Auth({
|
|||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
getUserInfo()
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
.then((meRes) => {
|
|
||||||
setIsAdmin(!!meRes.is_admin);
|
|
||||||
setUsername(meRes.username || null);
|
|
||||||
setUserId(meRes.userId || null);
|
|
||||||
setDbError(null);
|
|
||||||
postJWTToWebView();
|
|
||||||
|
|
||||||
if (isReactNativeWebView()) {
|
setTimeout(() => {
|
||||||
setMobileAuthSuccess(true);
|
getUserInfo()
|
||||||
|
.then((meRes) => {
|
||||||
|
setIsAdmin(!!meRes.is_admin);
|
||||||
|
setUsername(meRes.username || null);
|
||||||
|
setUserId(meRes.userId || null);
|
||||||
|
setDbError(null);
|
||||||
|
postJWTToWebView();
|
||||||
|
|
||||||
|
if (isReactNativeWebView()) {
|
||||||
|
setMobileAuthSuccess(true);
|
||||||
|
setOidcLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoggedIn(true);
|
||||||
|
onAuthSuccess({
|
||||||
|
isAdmin: !!meRes.is_admin,
|
||||||
|
username: meRes.username || null,
|
||||||
|
userId: meRes.userId || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInternalLoggedIn(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to get user info after OIDC callback:", err);
|
||||||
|
setError(t("errors.failedUserInfo"));
|
||||||
|
setInternalLoggedIn(false);
|
||||||
|
setLoggedIn(false);
|
||||||
|
setIsAdmin(false);
|
||||||
|
setUsername(null);
|
||||||
|
setUserId(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoggedIn(true);
|
|
||||||
onAuthSuccess({
|
|
||||||
isAdmin: !!meRes.is_admin,
|
|
||||||
username: meRes.username || null,
|
|
||||||
userId: meRes.userId || null,
|
|
||||||
});
|
});
|
||||||
|
}, 200);
|
||||||
setInternalLoggedIn(true);
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setError(t("errors.failedUserInfo"));
|
|
||||||
setInternalLoggedIn(false);
|
|
||||||
setLoggedIn(false);
|
|
||||||
setIsAdmin(false);
|
|
||||||
setUsername(null);
|
|
||||||
setUserId(null);
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setOidcLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user