diff --git a/server/better-auth.ts b/server/better-auth.ts index 141543a..7894c54 100644 --- a/server/better-auth.ts +++ b/server/better-auth.ts @@ -259,16 +259,16 @@ export async function getDisableAuthSession(): ReturnType { createdAt: obj.createdAt as Date, updatedAt: obj.updatedAt as Date, email: obj.email as string, - emailVerified: obj.emailVerified === 1, + emailVerified: Boolean(obj.emailVerified), name: obj.name as string, image: obj.image as string | null, username: obj.username as string | null, displayUsername: obj.displayUsername as string | null, - banned: obj.banned === 1, + banned: Boolean(obj.banned), role: obj.role as string | null, banReason: obj.banReason as string | null, banExpires: obj.banExpires as Date | null, - twoFactorEnabled: obj.twoFactorEnabled === 1, + twoFactorEnabled: Boolean(obj.twoFactorEnabled), }; return { diff --git a/server/client.js b/server/client.js index 5e7596c..bbe9dab 100644 --- a/server/client.js +++ b/server/client.js @@ -23,8 +23,8 @@ async function sendNotificationList(socket) { for (let bean of list) { let notificationObject = bean.export(); - notificationObject.isDefault = notificationObject.isDefault === 1; - notificationObject.active = notificationObject.active === 1; + notificationObject.isDefault = Boolean(notificationObject.isDefault); + notificationObject.active = Boolean(notificationObject.active); result.push(notificationObject); } diff --git a/server/database.js b/server/database.js index 6e0de10..616b3b3 100644 --- a/server/database.js +++ b/server/database.js @@ -503,6 +503,7 @@ class Database { CREATE OR REPLACE FUNCTION int_eq_bool(integer, boolean) RETURNS boolean STRICT IMMUTABLE LANGUAGE SQL AS $$ SELECT ($1 != 0) = $2; $$; CREATE OR REPLACE FUNCTION bool_ne_int(boolean, integer) RETURNS boolean STRICT IMMUTABLE LANGUAGE SQL AS $$ SELECT $1 != ($2 != 0); $$; CREATE OR REPLACE FUNCTION int_ne_bool(integer, boolean) RETURNS boolean STRICT IMMUTABLE LANGUAGE SQL AS $$ SELECT ($1 != 0) != $2; $$; + CREATE OR REPLACE FUNCTION int_to_bool(integer) RETURNS boolean STRICT IMMUTABLE LANGUAGE SQL AS $$ SELECT ($1 != 0); $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_operator WHERE oprname = '=' AND oprleft = 'boolean'::regtype AND oprright = 'integer'::regtype) THEN CREATE OPERATOR = (LEFTARG = boolean, RIGHTARG = integer, PROCEDURE = bool_eq_int, COMMUTATOR = =); @@ -516,6 +517,13 @@ class Database { IF NOT EXISTS (SELECT 1 FROM pg_operator WHERE oprname = '<>' AND oprleft = 'integer'::regtype AND oprright = 'boolean'::regtype) THEN CREATE OPERATOR <> (LEFTARG = integer, RIGHTARG = boolean, PROCEDURE = int_ne_bool, COMMUTATOR = <>); END IF; + IF NOT EXISTS (SELECT 1 FROM pg_cast WHERE castsource = 'integer'::regtype AND casttarget = 'boolean'::regtype) THEN + BEGIN + CREATE CAST (integer AS boolean) WITH FUNCTION int_to_bool(integer) AS IMPLICIT; + EXCEPTION WHEN OTHERS THEN + NULL; + END; + END IF; END $$; `); } catch (e) { diff --git a/server/model/monitor.js b/server/model/monitor.js index 0850890..f1a0b9c 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1307,7 +1307,7 @@ class Monitor extends BeanModel { static async isActive(monitorID, active) { const parentActive = await Monitor.isParentActive(monitorID); - return active === 1 && parentActive; + return Boolean(active) && parentActive; } /** @@ -2038,7 +2038,7 @@ class Monitor extends BeanModel { } const parentActive = await Monitor.isParentActive(parent.id); - return parent.active === 1 && parentActive; + return Boolean(parent.active) && parentActive; } /** diff --git a/server/proxy.js b/server/proxy.js index 92b8ddd..e07da3c 100644 --- a/server/proxy.js +++ b/server/proxy.js @@ -39,7 +39,7 @@ class Proxy { // When proxy is default update deactivate old default proxy if (proxy.default) { - await R.exec("UPDATE proxy SET `default` = 0 WHERE `default` = 1"); + await R.exec("UPDATE proxy SET `default` = FALSE WHERE `default` = TRUE"); } bean.user_id = userID; diff --git a/server/server.js b/server/server.js index 2babd90..53bac07 100644 --- a/server/server.js +++ b/server/server.js @@ -1290,7 +1290,7 @@ app.use(function (req, res, next) { log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`); - await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", ["", "0", monitorID]); + await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", ["", false, monitorID]); callback({ ok: true, @@ -1496,7 +1496,7 @@ async function initDatabase(testMode = false) { async function startMonitor(userID, monitorID) { log.info("manage", `Resume Monitor: ${monitorID} User ID: ${userID}`); - await R.exec("UPDATE monitor SET active = 1 WHERE id = ? ", [monitorID]); + await R.exec("UPDATE monitor SET active = TRUE WHERE id = ? ", [monitorID]); let monitor = await R.findOne("monitor", " id = ? ", [monitorID]); @@ -1527,7 +1527,7 @@ async function restartMonitor(userID, monitorID) { async function pauseMonitor(userID, monitorID) { log.info("manage", `Pause Monitor: ${monitorID} User ID: ${userID}`); - await R.exec("UPDATE monitor SET active = 0 WHERE id = ? ", [monitorID]); + await R.exec("UPDATE monitor SET active = FALSE WHERE id = ? ", [monitorID]); if (monitorID in server.monitorList) { await server.monitorList[monitorID].stop(); diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js index 99d091d..003af7a 100644 --- a/server/socket-handlers/api-key-socket-handler.js +++ b/server/socket-handlers/api-key-socket-handler.js @@ -98,7 +98,7 @@ module.exports.apiKeySocketHandler = (socket) => { log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`); - await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [keyID]); + await R.exec("UPDATE api_key SET active = FALSE WHERE id = ? ", [keyID]); apicache.clear(); @@ -123,7 +123,7 @@ module.exports.apiKeySocketHandler = (socket) => { log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`); - await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [keyID]); + await R.exec("UPDATE api_key SET active = TRUE WHERE id = ? ", [keyID]); apicache.clear(); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 9a1ce2b..c585a72 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -87,7 +87,7 @@ module.exports.statusPageSocketHandler = (socket) => { let statusPageID = await StatusPage.slugToID(slug); - await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [statusPageID]); + await R.exec("UPDATE incident SET pin = FALSE WHERE pin = TRUE AND status_page_id = ? ", [statusPageID]); callback({ ok: true, diff --git a/server/utils/patch-redbean-postgres.js b/server/utils/patch-redbean-postgres.js index 97c1455..a3092d4 100644 --- a/server/utils/patch-redbean-postgres.js +++ b/server/utils/patch-redbean-postgres.js @@ -1,5 +1,38 @@ const { R } = require("redbean-node"); +const PG_BOOLEAN_COLUMNS = [ + "active", + "pin", + "default", + "important", + "public", + "ignore_tls", + "upside_down", + "expiry_notification", + "domain_expiry_notification", + "save_response", + "save_error_response", + "published", + "twofa_status", + "is_default", + "invert_keyword", + "kafka_producer_ssl", + "kafka_producer_allow_auto_topic_creation", + "show_certificate_expiry", + "gamedig_given_port_only", + "ws_ignore_sec_websocket_accept_header", + "ping_numeric", + "show_only_last_heartbeat", + "retry_only_on_status_code_failure", + "cache_bust", + "email_verified", + "banned", + "two_factor_enabled", + "verified", + "enabled", + "rate_limit_enabled", +]; + function patchRedBeanPostgres() { const RedBeanNode = R.constructor; if (RedBeanNode.prototype._pgPatched) { @@ -9,11 +42,46 @@ function patchRedBeanPostgres() { function cleanPgSql(sql) { if (typeof sql === "string") { - return sql.replace(/`([^`]+)`/g, '"$1"'); + // Replace MySQL backticks with standard quotes + sql = sql.replace(/`([^`]+)`/g, '"$1"'); + // Replace bracket identifier escaping [col] with "col" + sql = sql.replace(/\[([^\]]+)\]/g, '"$1"'); + + // PostgreSQL is strictly typed for booleans: convert integer assignments and comparisons + const boolPattern = PG_BOOLEAN_COLUMNS.join("|"); + sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*=\\s*1\\b`, "gi"), "$1 = true"); + sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*=\\s*0\\b`, "gi"), "$1 = false"); + sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*(?:!=|<>)\\s*1\\b`, "gi"), "$1 != true"); + sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*(?:!=|<>)\\s*0\\b`, "gi"), "$1 != false"); + sql = sql.replace(new RegExp(`\\b1\\s*=\\s*(["\`]?\\b(?:${boolPattern})\\b["\`]?)`, "gi"), "true = $1"); + sql = sql.replace(new RegExp(`\\b0\\s*=\\s*(["\`]?\\b(?:${boolPattern})\\b["\`]?)`, "gi"), "false = $1"); } return sql; } + function sanitizePgBeanBooleans(bean) { + if (!bean) { + return; + } + for (const col of PG_BOOLEAN_COLUMNS) { + if (bean[col] === 0 || bean[col] === "0") { + bean[col] = false; + } else if (bean[col] === 1 || bean[col] === "1") { + bean[col] = true; + } + } + } + + const origExec = RedBeanNode.prototype.exec; + if (origExec) { + RedBeanNode.prototype.exec = function (sql, data) { + if (this.dbType === "pg") { + sql = cleanPgSql(sql); + } + return origExec.call(this, sql, data); + }; + } + const origNormalizeRawCore = RedBeanNode.prototype.normalizeRawCore; RedBeanNode.prototype.normalizeRawCore = function (sql, data) { if (this.dbType === "pg") { @@ -59,6 +127,36 @@ function patchRedBeanPostgres() { } }; + const origGetAll = RedBeanNode.prototype.getAll; + if (origGetAll) { + RedBeanNode.prototype.getAll = function (sql, data) { + if (this.dbType === "pg") { + sql = cleanPgSql(sql); + } + return origGetAll.call(this, sql, data); + }; + } + + const origGetCell = RedBeanNode.prototype.getCell; + if (origGetCell) { + RedBeanNode.prototype.getCell = function (sql, data) { + if (this.dbType === "pg") { + sql = cleanPgSql(sql); + } + return origGetCell.call(this, sql, data); + }; + } + + const origGetCol = RedBeanNode.prototype.getCol; + if (origGetCol) { + RedBeanNode.prototype.getCol = function (sql, data) { + if (this.dbType === "pg") { + sql = cleanPgSql(sql); + } + return origGetCol.call(this, sql, data); + }; + } + const origFindCore = RedBeanNode.prototype.findCore; RedBeanNode.prototype.findCore = function (type, clause, data = []) { if (this.dbType === "pg") { @@ -95,6 +193,9 @@ function patchRedBeanPostgres() { const origStoreCore = RedBeanNode.prototype.storeCore; RedBeanNode.prototype.storeCore = async function (bean, changedFieldsOnly = true) { + if (this.dbType === "pg") { + sanitizePgBeanBooleans(bean); + } if (this.dbType === "pg" && !bean.id) { this.devLog("Store PG insert", bean.beanMeta.type); await bean.storeTypeBeanList(); @@ -103,6 +204,13 @@ function patchRedBeanPostgres() { } let obj = bean.export(false); delete obj.id; + for (const col of PG_BOOLEAN_COLUMNS) { + if (obj[col] === 0 || obj[col] === "0") { + obj[col] = false; + } else if (obj[col] === 1 || obj[col] === "1") { + obj[col] = true; + } + } let queryPromise = this.knex(bean.getType()).insert(obj).returning("id"); this.queryLog(queryPromise); let result = await queryPromise;