diff --git a/src/backend/database/routes/host-normalizers.ts b/src/backend/database/routes/host-normalizers.ts index eb0ce4b1..18d218ae 100644 --- a/src/backend/database/routes/host-normalizers.ts +++ b/src/backend/database/routes/host-normalizers.ts @@ -14,6 +14,25 @@ export function isOptionalBoolean( return value === undefined || typeof value === "boolean"; } +const PROTOCOL_ENABLE_FIELDS = [ + "enableSsh", + "enableRdp", + "enableVnc", + "enableTelnet", +] as const; + +export function normalizeProtocolEnableFields( + values: Record, +): Partial> { + return Object.fromEntries( + PROTOCOL_ENABLE_FIELDS.flatMap((field) => + typeof values[field] === "boolean" + ? [[field, values[field] ? 1 : 0]] + : [], + ), + ); +} + export const OWNER_PRIVATE_AUTH_FIELDS = { ssh: [ "authType", diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index e9f825d6..3a2237ac 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -37,6 +37,7 @@ import { isNonEmptyString, isOptionalBoolean, isValidPort, + normalizeProtocolEnableFields, OWNER_PRIVATE_AUTH_FIELDS, OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS, sanitizeHostForRecipient, @@ -267,7 +268,8 @@ router.post( !isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port) || - !isOptionalBoolean(shareSshAuth) + !isOptionalBoolean(shareSshAuth) || + ![enableSsh, enableRdp, enableVnc, enableTelnet].every(isOptionalBoolean) ) { sshLogger.warn("Invalid SSH data input validation failed", { operation: "host_create", @@ -398,10 +400,7 @@ router.post( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, - enableSsh: enableSsh ? 1 : 0, - enableRdp: enableRdp ? 1 : 0, - enableVnc: enableVnc ? 1 : 0, - enableTelnet: enableTelnet ? 1 : 0, + ...normalizeProtocolEnableFields(hostData), sshPort: sshPort || port || 22, rdpPort: rdpPort || 3389, vncPort: vncPort || 5900, @@ -971,6 +970,9 @@ router.put( !isNonEmptyString(ip) || !isValidPort(port) || !isOptionalBoolean(shareSshAuth) || + ![enableSsh, enableRdp, enableVnc, enableTelnet].every( + isOptionalBoolean, + ) || !hostId ) { sshLogger.warn("Invalid SSH data input validation failed for update", { @@ -1102,10 +1104,7 @@ router.put( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, - enableSsh: enableSsh ? 1 : 0, - enableRdp: enableRdp ? 1 : 0, - enableVnc: enableVnc ? 1 : 0, - enableTelnet: enableTelnet ? 1 : 0, + ...normalizeProtocolEnableFields(hostData), sshPort: sshPort || port || 22, rdpPort: rdpPort || 3389, vncPort: vncPort || 5900, diff --git a/src/backend/tests/database/routes/host-normalizers.test.ts b/src/backend/tests/database/routes/host-normalizers.test.ts index 50939dbc..d505ed55 100644 --- a/src/backend/tests/database/routes/host-normalizers.test.ts +++ b/src/backend/tests/database/routes/host-normalizers.test.ts @@ -5,6 +5,7 @@ import { isOptionalBoolean, isValidPort, normalizeImportedHost, + normalizeProtocolEnableFields, renameFolderPath, sanitizeHostForRecipient, stripSensitiveFields, @@ -80,6 +81,23 @@ describe("isOptionalBoolean", () => { }); }); +describe("normalizeProtocolEnableFields", () => { + it("omits unspecified protocol fields so database defaults are preserved", () => { + expect(normalizeProtocolEnableFields({ name: "server" })).toEqual({}); + }); + + it("converts explicitly provided protocol booleans to database integers", () => { + expect( + normalizeProtocolEnableFields({ + enableSsh: true, + enableRdp: false, + enableVnc: undefined, + enableTelnet: true, + }), + ).toEqual({ enableSsh: 1, enableRdp: 0, enableTelnet: 1 }); + }); +}); + describe("renameFolderPath", () => { it("renames an exact folder match", () => { expect(renameFolderPath("Production", "Production", "Prod")).toBe("Prod");