mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
v2.0.0 (#621)
* Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes * Merge 2.0.0 with 2.0.0 that includes bug fixes (#620) * Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: allow customizing guacd backened url * fix: ssh route mistmatching and guacamole url not changing * chore: increment ver * feat: change default to work with default compose, added splits creen support, updated readmes * fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors * fix: global settings not setting * chore: update compose * feat: improve the global status input * chore: cleanup files * chore: update export/improt with new host fields * fix: file manager and docker not loading properly --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { db } from "../db/index.js";
|
||||
import {
|
||||
sshCredentials,
|
||||
sshCredentialUsage,
|
||||
sshData,
|
||||
hosts,
|
||||
hostAccess,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
@@ -606,9 +606,8 @@ router.put(
|
||||
userId,
|
||||
);
|
||||
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
await sharedCredManager.updateSharedCredentialsForOriginal(
|
||||
parseInt(id),
|
||||
@@ -691,17 +690,14 @@ router.delete(
|
||||
|
||||
const hostsUsingCredential = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(id)),
|
||||
eq(sshData.userId, userId),
|
||||
),
|
||||
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
|
||||
);
|
||||
|
||||
if (hostsUsingCredential.length > 0) {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
credentialId: null,
|
||||
password: null,
|
||||
@@ -710,10 +706,7 @@ router.delete(
|
||||
authType: "password",
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(id)),
|
||||
eq(sshData.userId, userId),
|
||||
),
|
||||
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
|
||||
);
|
||||
|
||||
for (const host of hostsUsingCredential) {
|
||||
@@ -737,9 +730,8 @@ router.delete(
|
||||
}
|
||||
}
|
||||
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id));
|
||||
|
||||
@@ -837,7 +829,7 @@ router.post(
|
||||
const credential = credentials[0];
|
||||
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
credentialId: parseInt(credentialId),
|
||||
username: (credential.username as string) || "",
|
||||
@@ -848,9 +840,7 @@ router.post(
|
||||
keyType: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(
|
||||
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
||||
);
|
||||
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId)));
|
||||
|
||||
await db.insert(sshCredentialUsage).values({
|
||||
credentialId: parseInt(credentialId),
|
||||
@@ -917,17 +907,17 @@ router.get(
|
||||
}
|
||||
|
||||
try {
|
||||
const hosts = await db
|
||||
const hostsUsingCredential = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.credentialId, parseInt(credentialId)),
|
||||
eq(sshData.userId, userId),
|
||||
eq(hosts.credentialId, parseInt(credentialId)),
|
||||
eq(hosts.userId, userId),
|
||||
),
|
||||
);
|
||||
|
||||
res.json(hosts.map((host) => formatSSHHostOutput(host)));
|
||||
res.json(hostsUsingCredential.map((host) => formatSSHHostOutput(host)));
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to fetch hosts using credential", err);
|
||||
res.status(500).json({
|
||||
@@ -1942,7 +1932,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
const targetHost = await SimpleDBOps.select(
|
||||
db.select().from(sshData).where(eq(sshData.id, targetHostId)).limit(1),
|
||||
db.select().from(hosts).where(eq(hosts.id, targetHostId)).limit(1),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import {
|
||||
sshData,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
sshCredentialUsage,
|
||||
fileManagerRecent,
|
||||
@@ -90,6 +90,12 @@ function transformHostResponse(
|
||||
socks5ProxyChain: host.socks5ProxyChain
|
||||
? JSON.parse(host.socks5ProxyChain as string)
|
||||
: [],
|
||||
domain: host.domain || undefined,
|
||||
security: host.security || undefined,
|
||||
ignoreCert: !!host.ignoreCert,
|
||||
guacamoleConfig: host.guacamoleConfig
|
||||
? JSON.parse(host.guacamoleConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,12 +145,9 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const autostartHosts = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.enableTunnel, true),
|
||||
isNotNull(sshData.tunnelConnections),
|
||||
),
|
||||
and(eq(hosts.enableTunnel, true), isNotNull(hosts.tunnelConnections)),
|
||||
);
|
||||
|
||||
const result = autostartHosts
|
||||
@@ -235,7 +238,7 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
|
||||
.json({ error: "Invalid internal authentication token" });
|
||||
}
|
||||
|
||||
const allHosts = await db.select().from(sshData);
|
||||
const allHosts = await db.select().from(hosts);
|
||||
|
||||
const result = allHosts.map((host) => {
|
||||
const tunnelConnections = host.tunnelConnections
|
||||
@@ -334,6 +337,7 @@ router.post(
|
||||
}
|
||||
|
||||
const {
|
||||
connectionType,
|
||||
name,
|
||||
folder,
|
||||
tags,
|
||||
@@ -363,8 +367,13 @@ router.post(
|
||||
jumpHosts,
|
||||
quickActions,
|
||||
statsConfig,
|
||||
dockerConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
guacamoleConfig,
|
||||
notes,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
@@ -397,8 +406,10 @@ router.post(
|
||||
}
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const effectiveConnectionType = connectionType || "ssh";
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
userId: userId,
|
||||
connectionType: effectiveConnectionType,
|
||||
name,
|
||||
folder: folder || null,
|
||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||
@@ -431,12 +442,21 @@ router.post(
|
||||
? statsConfig
|
||||
: JSON.stringify(statsConfig)
|
||||
: null,
|
||||
dockerConfig: dockerConfig
|
||||
? typeof dockerConfig === "string"
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
: JSON.stringify(terminalConfig)
|
||||
: null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
|
||||
notes: notes || null,
|
||||
sudoPassword: sudoPassword || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
@@ -449,7 +469,13 @@ router.post(
|
||||
: null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
if (effectiveConnectionType !== "ssh") {
|
||||
sshDataObj.password = password || null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "password") {
|
||||
sshDataObj.password = password || null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
@@ -501,7 +527,7 @@ router.post(
|
||||
|
||||
try {
|
||||
const result = await SimpleDBOps.insert(
|
||||
sshData,
|
||||
hosts,
|
||||
"ssh_data",
|
||||
sshDataObj,
|
||||
userId,
|
||||
@@ -532,7 +558,7 @@ router.post(
|
||||
|
||||
try {
|
||||
const axios = (await import("axios")).default;
|
||||
const statsPort = process.env.STATS_PORT || 30005;
|
||||
const statsPort = 30005;
|
||||
await axios.post(
|
||||
`http://localhost:${statsPort}/host-updated`,
|
||||
{ hostId: createdHost.id },
|
||||
@@ -818,6 +844,7 @@ router.put(
|
||||
}
|
||||
|
||||
const {
|
||||
connectionType,
|
||||
name,
|
||||
folder,
|
||||
tags,
|
||||
@@ -847,8 +874,13 @@ router.put(
|
||||
jumpHosts,
|
||||
quickActions,
|
||||
statsConfig,
|
||||
dockerConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
guacamoleConfig,
|
||||
notes,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
@@ -884,6 +916,7 @@ router.put(
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
connectionType: connectionType || "ssh",
|
||||
name,
|
||||
folder,
|
||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||
@@ -916,12 +949,21 @@ router.put(
|
||||
? statsConfig
|
||||
: JSON.stringify(statsConfig)
|
||||
: null,
|
||||
dockerConfig: dockerConfig
|
||||
? typeof dockerConfig === "string"
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
: JSON.stringify(terminalConfig)
|
||||
: null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
|
||||
notes: notes || null,
|
||||
sudoPassword: sudoPassword || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
@@ -934,7 +976,15 @@ router.put(
|
||||
: null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
if ((connectionType || "ssh") !== "ssh") {
|
||||
if (password) {
|
||||
sshDataObj.password = password;
|
||||
}
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "password") {
|
||||
if (password) {
|
||||
sshDataObj.password = password;
|
||||
}
|
||||
@@ -1021,12 +1071,12 @@ router.put(
|
||||
|
||||
const hostRecord = await db
|
||||
.select({
|
||||
userId: sshData.userId,
|
||||
credentialId: sshData.credentialId,
|
||||
authType: sshData.authType,
|
||||
userId: hosts.userId,
|
||||
credentialId: hosts.credentialId,
|
||||
authType: hosts.authType,
|
||||
})
|
||||
.from(sshData)
|
||||
.where(eq(sshData.id, Number(hostId)))
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, Number(hostId)))
|
||||
.limit(1);
|
||||
|
||||
if (hostRecord.length === 0) {
|
||||
@@ -1072,9 +1122,9 @@ router.put(
|
||||
}
|
||||
|
||||
await SimpleDBOps.update(
|
||||
sshData,
|
||||
hosts,
|
||||
"ssh_data",
|
||||
eq(sshData.id, Number(hostId)),
|
||||
eq(hosts.id, Number(hostId)),
|
||||
sshDataObj,
|
||||
ownerId,
|
||||
);
|
||||
@@ -1082,8 +1132,8 @@ router.put(
|
||||
const updatedHosts = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.id, Number(hostId))),
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, Number(hostId))),
|
||||
"ssh_data",
|
||||
ownerId,
|
||||
);
|
||||
@@ -1110,7 +1160,7 @@ router.put(
|
||||
|
||||
try {
|
||||
const axios = (await import("axios")).default;
|
||||
const statsPort = process.env.STATS_PORT || 30005;
|
||||
const statsPort = 30005;
|
||||
await axios.post(
|
||||
`http://localhost:${statsPort}/host-updated`,
|
||||
{ hostId: parseInt(hostId) },
|
||||
@@ -1186,62 +1236,67 @@ router.get(
|
||||
|
||||
const rawData = await db
|
||||
.select({
|
||||
id: sshData.id,
|
||||
userId: sshData.userId,
|
||||
name: sshData.name,
|
||||
ip: sshData.ip,
|
||||
port: sshData.port,
|
||||
username: sshData.username,
|
||||
folder: sshData.folder,
|
||||
tags: sshData.tags,
|
||||
pin: sshData.pin,
|
||||
authType: sshData.authType,
|
||||
password: sshData.password,
|
||||
key: sshData.key,
|
||||
keyPassword: sshData.keyPassword,
|
||||
keyType: sshData.keyType,
|
||||
enableTerminal: sshData.enableTerminal,
|
||||
enableTunnel: sshData.enableTunnel,
|
||||
tunnelConnections: sshData.tunnelConnections,
|
||||
jumpHosts: sshData.jumpHosts,
|
||||
enableFileManager: sshData.enableFileManager,
|
||||
defaultPath: sshData.defaultPath,
|
||||
autostartPassword: sshData.autostartPassword,
|
||||
autostartKey: sshData.autostartKey,
|
||||
autostartKeyPassword: sshData.autostartKeyPassword,
|
||||
forceKeyboardInteractive: sshData.forceKeyboardInteractive,
|
||||
statsConfig: sshData.statsConfig,
|
||||
terminalConfig: sshData.terminalConfig,
|
||||
sudoPassword: sshData.sudoPassword,
|
||||
createdAt: sshData.createdAt,
|
||||
updatedAt: sshData.updatedAt,
|
||||
credentialId: sshData.credentialId,
|
||||
overrideCredentialUsername: sshData.overrideCredentialUsername,
|
||||
quickActions: sshData.quickActions,
|
||||
notes: sshData.notes,
|
||||
enableDocker: sshData.enableDocker,
|
||||
showTerminalInSidebar: sshData.showTerminalInSidebar,
|
||||
showFileManagerInSidebar: sshData.showFileManagerInSidebar,
|
||||
showTunnelInSidebar: sshData.showTunnelInSidebar,
|
||||
showDockerInSidebar: sshData.showDockerInSidebar,
|
||||
showServerStatsInSidebar: sshData.showServerStatsInSidebar,
|
||||
useSocks5: sshData.useSocks5,
|
||||
socks5Host: sshData.socks5Host,
|
||||
socks5Port: sshData.socks5Port,
|
||||
socks5Username: sshData.socks5Username,
|
||||
socks5Password: sshData.socks5Password,
|
||||
socks5ProxyChain: sshData.socks5ProxyChain,
|
||||
id: hosts.id,
|
||||
userId: hosts.userId,
|
||||
connectionType: hosts.connectionType,
|
||||
name: hosts.name,
|
||||
ip: hosts.ip,
|
||||
port: hosts.port,
|
||||
username: hosts.username,
|
||||
folder: hosts.folder,
|
||||
tags: hosts.tags,
|
||||
pin: hosts.pin,
|
||||
authType: hosts.authType,
|
||||
password: hosts.password,
|
||||
key: hosts.key,
|
||||
keyPassword: hosts.keyPassword,
|
||||
keyType: hosts.keyType,
|
||||
enableTerminal: hosts.enableTerminal,
|
||||
enableTunnel: hosts.enableTunnel,
|
||||
tunnelConnections: hosts.tunnelConnections,
|
||||
jumpHosts: hosts.jumpHosts,
|
||||
enableFileManager: hosts.enableFileManager,
|
||||
defaultPath: hosts.defaultPath,
|
||||
autostartPassword: hosts.autostartPassword,
|
||||
autostartKey: hosts.autostartKey,
|
||||
autostartKeyPassword: hosts.autostartKeyPassword,
|
||||
forceKeyboardInteractive: hosts.forceKeyboardInteractive,
|
||||
statsConfig: hosts.statsConfig,
|
||||
terminalConfig: hosts.terminalConfig,
|
||||
sudoPassword: hosts.sudoPassword,
|
||||
createdAt: hosts.createdAt,
|
||||
updatedAt: hosts.updatedAt,
|
||||
credentialId: hosts.credentialId,
|
||||
overrideCredentialUsername: hosts.overrideCredentialUsername,
|
||||
quickActions: hosts.quickActions,
|
||||
notes: hosts.notes,
|
||||
enableDocker: hosts.enableDocker,
|
||||
showTerminalInSidebar: hosts.showTerminalInSidebar,
|
||||
showFileManagerInSidebar: hosts.showFileManagerInSidebar,
|
||||
showTunnelInSidebar: hosts.showTunnelInSidebar,
|
||||
showDockerInSidebar: hosts.showDockerInSidebar,
|
||||
showServerStatsInSidebar: hosts.showServerStatsInSidebar,
|
||||
useSocks5: hosts.useSocks5,
|
||||
socks5Host: hosts.socks5Host,
|
||||
socks5Port: hosts.socks5Port,
|
||||
socks5Username: hosts.socks5Username,
|
||||
socks5Password: hosts.socks5Password,
|
||||
socks5ProxyChain: hosts.socks5ProxyChain,
|
||||
domain: hosts.domain,
|
||||
security: hosts.security,
|
||||
ignoreCert: hosts.ignoreCert,
|
||||
guacamoleConfig: hosts.guacamoleConfig,
|
||||
|
||||
ownerId: sshData.userId,
|
||||
isShared: sql<boolean>`${hostAccess.id} IS NOT NULL AND ${sshData.userId} != ${userId}`,
|
||||
ownerId: hosts.userId,
|
||||
isShared: sql<boolean>`${hostAccess.id} IS NOT NULL AND ${hosts.userId} != ${userId}`,
|
||||
permissionLevel: hostAccess.permissionLevel,
|
||||
expiresAt: hostAccess.expiresAt,
|
||||
})
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.leftJoin(
|
||||
hostAccess,
|
||||
and(
|
||||
eq(hostAccess.hostId, sshData.id),
|
||||
eq(hostAccess.hostId, hosts.id),
|
||||
or(
|
||||
eq(hostAccess.userId, userId),
|
||||
roleIds.length > 0
|
||||
@@ -1253,7 +1308,7 @@ router.get(
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
eq(sshData.userId, userId),
|
||||
eq(hosts.userId, userId),
|
||||
and(
|
||||
eq(hostAccess.userId, userId),
|
||||
or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
|
||||
@@ -1361,10 +1416,14 @@ router.get(
|
||||
return res.status(400).json({ error: "Invalid userId or hostId" });
|
||||
}
|
||||
try {
|
||||
const data = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||
const data = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, Number(hostId)), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (data.length === 0) {
|
||||
sshLogger.warn("SSH host not found", {
|
||||
@@ -1429,37 +1488,36 @@ router.get(
|
||||
}
|
||||
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(
|
||||
and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)),
|
||||
),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, Number(hostId)), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return res.status(404).json({ error: "SSH host not found" });
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
const resolvedHost = (await resolveHostCredentials(host, userId)) || host;
|
||||
|
||||
const exportData = {
|
||||
const exportedConnectionType =
|
||||
(resolvedHost.connectionType as string) || "ssh";
|
||||
const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes(
|
||||
exportedConnectionType,
|
||||
);
|
||||
|
||||
const baseExportData = {
|
||||
connectionType: exportedConnectionType,
|
||||
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,
|
||||
credentialId: resolvedHost.credentialId || null,
|
||||
overrideCredentialUsername: !!resolvedHost.overrideCredentialUsername,
|
||||
folder: resolvedHost.folder,
|
||||
tags:
|
||||
typeof resolvedHost.tags === "string"
|
||||
@@ -1467,44 +1525,68 @@ router.get(
|
||||
: resolvedHost.tags || [],
|
||||
pin: !!resolvedHost.pin,
|
||||
notes: resolvedHost.notes || null,
|
||||
enableTerminal: !!resolvedHost.enableTerminal,
|
||||
enableTunnel: !!resolvedHost.enableTunnel,
|
||||
enableFileManager: !!resolvedHost.enableFileManager,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
|
||||
showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
|
||||
showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
|
||||
showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
|
||||
showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
|
||||
defaultPath: resolvedHost.defaultPath,
|
||||
sudoPassword: resolvedHost.sudoPassword || null,
|
||||
tunnelConnections: resolvedHost.tunnelConnections
|
||||
? JSON.parse(resolvedHost.tunnelConnections as string)
|
||||
: [],
|
||||
jumpHosts: resolvedHost.jumpHosts
|
||||
? JSON.parse(resolvedHost.jumpHosts as string)
|
||||
: null,
|
||||
quickActions: resolvedHost.quickActions
|
||||
? JSON.parse(resolvedHost.quickActions as string)
|
||||
: null,
|
||||
statsConfig: resolvedHost.statsConfig
|
||||
? JSON.parse(resolvedHost.statsConfig as string)
|
||||
: null,
|
||||
terminalConfig: resolvedHost.terminalConfig
|
||||
? JSON.parse(resolvedHost.terminalConfig as string)
|
||||
: null,
|
||||
forceKeyboardInteractive:
|
||||
resolvedHost.forceKeyboardInteractive === "true",
|
||||
useSocks5: !!resolvedHost.useSocks5,
|
||||
socks5Host: resolvedHost.socks5Host || null,
|
||||
socks5Port: resolvedHost.socks5Port || null,
|
||||
socks5Username: resolvedHost.socks5Username || null,
|
||||
socks5Password: resolvedHost.socks5Password || null,
|
||||
socks5ProxyChain: resolvedHost.socks5ProxyChain
|
||||
? JSON.parse(resolvedHost.socks5ProxyChain as string)
|
||||
: null,
|
||||
};
|
||||
|
||||
const exportData = isRemoteDesktop
|
||||
? {
|
||||
...baseExportData,
|
||||
domain: resolvedHost.domain || null,
|
||||
security: resolvedHost.security || null,
|
||||
ignoreCert: !!resolvedHost.ignoreCert,
|
||||
guacamoleConfig: resolvedHost.guacamoleConfig
|
||||
? JSON.parse(resolvedHost.guacamoleConfig as string)
|
||||
: null,
|
||||
}
|
||||
: {
|
||||
...baseExportData,
|
||||
authType: resolvedHost.authType,
|
||||
key: resolvedHost.key || null,
|
||||
keyPassword: resolvedHost.keyPassword || null,
|
||||
keyType: resolvedHost.keyType || null,
|
||||
credentialId: resolvedHost.credentialId || null,
|
||||
overrideCredentialUsername:
|
||||
!!resolvedHost.overrideCredentialUsername,
|
||||
enableTerminal: !!resolvedHost.enableTerminal,
|
||||
enableTunnel: !!resolvedHost.enableTunnel,
|
||||
enableFileManager: !!resolvedHost.enableFileManager,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
|
||||
showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
|
||||
showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
|
||||
showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
|
||||
showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
|
||||
defaultPath: resolvedHost.defaultPath,
|
||||
sudoPassword: resolvedHost.sudoPassword || null,
|
||||
tunnelConnections: resolvedHost.tunnelConnections
|
||||
? JSON.parse(resolvedHost.tunnelConnections as string)
|
||||
: [],
|
||||
jumpHosts: resolvedHost.jumpHosts
|
||||
? JSON.parse(resolvedHost.jumpHosts as string)
|
||||
: null,
|
||||
quickActions: resolvedHost.quickActions
|
||||
? JSON.parse(resolvedHost.quickActions as string)
|
||||
: null,
|
||||
statsConfig: resolvedHost.statsConfig
|
||||
? JSON.parse(resolvedHost.statsConfig as string)
|
||||
: null,
|
||||
dockerConfig: resolvedHost.dockerConfig
|
||||
? JSON.parse(resolvedHost.dockerConfig as string)
|
||||
: null,
|
||||
terminalConfig: resolvedHost.terminalConfig
|
||||
? JSON.parse(resolvedHost.terminalConfig as string)
|
||||
: null,
|
||||
forceKeyboardInteractive:
|
||||
resolvedHost.forceKeyboardInteractive === "true",
|
||||
useSocks5: !!resolvedHost.useSocks5,
|
||||
socks5Host: resolvedHost.socks5Host || null,
|
||||
socks5Port: resolvedHost.socks5Port || null,
|
||||
socks5Username: resolvedHost.socks5Username || null,
|
||||
socks5Password: resolvedHost.socks5Password || null,
|
||||
socks5ProxyChain: resolvedHost.socks5ProxyChain
|
||||
? JSON.parse(resolvedHost.socks5ProxyChain as string)
|
||||
: null,
|
||||
};
|
||||
|
||||
sshLogger.success("Host exported with decrypted credentials", {
|
||||
operation: "host_export",
|
||||
hostId: parseInt(hostId),
|
||||
@@ -1573,8 +1655,8 @@ router.delete(
|
||||
try {
|
||||
const hostToDelete = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, Number(hostId)), eq(hosts.userId, userId)));
|
||||
|
||||
if (hostToDelete.length === 0) {
|
||||
sshLogger.warn("SSH host not found for deletion", {
|
||||
@@ -1618,8 +1700,8 @@ router.delete(
|
||||
.where(eq(sessionRecordings.hostId, numericHostId));
|
||||
|
||||
await db
|
||||
.delete(sshData)
|
||||
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
|
||||
.delete(hosts)
|
||||
.where(and(eq(hosts.id, numericHostId), eq(hosts.userId, userId)));
|
||||
|
||||
databaseLogger.success("SSH host deleted", {
|
||||
operation: "host_delete_success",
|
||||
@@ -1629,7 +1711,7 @@ router.delete(
|
||||
|
||||
try {
|
||||
const axios = (await import("axios")).default;
|
||||
const statsPort = process.env.STATS_PORT || 30005;
|
||||
const statsPort = 30005;
|
||||
await axios.post(
|
||||
`http://localhost:${statsPort}/host-deleted`,
|
||||
{ hostId: numericHostId },
|
||||
@@ -2522,9 +2604,9 @@ router.put(
|
||||
|
||||
try {
|
||||
const updatedHosts = await SimpleDBOps.update(
|
||||
sshData,
|
||||
hosts,
|
||||
"ssh_data",
|
||||
and(eq(sshData.userId, userId), eq(sshData.folder, oldName)),
|
||||
and(eq(hosts.userId, userId), eq(hosts.folder, oldName)),
|
||||
{
|
||||
folder: newName,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -2748,8 +2830,8 @@ router.delete(
|
||||
try {
|
||||
const hostsToDelete = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.userId, userId), eq(sshData.folder, folderName)));
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.userId, userId), eq(hosts.folder, folderName)));
|
||||
|
||||
if (hostsToDelete.length === 0) {
|
||||
return res.json({
|
||||
@@ -2793,8 +2875,8 @@ router.delete(
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(sshData)
|
||||
.where(and(eq(sshData.userId, userId), eq(sshData.folder, folderName)));
|
||||
.delete(hosts)
|
||||
.where(and(eq(hosts.userId, userId), eq(hosts.folder, folderName)));
|
||||
|
||||
await db
|
||||
.delete(sshFolders)
|
||||
@@ -2806,7 +2888,7 @@ router.delete(
|
||||
|
||||
try {
|
||||
const axios = (await import("axios")).default;
|
||||
const statsPort = process.env.STATS_PORT || 30005;
|
||||
const statsPort = 30005;
|
||||
for (const host of hostsToDelete) {
|
||||
try {
|
||||
await axios.post(
|
||||
@@ -2935,9 +3017,9 @@ router.patch(
|
||||
|
||||
try {
|
||||
const ownedHosts = await db
|
||||
.select({ id: sshData.id, statsConfig: sshData.statsConfig })
|
||||
.from(sshData)
|
||||
.where(and(inArray(sshData.id, hostIds), eq(sshData.userId, userId)));
|
||||
.select({ id: hosts.id, statsConfig: hosts.statsConfig })
|
||||
.from(hosts)
|
||||
.where(and(inArray(hosts.id, hostIds), eq(hosts.userId, userId)));
|
||||
|
||||
const ownedIds = ownedHosts.map((h) => h.id);
|
||||
const unauthorizedIds = hostIds.filter(
|
||||
@@ -2968,11 +3050,9 @@ router.patch(
|
||||
|
||||
if (Object.keys(simpleUpdates).length > 0) {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set(simpleUpdates)
|
||||
.where(
|
||||
and(inArray(sshData.id, ownedIds), eq(sshData.userId, userId)),
|
||||
);
|
||||
.where(and(inArray(hosts.id, ownedIds), eq(hosts.userId, userId)));
|
||||
}
|
||||
|
||||
if (updates.statsConfig && typeof updates.statsConfig === "object") {
|
||||
@@ -2983,9 +3063,9 @@ router.patch(
|
||||
: {};
|
||||
const merged = { ...existing, ...updates.statsConfig };
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({ statsConfig: JSON.stringify(merged) })
|
||||
.where(and(eq(sshData.id, host.id), eq(sshData.userId, userId)));
|
||||
.where(and(eq(hosts.id, host.id), eq(hosts.userId, userId)));
|
||||
} catch (e) {
|
||||
errors.push(`Failed to update statsConfig for host ${host.id}`);
|
||||
}
|
||||
@@ -3011,15 +3091,15 @@ router.post(
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hosts, overwrite } = req.body;
|
||||
const { hosts: hostsToImport, overwrite } = req.body;
|
||||
|
||||
if (!Array.isArray(hosts) || hosts.length === 0) {
|
||||
if (!Array.isArray(hostsToImport) || hostsToImport.length === 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Hosts array is required and must not be empty" });
|
||||
}
|
||||
|
||||
if (hosts.length > 100) {
|
||||
if (hostsToImport.length > 100) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Maximum 100 hosts allowed per import" });
|
||||
@@ -3037,7 +3117,7 @@ router.post(
|
||||
if (overwrite) {
|
||||
try {
|
||||
const allHosts = await SimpleDBOps.select<Record<string, unknown>>(
|
||||
db.select().from(sshData).where(eq(sshData.userId, userId)),
|
||||
db.select().from(hosts).where(eq(hosts.userId, userId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
@@ -3051,23 +3131,34 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
const hostData = hosts[i];
|
||||
for (let i = 0; i < hostsToImport.length; i++) {
|
||||
const hostData = hostsToImport[i];
|
||||
|
||||
try {
|
||||
if (
|
||||
!isNonEmptyString(hostData.ip) ||
|
||||
!isValidPort(hostData.port) ||
|
||||
!isNonEmptyString(hostData.username)
|
||||
) {
|
||||
const effectiveConnectionType = hostData.connectionType || "ssh";
|
||||
|
||||
if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port)) {
|
||||
results.failed++;
|
||||
results.errors.push(
|
||||
`Host ${i + 1}: Missing required fields (ip, port, username)`,
|
||||
`Host ${i + 1}: Missing required fields (ip, port)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
effectiveConnectionType === "ssh" &&
|
||||
!isNonEmptyString(hostData.username)
|
||||
) {
|
||||
results.failed++;
|
||||
results.errors.push(
|
||||
`Host ${i + 1}: Username required for SSH connections`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
effectiveConnectionType === "ssh" &&
|
||||
hostData.authType &&
|
||||
!["password", "key", "credential", "none", "opkssh"].includes(
|
||||
hostData.authType,
|
||||
)
|
||||
@@ -3080,6 +3171,7 @@ router.post(
|
||||
}
|
||||
|
||||
if (
|
||||
effectiveConnectionType === "ssh" &&
|
||||
hostData.authType === "password" &&
|
||||
!isNonEmptyString(hostData.password)
|
||||
) {
|
||||
@@ -3090,7 +3182,11 @@ router.post(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hostData.authType === "key" && !isNonEmptyString(hostData.key)) {
|
||||
if (
|
||||
effectiveConnectionType === "ssh" &&
|
||||
hostData.authType === "key" &&
|
||||
!isNonEmptyString(hostData.key)
|
||||
) {
|
||||
results.failed++;
|
||||
results.errors.push(
|
||||
`Host ${i + 1}: Key required for key authentication`,
|
||||
@@ -3098,7 +3194,11 @@ router.post(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hostData.authType === "credential" && !hostData.credentialId) {
|
||||
if (
|
||||
effectiveConnectionType === "ssh" &&
|
||||
hostData.authType === "credential" &&
|
||||
!hostData.credentialId
|
||||
) {
|
||||
results.failed++;
|
||||
results.errors.push(
|
||||
`Host ${i + 1}: credentialId required for credential authentication`,
|
||||
@@ -3108,21 +3208,13 @@ router.post(
|
||||
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
userId: userId,
|
||||
name: hostData.name || `${hostData.username}@${hostData.ip}`,
|
||||
connectionType: effectiveConnectionType,
|
||||
name: hostData.name || `${hostData.username || ""}@${hostData.ip}`,
|
||||
folder: hostData.folder || "Default",
|
||||
tags: Array.isArray(hostData.tags) ? hostData.tags.join(",") : "",
|
||||
ip: hostData.ip,
|
||||
port: hostData.port,
|
||||
username: hostData.username,
|
||||
password: hostData.authType === "password" ? hostData.password : null,
|
||||
authType: hostData.authType,
|
||||
credentialId:
|
||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
||||
key: hostData.authType === "key" ? hostData.key : null,
|
||||
keyPassword:
|
||||
hostData.authType === "key" ? hostData.keyPassword || null : null,
|
||||
keyType:
|
||||
hostData.authType === "key" ? hostData.keyType || "auto" : null,
|
||||
username: hostData.username || null,
|
||||
pin: hostData.pin || false,
|
||||
enableTerminal: hostData.enableTerminal !== false,
|
||||
enableTunnel: hostData.enableTunnel !== false,
|
||||
@@ -3147,6 +3239,9 @@ router.post(
|
||||
statsConfig: hostData.statsConfig
|
||||
? JSON.stringify(hostData.statsConfig)
|
||||
: null,
|
||||
dockerConfig: hostData.dockerConfig
|
||||
? JSON.stringify(hostData.dockerConfig)
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig
|
||||
? JSON.stringify(hostData.terminalConfig)
|
||||
: null,
|
||||
@@ -3168,21 +3263,51 @@ router.post(
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (effectiveConnectionType !== "ssh") {
|
||||
sshDataObj.password = hostData.password || null;
|
||||
sshDataObj.authType = "password";
|
||||
sshDataObj.credentialId = null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
sshDataObj.domain = hostData.domain || null;
|
||||
sshDataObj.security = hostData.security || null;
|
||||
sshDataObj.ignoreCert = hostData.ignoreCert ? 1 : 0;
|
||||
sshDataObj.guacamoleConfig = hostData.guacamoleConfig
|
||||
? JSON.stringify(hostData.guacamoleConfig)
|
||||
: null;
|
||||
} else {
|
||||
sshDataObj.password =
|
||||
hostData.authType === "password" ? hostData.password : null;
|
||||
sshDataObj.authType = hostData.authType || "password";
|
||||
sshDataObj.credentialId =
|
||||
hostData.authType === "credential" ? hostData.credentialId : null;
|
||||
sshDataObj.key = hostData.authType === "key" ? hostData.key : null;
|
||||
sshDataObj.keyPassword =
|
||||
hostData.authType === "key" ? hostData.keyPassword || null : null;
|
||||
sshDataObj.keyType =
|
||||
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
||||
sshDataObj.domain = null;
|
||||
sshDataObj.security = null;
|
||||
sshDataObj.ignoreCert = 0;
|
||||
sshDataObj.guacamoleConfig = null;
|
||||
}
|
||||
|
||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||
const existing = existingHostMap?.get(lookupKey);
|
||||
|
||||
if (existing) {
|
||||
await SimpleDBOps.update(
|
||||
sshData,
|
||||
hosts,
|
||||
"ssh_data",
|
||||
eq(sshData.id, existing.id),
|
||||
eq(hosts.id, existing.id),
|
||||
sshDataObj,
|
||||
userId,
|
||||
);
|
||||
results.updated++;
|
||||
} else {
|
||||
sshDataObj.createdAt = new Date().toISOString();
|
||||
await SimpleDBOps.insert(sshData, "ssh_data", sshDataObj, userId);
|
||||
await SimpleDBOps.insert(hosts, "ssh_data", sshDataObj, userId);
|
||||
results.success++;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -3204,6 +3329,104 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /ssh/folders/{folderName}/hosts:
|
||||
* delete:
|
||||
* summary: Delete all hosts in a folder
|
||||
* description: Deletes all hosts within a specific folder.
|
||||
* tags:
|
||||
* - SSH
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: folderName
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: All hosts deleted successfully.
|
||||
* 400:
|
||||
* description: Invalid folder name.
|
||||
* 500:
|
||||
* description: Failed to delete hosts.
|
||||
*/
|
||||
router.delete(
|
||||
"/folders/:folderName/hosts",
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const folderName = decodeURIComponent(
|
||||
Array.isArray(req.params.folderName)
|
||||
? req.params.folderName[0]
|
||||
: req.params.folderName,
|
||||
);
|
||||
|
||||
if (!folderName) {
|
||||
return res.status(400).json({ error: "Folder name is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const hostsToDelete = await db
|
||||
.select({ id: hosts.id })
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.userId, userId), eq(hosts.folder, folderName)));
|
||||
|
||||
if (hostsToDelete.length === 0) {
|
||||
return res.json({ deletedCount: 0 });
|
||||
}
|
||||
|
||||
const hostIds = hostsToDelete.map((h) => h.id);
|
||||
|
||||
for (const hostId of hostIds) {
|
||||
await db
|
||||
.delete(fileManagerRecent)
|
||||
.where(eq(fileManagerRecent.hostId, hostId));
|
||||
await db
|
||||
.delete(fileManagerPinned)
|
||||
.where(eq(fileManagerPinned.hostId, hostId));
|
||||
await db
|
||||
.delete(fileManagerShortcuts)
|
||||
.where(eq(fileManagerShortcuts.hostId, hostId));
|
||||
await db
|
||||
.delete(commandHistory)
|
||||
.where(eq(commandHistory.hostId, hostId));
|
||||
await db
|
||||
.delete(sshCredentialUsage)
|
||||
.where(eq(sshCredentialUsage.hostId, hostId));
|
||||
await db
|
||||
.delete(recentActivity)
|
||||
.where(eq(recentActivity.hostId, hostId));
|
||||
await db.delete(hostAccess).where(eq(hostAccess.hostId, hostId));
|
||||
await db
|
||||
.delete(sessionRecordings)
|
||||
.where(eq(sessionRecordings.hostId, hostId));
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(hosts)
|
||||
.where(and(eq(hosts.userId, userId), eq(hosts.folder, folderName)));
|
||||
|
||||
databaseLogger.success("All hosts in folder deleted", {
|
||||
operation: "delete_folder_hosts",
|
||||
userId,
|
||||
folderName,
|
||||
deletedCount: hostsToDelete.length,
|
||||
});
|
||||
|
||||
res.json({ deletedCount: hostsToDelete.length });
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to delete hosts in folder", error, {
|
||||
operation: "delete_folder_hosts",
|
||||
userId,
|
||||
folderName,
|
||||
});
|
||||
res.status(500).json({ error: "Failed to delete hosts in folder" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /ssh/autostart/enable:
|
||||
@@ -3270,8 +3493,8 @@ router.post(
|
||||
|
||||
const sshConfig = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, sshConfigId), eq(sshData.userId, userId)));
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, sshConfigId), eq(hosts.userId, userId)));
|
||||
|
||||
if (sshConfig.length === 0) {
|
||||
sshLogger.warn("SSH config not found for autostart enable", {
|
||||
@@ -3309,8 +3532,8 @@ router.post(
|
||||
) {
|
||||
const endpointHosts = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.userId, userId));
|
||||
.from(hosts)
|
||||
.where(eq(hosts.userId, userId));
|
||||
|
||||
const endpointHost = endpointHosts.find(
|
||||
(h) =>
|
||||
@@ -3349,14 +3572,14 @@ router.post(
|
||||
}
|
||||
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
autostartPassword: decryptedConfig.password || null,
|
||||
autostartKey: decryptedConfig.key || null,
|
||||
autostartKeyPassword: decryptedConfig.keyPassword || null,
|
||||
tunnelConnections: updatedTunnelConnections,
|
||||
})
|
||||
.where(eq(sshData.id, sshConfigId));
|
||||
.where(eq(hosts.id, sshConfigId));
|
||||
|
||||
try {
|
||||
await DatabaseSaveTrigger.triggerSave();
|
||||
@@ -3429,13 +3652,13 @@ router.delete(
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
autostartPassword: null,
|
||||
autostartKey: null,
|
||||
autostartKeyPassword: null,
|
||||
})
|
||||
.where(and(eq(sshData.id, sshConfigId), eq(sshData.userId, userId)));
|
||||
.where(and(eq(hosts.id, sshConfigId), eq(hosts.userId, userId)));
|
||||
|
||||
res.json({
|
||||
message: "AutoStart disabled successfully",
|
||||
@@ -3475,13 +3698,13 @@ router.get(
|
||||
try {
|
||||
const autostartConfigs = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.userId, userId),
|
||||
eq(hosts.userId, userId),
|
||||
or(
|
||||
isNotNull(sshData.autostartPassword),
|
||||
isNotNull(sshData.autostartKey),
|
||||
isNotNull(hosts.autostartPassword),
|
||||
isNotNull(hosts.autostartKey),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import {
|
||||
hostAccess,
|
||||
sshData,
|
||||
hosts,
|
||||
users,
|
||||
roles,
|
||||
userRoles,
|
||||
@@ -111,8 +111,8 @@ router.post(
|
||||
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -201,9 +201,8 @@ router.post(
|
||||
.delete(sharedCredentials)
|
||||
.where(eq(sharedCredentials.hostAccessId, existing[0].id));
|
||||
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
if (targetType === "user") {
|
||||
await sharedCredManager.createSharedCredentialForUser(
|
||||
@@ -244,9 +243,8 @@ router.post(
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
|
||||
if (targetType === "user") {
|
||||
@@ -336,8 +334,8 @@ router.delete(
|
||||
try {
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -404,8 +402,8 @@ router.get(
|
||||
try {
|
||||
const host = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (host.length === 0) {
|
||||
@@ -484,21 +482,21 @@ router.get(
|
||||
|
||||
const sharedHosts = await db
|
||||
.select({
|
||||
id: sshData.id,
|
||||
name: sshData.name,
|
||||
ip: sshData.ip,
|
||||
port: sshData.port,
|
||||
username: sshData.username,
|
||||
folder: sshData.folder,
|
||||
tags: sshData.tags,
|
||||
id: hosts.id,
|
||||
name: hosts.name,
|
||||
ip: hosts.ip,
|
||||
port: hosts.port,
|
||||
username: hosts.username,
|
||||
folder: hosts.folder,
|
||||
tags: hosts.tags,
|
||||
permissionLevel: hostAccess.permissionLevel,
|
||||
expiresAt: hostAccess.expiresAt,
|
||||
grantedBy: hostAccess.grantedBy,
|
||||
ownerUsername: users.username,
|
||||
})
|
||||
.from(hostAccess)
|
||||
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
|
||||
.innerJoin(users, eq(sshData.userId, users.id))
|
||||
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
|
||||
.innerJoin(users, eq(hosts.userId, users.id))
|
||||
.where(
|
||||
and(
|
||||
eq(hostAccess.userId, userId),
|
||||
@@ -978,12 +976,11 @@ router.post(
|
||||
const hostsSharedWithRole = await db
|
||||
.select()
|
||||
.from(hostAccess)
|
||||
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
|
||||
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
|
||||
.where(eq(hostAccess.roleId, roleId));
|
||||
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
|
||||
for (const { host_access, ssh_data } of hostsSharedWithRole) {
|
||||
|
||||
@@ -632,17 +632,15 @@ router.post(
|
||||
const snippet = snippetResult[0];
|
||||
|
||||
const { Client } = await import("ssh2");
|
||||
const { sshData, sshCredentials } = await import("../db/schema.js");
|
||||
const { hosts, sshCredentials } = await import("../db/schema.js");
|
||||
|
||||
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
|
||||
|
||||
const hostResult = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(
|
||||
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
|
||||
),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { restartGuacServer } from "../../guacamole/guacamole-server.js";
|
||||
import crypto from "crypto";
|
||||
import { db } from "../db/index.js";
|
||||
import {
|
||||
users,
|
||||
sessions,
|
||||
sshData,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
@@ -265,7 +266,7 @@ async function deleteUserAndRelatedData(userId: string): Promise<void> {
|
||||
|
||||
await db.delete(commandHistory).where(eq(commandHistory.userId, userId));
|
||||
|
||||
await db.delete(sshData).where(eq(sshData.userId, userId));
|
||||
await db.delete(hosts).where(eq(hosts.userId, userId));
|
||||
await db.delete(sshCredentials).where(eq(sshCredentials.userId, userId));
|
||||
|
||||
await db.delete(networkTopology).where(eq(networkTopology.userId, userId));
|
||||
@@ -789,6 +790,12 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
|
||||
* description: Returns the OIDC authorization URL.
|
||||
* tags:
|
||||
* - Users
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: rememberMe
|
||||
* schema:
|
||||
* type: boolean
|
||||
* description: Whether to extend the session to 30 days instead of 2 hours.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OIDC authorization URL.
|
||||
@@ -799,29 +806,10 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
|
||||
*/
|
||||
router.get("/oidc/authorize", async (req, res) => {
|
||||
try {
|
||||
const { rememberMe } = req.query;
|
||||
const origin = getRequestOriginWithForceHTTPS(req);
|
||||
const backendCallbackUri = `${origin}/users/oidc/callback`;
|
||||
|
||||
authLogger.info("OIDC authorize request headers", {
|
||||
protocol: req.protocol,
|
||||
host: req.get("Host"),
|
||||
origin: req.get("Origin"),
|
||||
referer: req.get("Referer"),
|
||||
"x-forwarded-proto": req.get("X-Forwarded-Proto"),
|
||||
"x-forwarded-host": req.get("X-Forwarded-Host"),
|
||||
"x-forwarded-port": req.get("X-Forwarded-Port"),
|
||||
secure: req.secure,
|
||||
calculatedOrigin: origin,
|
||||
backendCallbackUri: backendCallbackUri,
|
||||
});
|
||||
|
||||
authLogger.info(
|
||||
`\n${"=".repeat(68)}\n` +
|
||||
` OIDC CALLBACK URL - Register this in your OAuth provider:\n` +
|
||||
` ${backendCallbackUri}\n` +
|
||||
`${"=".repeat(68)}`,
|
||||
);
|
||||
|
||||
const envConfig = getOIDCConfigFromEnv();
|
||||
let config;
|
||||
|
||||
@@ -860,12 +848,12 @@ router.get("/oidc/authorize", async (req, res) => {
|
||||
.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.run(`oidc_frontend_origin_${state}`, frontendOrigin);
|
||||
|
||||
authLogger.info("OIDC authorization initiated", {
|
||||
operation: "oidc_authorize",
|
||||
backendCallbackUri,
|
||||
frontendOrigin,
|
||||
referer,
|
||||
});
|
||||
db.$client
|
||||
.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.run(
|
||||
`oidc_remember_me_${state}`,
|
||||
rememberMe === "true" ? "true" : "false",
|
||||
);
|
||||
|
||||
const authUrl = new URL(config.authorization_url);
|
||||
authUrl.searchParams.set("client_id", config.client_id);
|
||||
@@ -898,10 +886,6 @@ router.get("/oidc/authorize", async (req, res) => {
|
||||
*/
|
||||
router.get("/oidc/callback", async (req, res) => {
|
||||
const { code, state } = req.query;
|
||||
authLogger.info("OIDC login callback received", {
|
||||
operation: "oidc_login_request",
|
||||
state,
|
||||
});
|
||||
|
||||
if (!isNonEmptyString(code) || !isNonEmptyString(state)) {
|
||||
return res.status(400).json({ error: "Code and state are required" });
|
||||
@@ -913,6 +897,9 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const storedFrontendOriginRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = ?")
|
||||
.get(`oidc_frontend_origin_${state}`);
|
||||
const storedRememberMeRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = ?")
|
||||
.get(`oidc_remember_me_${state}`);
|
||||
|
||||
if (!storedBackendCallbackRow || !storedFrontendOriginRow) {
|
||||
return res
|
||||
@@ -925,6 +912,8 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
).value as string;
|
||||
const frontendOrigin = (storedFrontendOriginRow as Record<string, unknown>)
|
||||
.value as string;
|
||||
const storedRememberMe =
|
||||
(storedRememberMeRow as Record<string, unknown> | null)?.value === "true";
|
||||
|
||||
try {
|
||||
const storedNonce = db.$client
|
||||
@@ -951,12 +940,6 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
authLogger.info("OIDC token exchange attempt", {
|
||||
operation: "oidc_token_exchange",
|
||||
backendCallbackUri,
|
||||
frontendOrigin,
|
||||
});
|
||||
|
||||
const tokenResponse = await fetch(config.token_url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -998,6 +981,9 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
db.$client
|
||||
.prepare("DELETE FROM settings WHERE key = ?")
|
||||
.run(`oidc_frontend_origin_${state}`);
|
||||
db.$client
|
||||
.prepare("DELETE FROM settings WHERE key = ?")
|
||||
.run(`oidc_remember_me_${state}`);
|
||||
|
||||
let userInfo: Record<string, unknown> = null;
|
||||
const userInfoUrls: string[] = [];
|
||||
@@ -1320,6 +1306,7 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const token = await authManager.generateJWTToken(userRecord.id, {
|
||||
deviceType: deviceInfo.type,
|
||||
deviceInfo: deviceInfo.deviceInfo,
|
||||
rememberMe: storedRememberMe,
|
||||
});
|
||||
|
||||
authLogger.success("OIDC login successful", {
|
||||
@@ -1334,7 +1321,9 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const maxAge =
|
||||
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
|
||||
? 30 * 24 * 60 * 60 * 1000
|
||||
: 2 * 60 * 60 * 1000;
|
||||
: storedRememberMe
|
||||
? 30 * 24 * 60 * 60 * 1000
|
||||
: 2 * 60 * 60 * 1000;
|
||||
|
||||
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
|
||||
|
||||
@@ -2488,7 +2477,7 @@ router.post("/complete-reset", async (req, res) => {
|
||||
.delete(dismissedAlerts)
|
||||
.where(eq(dismissedAlerts.userId, userId));
|
||||
await db.delete(snippets).where(eq(snippets.userId, userId));
|
||||
await db.delete(sshData).where(eq(sshData.userId, userId));
|
||||
await db.delete(hosts).where(eq(hosts.userId, userId));
|
||||
await db
|
||||
.delete(sshCredentials)
|
||||
.where(eq(sshCredentials.userId, userId));
|
||||
@@ -4126,4 +4115,115 @@ router.post("/unlink-oidc-from-password", authenticateJWT, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /users/guacamole-settings:
|
||||
* get:
|
||||
* summary: Get Guacamole settings
|
||||
* description: Returns current guacd enabled status and host:port URL. No authentication required.
|
||||
* tags:
|
||||
* - Users
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Guacamole settings.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* url:
|
||||
* type: string
|
||||
* 500:
|
||||
* description: Failed to get guacamole settings.
|
||||
*/
|
||||
router.get("/guacamole-settings", async (req, res) => {
|
||||
try {
|
||||
const enabledRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
|
||||
.get() as { value: string } | undefined;
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
res.json({
|
||||
enabled: enabledRow ? enabledRow.value !== "false" : true,
|
||||
url: urlRow ? urlRow.value : "guacd:4822",
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to get guacamole settings", err);
|
||||
res.status(500).json({ error: "Failed to get guacamole settings" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /users/guacamole-settings:
|
||||
* patch:
|
||||
* summary: Update Guacamole settings
|
||||
* description: Admin-only. Updates guacd enabled status and/or host:port URL.
|
||||
* tags:
|
||||
* - Users
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* url:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Guacamole settings updated.
|
||||
* 403:
|
||||
* description: Not authorized.
|
||||
* 500:
|
||||
* description: Failed to update guacamole settings.
|
||||
*/
|
||||
router.patch("/guacamole-settings", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].isAdmin) {
|
||||
return res.status(403).json({ error: "Not authorized" });
|
||||
}
|
||||
const { enabled, url } = req.body;
|
||||
if (typeof enabled === "boolean") {
|
||||
db.$client
|
||||
.prepare(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('guac_enabled', ?)",
|
||||
)
|
||||
.run(enabled ? "true" : "false");
|
||||
}
|
||||
if (typeof url === "string") {
|
||||
db.$client
|
||||
.prepare(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES ('guac_url', ?)",
|
||||
)
|
||||
.run(url);
|
||||
try {
|
||||
await restartGuacServer();
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to restart guac server after URL update", err);
|
||||
}
|
||||
}
|
||||
const enabledRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
|
||||
.get() as { value: string } | undefined;
|
||||
const urlRow = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
|
||||
.get() as { value: string } | undefined;
|
||||
res.json({
|
||||
enabled: enabledRow ? enabledRow.value !== "false" : true,
|
||||
url: urlRow ? urlRow.value : "guacd:4822",
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to update guacamole settings", err);
|
||||
res.status(500).json({ error: "Failed to update guacamole settings" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user