From 8c64221f88654509e74d4baa8831f3f4cedf432d Mon Sep 17 00:00:00 2001 From: Cahit Atilgan Date: Tue, 13 May 2025 16:21:30 +0200 Subject: [PATCH 01/97] fix: configuring gmail smtp relay without any authentication --- server/service/emailService.js | 12 +++++++++++- server/service/settingsService.js | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/service/emailService.js b/server/service/emailService.js index 94d19f0f4..03e9cdebe 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -96,9 +96,10 @@ class EmailService { systemEmailUser, systemEmailAddress, systemEmailPassword, + systemEmailConnectionHost } = await this.settingsService.getDBSettings(); - const emailConfig = { + const baseEmailConfig = { host: systemEmailHost, port: systemEmailPort, secure: true, @@ -109,6 +110,15 @@ class EmailService { connectionTimeout: 5000, }; + const emailConfig = systemEmailPort === "465" ? baseEmailConfig : { + ...baseEmailConfig, + name: systemEmailConnectionHost, + secure: false, + pool: true, + tls: { rejectUnauthorized: false }, + auth: null + }; + this.transporter = this.nodemailer.createTransport(emailConfig); const buildHtml = async (template, context) => { diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 290e645e9..bde361f80 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -7,6 +7,7 @@ const envConfig = { systemEmailUser: process.env.SYSTEM_EMAIL_USER, systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS, systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD, + systemEmailConnectionHost: process.env.SYSTEM_EMAIL_CONNECTION_HOST, jwtSecret: process.env.JWT_SECRET, jwtTTL: process.env.TOKEN_TTL, clientHost: process.env.CLIENT_HOST, From 12925c515aceffce215258605e5ca52303b1cbde Mon Sep 17 00:00:00 2001 From: Cahit Atilgan Date: Tue, 13 May 2025 18:07:53 +0200 Subject: [PATCH 02/97] fix: configuring gmail smtp relay without any authentication --- server/service/emailService.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/service/emailService.js b/server/service/emailService.js index 03e9cdebe..bdd727813 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -110,14 +110,19 @@ class EmailService { connectionTimeout: 5000, }; - const emailConfig = systemEmailPort === "465" ? baseEmailConfig : { + const isSmtps = Number(systemEmailPort) === 465; + + const emailConfig = !isSmtps ? { ...baseEmailConfig, name: systemEmailConnectionHost, secure: false, pool: true, tls: { rejectUnauthorized: false }, - auth: null - }; + } : baseEmailConfig; + + if (!isSmtps) { + delete emailConfig.auth; + } this.transporter = this.nodemailer.createTransport(emailConfig); From 3c48eacc9894a73cf80abcf692e5cd7a74de191a Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 15 May 2025 02:38:26 +0530 Subject: [PATCH 03/97] Fix: Port Validation Uptime --- client/src/Validation/validation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index be6cb19f7..096db1a1a 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -171,7 +171,7 @@ const monitorValidation = joi.object({ .max(65535) .when("type", { is: "port", - then: joi.required().messages({ + then: joi.number().messages({ "number.base": "Port must be a number.", "number.min": "Port must be at least 1.", "number.max": "Port must be at most 65535.", From edad05b63b6b1b19744ff184bb61b6f55a2fb04e Mon Sep 17 00:00:00 2001 From: Anurag123Chauhan Date: Thu, 15 May 2025 10:48:04 +0530 Subject: [PATCH 04/97] Changed error msg of invalid of Docker container ID --- client/src/Validation/validation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 096db1a1a..8ed37492b 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -161,7 +161,7 @@ const monitorValidation = joi.object({ .messages({ "string.empty": "This field is required.", "string.uri": "The URL you provided is not valid.", - "string.invalidUrl": "Please enter a valid URL with optional port", + "string.invalidUrl": "Please enter a valid 64-character Docker container ID", "string.pattern.base": "Please enter a valid container ID.", }), port: joi From 0f91d3b6e101d7b1a9940dc6a2f7dc96fe227dbf Mon Sep 17 00:00:00 2001 From: Anurag123Chauhan Date: Thu, 15 May 2025 11:03:05 +0530 Subject: [PATCH 05/97] fix: separate Docker ID and URL validation error messages --- client/src/Validation/validation.js | 109 +++++++++++++++------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 8ed37492b..847226a1f 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -111,59 +111,64 @@ const monitorValidation = joi.object({ .trim() .regex(/^[a-z0-9]{64}$/), otherwise: joi - .string() - .trim() - .custom((value, helpers) => { - // Regex from https://gist.github.com/dperini/729294 - var urlRegex = new RegExp( - "^" + - // protocol identifier (optional) - // short syntax // still required - "(?:(?:https?|ftp):\\/\\/)?" + - // user:pass BasicAuth (optional) - "(?:" + - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broadcast addresses - // (first & last IP address of each class) - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + - "|" + - // host & domain names, may end with dot - // can be replaced by a shortest alternative - // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ - "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + - ")+" + - // TLD identifier name, may end with dot - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + - ")" + - // port number (optional) - "(?::\\d{2,5})?" + - // resource path (optional) - "(?:[/?#]\\S*)?" + - "$", - "i" - ); - if (!urlRegex.test(value)) { - return helpers.error("string.invalidUrl"); - } + .string() + .trim() + .custom((value, helpers) => { + // Get the current validation context + const { type } = helpers.state.ancestors[0]; + + // Regex from https://gist.github.com/dperini/729294 + var urlRegex = new RegExp( + "^" + + // protocol identifier (optional) + // short syntax // still required + "(?:(?:https?|ftp):\\/\\/)?" + + // user:pass BasicAuth (optional) + "(?:" + + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broadcast addresses + // (first & last IP address of each class) + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + // host & domain names, may end with dot + // can be replaced by a shortest alternative + // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ + "(?:" + + "(?:" + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + + ")+" + + // TLD identifier name, may end with dot + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + + ")" + + // port number (optional) + "(?::\\d{2,5})?" + + // resource path (optional) + "(?:[/?#]\\S*)?" + + "$", + "i" + ); + if (!urlRegex.test(value)) { + // Use different error key based on monitor type + return helpers.error(type === "docker" ? "string.invalidDockerID" : "string.invalidUrl"); + } - return value; - }), - }) - .messages({ - "string.empty": "This field is required.", - "string.uri": "The URL you provided is not valid.", - "string.invalidUrl": "Please enter a valid 64-character Docker container ID", - "string.pattern.base": "Please enter a valid container ID.", - }), + return value; + }), + }) + .messages({ + "string.empty": "This field is required.", + "string.uri": "The URL you provided is not valid.", + "string.invalidUrl": "Please enter a valid URL.", + "string.invalidDockerID": "Please enter a valid 64-character Docker container ID.", + "string.pattern.base": "Please enter a valid container ID.", + }), port: joi .number() .integer() From 18b439bad6eb585327f0ec4a5586507f64d97ea8 Mon Sep 17 00:00:00 2001 From: Anurag123Chauhan Date: Thu, 15 May 2025 11:05:45 +0530 Subject: [PATCH 06/97] added msg --- client/src/Validation/validation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 847226a1f..7cba122e0 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -165,7 +165,7 @@ const monitorValidation = joi.object({ .messages({ "string.empty": "This field is required.", "string.uri": "The URL you provided is not valid.", - "string.invalidUrl": "Please enter a valid URL.", + "string.invalidUrl": "Please enter a valid URL with optional port", "string.invalidDockerID": "Please enter a valid 64-character Docker container ID.", "string.pattern.base": "Please enter a valid container ID.", }), From 9da9ef693960b9b329e0be4e2ecddce0fb62677a Mon Sep 17 00:00:00 2001 From: Anurag123Chauhan Date: Thu, 15 May 2025 11:16:02 +0530 Subject: [PATCH 07/97] recommended changes --- client/src/Validation/validation.js | 661 ++++++++++++++-------------- 1 file changed, 330 insertions(+), 331 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 7cba122e0..8a14738e2 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -4,119 +4,120 @@ import dayjs from "dayjs"; const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number."; const nameSchema = joi - .string() - .max(50) - .trim() - .pattern(/^[\p{L}\p{M}''\- ]+$/u) - .messages({ - "string.empty": "Name is required", - "string.max": "Name must be less than 50 characters", - "string.pattern.base": - "Name must contain only letters, spaces, apostrophes, or hyphens", - }); + .string() + .max(50) + .trim() + .pattern(/^[\p{L}\p{M}''\- ]+$/u) + .messages({ + "string.empty": "Name is required", + "string.max": "Name must be less than 50 characters", + "string.pattern.base": + "Name must contain only letters, spaces, apostrophes, or hyphens", + }); const lastnameSchema = joi - .string() - .max(50) - .trim() - .pattern(/^[\p{L}\p{M}''\- ]+$/u) - .messages({ - "string.empty": "Surname is required", - "string.max": "Surname must be less than 50 characters", - "string.pattern.base": "Surname must contain only letters, spaces, apostrophes, or hyphens" - }); + .string() + .max(50) + .trim() + .pattern(/^[\p{L}\p{M}''\- ]+$/u) + .messages({ + "string.empty": "Surname is required", + "string.max": "Surname must be less than 50 characters", + "string.pattern.base": "Surname must contain only letters, spaces, apostrophes, or hyphens" + }); const passwordSchema = joi - .string() - .trim() - .min(8) - .custom((value, helpers) => { - if (!/[A-Z]/.test(value)) { - return helpers.error("uppercase"); - } - return value; - }) - .custom((value, helpers) => { - if (!/[a-z]/.test(value)) { - return helpers.error("lowercase"); - } - return value; - }) - .custom((value, helpers) => { - if (!/\d/.test(value)) { - return helpers.error("number"); - } - return value; - }) - .custom((value, helpers) => { - if (!/[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/.test(value)) { - return helpers.error("special"); - } - return value; - }) - .messages({ - "string.empty": "Password is required", - "string.min": "Password must be at least 8 characters long", - uppercase: "Password must contain at least one uppercase letter", - lowercase: "Password must contain at least one lowercase letter", - number: "Password must contain at least one number", - special: "Password must contain at least one special character", - }); + .string() + .trim() + .min(8) + .custom((value, helpers) => { + if (!/[A-Z]/.test(value)) { + return helpers.error("uppercase"); + } + return value; + }) + .custom((value, helpers) => { + if (!/[a-z]/.test(value)) { + return helpers.error("lowercase"); + } + return value; + }) + .custom((value, helpers) => { + if (!/\d/.test(value)) { + return helpers.error("number"); + } + return value; + }) + .custom((value, helpers) => { + if (!/[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/.test(value)) { + return helpers.error("special"); + } + return value; + }) + .messages({ + "string.empty": "Password is required", + "string.min": "Password must be at least 8 characters long", + uppercase: "Password must contain at least one uppercase letter", + lowercase: "Password must contain at least one lowercase letter", + number: "Password must contain at least one number", + special: "Password must contain at least one special character", + }); const credentials = joi.object({ - firstName: nameSchema, - lastName: lastnameSchema, - email: joi - .string() - .trim() - .email({ tlds: { allow: false } }) - .custom((value, helpers) => { - const lowercasedValue = value.toLowerCase(); - if (value !== lowercasedValue) { - return helpers.message("Email must be in lowercase"); - } - return lowercasedValue; - }) - .messages({ - "string.empty": "authRegisterEmailRequired", - "string.email": "authRegisterEmailInvalid", - }), - password: passwordSchema, - newPassword: passwordSchema, - confirm: joi - .string() - .trim() - .custom((value, helpers) => { - const { password } = helpers.prefs.context; - if (value !== password) { - return helpers.error("different"); - } - return value; - }) - .messages({ - "string.empty": "This field can't be empty", - different: "Passwords do not match", - }), - role: joi.array(), - teamId: joi.string().allow("").optional(), - inviteToken: joi.string().allow(""), + firstName: nameSchema, + lastName: lastnameSchema, + email: joi + .string() + .trim() + .email({ tlds: { allow: false } }) + .custom((value, helpers) => { + const lowercasedValue = value.toLowerCase(); + if (value !== lowercasedValue) { + return helpers.message("Email must be in lowercase"); + } + return lowercasedValue; + }) + .messages({ + "string.empty": "authRegisterEmailRequired", + "string.email": "authRegisterEmailInvalid", + }), + password: passwordSchema, + newPassword: passwordSchema, + confirm: joi + .string() + .trim() + .custom((value, helpers) => { + const { password } = helpers.prefs.context; + if (value !== password) { + return helpers.error("different"); + } + return value; + }) + .messages({ + "string.empty": "This field can't be empty", + different: "Passwords do not match", + }), + role: joi.array(), + teamId: joi.string().allow("").optional(), + inviteToken: joi.string().allow(""), }); const monitorValidation = joi.object({ - url: joi - .when("type", { - is: "docker", - then: joi - .string() - .trim() - .regex(/^[a-z0-9]{64}$/), - otherwise: joi + url: joi + .when("type", { + is: "docker", + then: joi + .string() + .trim() + .regex(/^[a-z0-9]{64}$/) + .messages({ + "string.empty": "This field is required.", + "string.pattern.base": "Please enter a valid 64-character Docker container ID.", + }), + otherwise: joi .string() .trim() .custom((value, helpers) => { - // Get the current validation context - const { type } = helpers.state.ancestors[0]; - // Regex from https://gist.github.com/dperini/729294 var urlRegex = new RegExp( "^" + @@ -155,253 +156,251 @@ const monitorValidation = joi.object({ "i" ); if (!urlRegex.test(value)) { - // Use different error key based on monitor type - return helpers.error(type === "docker" ? "string.invalidDockerID" : "string.invalidUrl"); + return helpers.error("string.invalidUrl"); } return value; + }) + .messages({ + "string.empty": "This field is required.", + "string.uri": "The URL you provided is not valid.", + "string.invalidUrl": "Please enter a valid URL with optional port", }), + }), + port: joi + .number() + .integer() + .min(1) + .max(65535) + .when("type", { + is: "port", + then: joi.number().messages({ + "number.base": "Port must be a number.", + "number.min": "Port must be at least 1.", + "number.max": "Port must be at most 65535.", + "any.required": "Port is required for port monitors.", + }), + otherwise: joi.optional(), + }), + name: joi.string().trim().max(50).allow("").messages({ + "string.max": "This field should not exceed the 50 characters limit.", + }), + type: joi.string().trim().messages({ "string.empty": "This field is required." }), + ignoreTlsErrors: joi.boolean(), + interval: joi.number().messages({ + "number.base": "Frequency must be a number.", + "any.required": "Frequency is required.", + }), + expectedValue: joi.string().allow(""), + jsonPath: joi.string().allow(""), + matchMethod: joi.string(), +}); + +const imageValidation = joi.object({ + type: joi.string().valid("image/jpeg", "image/png").messages({ + "any.only": "Invalid file format.", + "string.empty": "File type required.", + }), + size: joi + .number() + .max(3 * 1024 * 1024) + .messages({ + "number.base": "File size must be a number.", + "number.max": "File size must be less than 3 MB.", + "number.empty": "File size required.", + }), +}); + +const logoImageValidation = joi + .object({ + src: joi.string(), + name: joi.string(), + type: joi + .string() + .valid("image/jpeg", "image/png") + .allow(null) // Allow null and empty string + .messages({ + "any.only": "Invalid file format.", + "string.empty": "File type required.", + }) + .optional(), + size: joi + .number() + .max(3000000) + .allow(null) // Allow null and empty string + .messages({ + "number.base": "File size must be a number.", + "number.max": "File size must be less than 3MB.", + "number.empty": "File size required.", + }) + .optional(), + }) + .allow(null) + .optional(); // Make entire object optional + +const statusPageValidation = joi.object({ + type: joi.string().valid("uptime", "distributed").required(), + isPublished: joi.bool(), + companyName: joi + .string() + .trim() + .messages({ "string.empty": "Company name is required." }), + url: joi + .string() + .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen + .required() + .messages({ + "string.pattern.base": + "URL can only contain letters, numbers, underscores, and hyphens", + }), + timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }), + color: joi.string().trim().messages({ "string.empty": "Color is required." }), + theme: joi.string(), + monitors: joi.array().min(1).required().messages({ + "string.pattern.base": "Must be a valid monitor ID", + "array.base": "Monitors must be an array", + "array.min": "At least one monitor is required", + "array.empty": "At least one monitor is required", + "any.required": "At least one monitor is required", + }), + subMonitors: joi.array().optional(), + logo: logoImageValidation, + showUptimePercentage: joi.boolean(), + showCharts: joi.boolean(), +}); + +const settingsValidation = joi.object({ + checkTTL: joi.number().required().messages({ + "string.empty": "Please enter a value", + "number.base": "Please enter a valid number", + "any.required": "Please enter a value", + }), + pagespeedApiKey: joi.string().allow("").optional(), + language: joi.string().required(), + systemEmailHost: joi.string().allow(""), + systemEmailPort: joi.number().allow(null, ""), + systemEmailAddress: joi.string().allow(""), + systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), +}); + +const dayjsValidator = (value, helpers) => { + if (!dayjs(value).isValid()) { + return helpers.error("any.invalid"); + } + return value; +}; + +const maintenanceWindowValidation = joi.object({ + repeat: joi.string(), + startDate: joi.custom(dayjsValidator, "Day.js date validation"), + startTime: joi.custom(dayjsValidator, "Day.js date validation"), + duration: joi.number().integer().min(0), + durationUnit: joi.string(), + name: joi.string(), + monitors: joi.array().min(1), +}); + +const advancedSettingsValidation = joi.object({ + apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({ + "string.empty": "API base url is required.", + "string.uri": "The URL you provided is not valid.", + }), + logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""), + systemEmailHost: joi.string().allow(""), + systemEmailPort: joi.number().allow(null, ""), + systemEmailAddress: joi.string().allow(""), + systemEmailPassword: joi.string().allow(""), + jwtTTLNum: joi.number().messages({ + "number.base": "JWT TTL is required.", + }), + jwtTTLUnits: joi + .string() + .trim() + .custom((value, helpers) => { + if (!["days", "hours"].includes(value)) { + return helpers.message("JWT TTL unit is required."); + } + return value; + }), + dbType: joi.string().trim().messages({ + "string.empty": "DB type is required.", + }), + redisHost: joi.string().trim().messages({ + "string.empty": "Redis host is required.", + }), + redisPort: joi.number().allow(null, ""), + pagespeedApiKey: joi.string().allow(""), +}); + +const infrastructureMonitorValidation = joi.object({ + url: joi + .string() + .trim() + .custom((value, helpers) => { + const urlRegex = + /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i; + + if (!urlRegex.test(value)) { + return helpers.error("string.invalidUrl"); + } + + return value; }) .messages({ "string.empty": "This field is required.", "string.uri": "The URL you provided is not valid.", "string.invalidUrl": "Please enter a valid URL with optional port", - "string.invalidDockerID": "Please enter a valid 64-character Docker container ID.", - "string.pattern.base": "Please enter a valid container ID.", }), - port: joi - .number() - .integer() - .min(1) - .max(65535) - .when("type", { - is: "port", - then: joi.number().messages({ - "number.base": "Port must be a number.", - "number.min": "Port must be at least 1.", - "number.max": "Port must be at most 65535.", - "any.required": "Port is required for port monitors.", - }), - otherwise: joi.optional(), - }), - name: joi.string().trim().max(50).allow("").messages({ - "string.max": "This field should not exceed the 50 characters limit.", - }), - type: joi.string().trim().messages({ "string.empty": "This field is required." }), - ignoreTlsErrors: joi.boolean(), - interval: joi.number().messages({ - "number.base": "Frequency must be a number.", - "any.required": "Frequency is required.", - }), - expectedValue: joi.string().allow(""), - jsonPath: joi.string().allow(""), - matchMethod: joi.string(), -}); - -const imageValidation = joi.object({ - type: joi.string().valid("image/jpeg", "image/png").messages({ - "any.only": "Invalid file format.", - "string.empty": "File type required.", - }), - size: joi - .number() - .max(3 * 1024 * 1024) - .messages({ - "number.base": "File size must be a number.", - "number.max": "File size must be less than 3 MB.", - "number.empty": "File size required.", - }), -}); - -const logoImageValidation = joi - .object({ - src: joi.string(), - name: joi.string(), - type: joi - .string() - .valid("image/jpeg", "image/png") - .allow(null) // Allow null and empty string - .messages({ - "any.only": "Invalid file format.", - "string.empty": "File type required.", - }) - .optional(), - size: joi - .number() - .max(3000000) - .allow(null) // Allow null and empty string - .messages({ - "number.base": "File size must be a number.", - "number.max": "File size must be less than 3MB.", - "number.empty": "File size required.", - }) - .optional(), - }) - .allow(null) - .optional(); // Make entire object optional - -const statusPageValidation = joi.object({ - type: joi.string().valid("uptime", "distributed").required(), - isPublished: joi.bool(), - companyName: joi - .string() - .trim() - .messages({ "string.empty": "Company name is required." }), - url: joi - .string() - .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen - .required() - .messages({ - "string.pattern.base": - "URL can only contain letters, numbers, underscores, and hyphens", - }), - timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }), - color: joi.string().trim().messages({ "string.empty": "Color is required." }), - theme: joi.string(), - monitors: joi.array().min(1).required().messages({ - "string.pattern.base": "Must be a valid monitor ID", - "array.base": "Monitors must be an array", - "array.min": "At least one monitor is required", - "array.empty": "At least one monitor is required", - "any.required": "At least one monitor is required", - }), - subMonitors: joi.array().optional(), - logo: logoImageValidation, - showUptimePercentage: joi.boolean(), - showCharts: joi.boolean(), -}); -const settingsValidation = joi.object({ - checkTTL: joi.number().required().messages({ - "string.empty": "Please enter a value", - "number.base": "Please enter a valid number", - "any.required": "Please enter a value", - }), - pagespeedApiKey: joi.string().allow("").optional(), - language: joi.string().required(), - systemEmailHost: joi.string().allow(""), - systemEmailPort: joi.number().allow(null, ""), - systemEmailAddress: joi.string().allow(""), - systemEmailPassword: joi.string().allow(""), - systemEmailUser: joi.string().allow(""), -}); - -const dayjsValidator = (value, helpers) => { - if (!dayjs(value).isValid()) { - return helpers.error("any.invalid"); - } - return value; -}; - -const maintenanceWindowValidation = joi.object({ - repeat: joi.string(), - startDate: joi.custom(dayjsValidator, "Day.js date validation"), - startTime: joi.custom(dayjsValidator, "Day.js date validation"), - duration: joi.number().integer().min(0), - durationUnit: joi.string(), - name: joi.string(), - monitors: joi.array().min(1), -}); - -const advancedSettingsValidation = joi.object({ - apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({ - "string.empty": "API base url is required.", - "string.uri": "The URL you provided is not valid.", - }), - logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""), - systemEmailHost: joi.string().allow(""), - systemEmailPort: joi.number().allow(null, ""), - systemEmailAddress: joi.string().allow(""), - systemEmailPassword: joi.string().allow(""), - jwtTTLNum: joi.number().messages({ - "number.base": "JWT TTL is required.", - }), - jwtTTLUnits: joi - .string() - .trim() - .custom((value, helpers) => { - if (!["days", "hours"].includes(value)) { - return helpers.message("JWT TTL unit is required."); - } - return value; - }), - dbType: joi.string().trim().messages({ - "string.empty": "DB type is required.", - }), - redisHost: joi.string().trim().messages({ - "string.empty": "Redis host is required.", - }), - redisPort: joi.number().allow(null, ""), - pagespeedApiKey: joi.string().allow(""), -}); - -const infrastructureMonitorValidation = joi.object({ - url: joi - .string() - .trim() - .custom((value, helpers) => { - const urlRegex = - /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i; - - if (!urlRegex.test(value)) { - return helpers.error("string.invalidUrl"); - } - - return value; - }) - .messages({ - "string.empty": "This field is required.", - "string.uri": "The URL you provided is not valid.", - "string.invalidUrl": "Please enter a valid URL with optional port", - }), - name: joi.string().trim().max(50).allow("").messages({ - "string.max": "This field should not exceed the 50 characters limit.", - }), - secret: joi.string().trim().messages({ "string.empty": "This field is required." }), - usage_cpu: joi.number().messages({ - "number.base": THRESHOLD_COMMON_BASE_MSG, - }), - cpu: joi.boolean(), - memory: joi.boolean(), - disk: joi.boolean(), - temperature: joi.boolean(), - usage_memory: joi.number().messages({ - "number.base": THRESHOLD_COMMON_BASE_MSG, - }), - usage_disk: joi.number().messages({ - "number.base": THRESHOLD_COMMON_BASE_MSG, - }), - usage_temperature: joi.number().messages({ - "number.base": "Temperature must be a number.", - }), + name: joi.string().trim().max(50).allow("").messages({ + "string.max": "This field should not exceed the 50 characters limit.", + }), + secret: joi.string().trim().messages({ "string.empty": "This field is required." }), + usage_cpu: joi.number().messages({ + "number.base": THRESHOLD_COMMON_BASE_MSG, + }), + cpu: joi.boolean(), + memory: joi.boolean(), + disk: joi.boolean(), + temperature: joi.boolean(), + usage_memory: joi.number().messages({ + "number.base": THRESHOLD_COMMON_BASE_MSG, + }), + usage_disk: joi.number().messages({ + "number.base": THRESHOLD_COMMON_BASE_MSG, + }), + usage_temperature: joi.number().messages({ + "number.base": "Temperature must be a number.", + }), // usage_system: joi.number().messages({ // "number.base": "System load must be a number.", // }), // usage_swap: joi.number().messages({ // "number.base": "Swap used must be a number.", // }), - interval: joi.number().messages({ - "number.base": "Frequency must be a number.", - "any.required": "Frequency is required.", - }), - notifications: joi.array().items( - joi.object({ - type: joi.string().valid("email").required(), - address: joi - .string() - .email({ tlds: { allow: false } }) - .required(), - }) - ), + interval: joi.number().messages({ + "number.base": "Frequency must be a number.", + "any.required": "Frequency is required.", + }), + notifications: joi.array().items( + joi.object({ + type: joi.string().valid("email").required(), + address: joi + .string() + .email({ tlds: { allow: false } }) + .required(), + }) + ), }); export { - credentials, - imageValidation, - monitorValidation, - settingsValidation, - maintenanceWindowValidation, - advancedSettingsValidation, - infrastructureMonitorValidation, - statusPageValidation, - logoImageValidation, + credentials, + imageValidation, + monitorValidation, + settingsValidation, + maintenanceWindowValidation, + advancedSettingsValidation, + infrastructureMonitorValidation, + statusPageValidation, + logoImageValidation, }; From a3a381aad14a1e7da542e6e7064ad41ca830aea7 Mon Sep 17 00:00:00 2001 From: Cahit Atilgan Date: Thu, 15 May 2025 14:27:08 +0200 Subject: [PATCH 08/97] fix: removed env variable, configured a form field for editing email connect host --- client/src/Pages/Settings/SettingsEmail.jsx | 9 +++++++++ client/src/Validation/validation.js | 1 + client/src/locales/gb.json | 1 + server/db/models/AppSettings.js | 3 +++ server/service/settingsService.js | 1 - 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index 694219614..46430e2e5 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -122,6 +122,15 @@ const SettingsEmail = ({ )} + + {t("systemEmailConnectionHost")} + + + + {isAdmin && ( + + )} + + + ); +}; + +MonitorDetailsControlHeader.propTypes = { + path: PropTypes.string, + isLoading: PropTypes.bool, + isAdmin: PropTypes.bool, + monitor: PropTypes.object, + triggerUpdate: PropTypes.func, +}; + +export default MonitorDetailsControlHeader; diff --git a/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx b/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx new file mode 100644 index 000000000..64dc0547f --- /dev/null +++ b/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx @@ -0,0 +1,23 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/client/src/Components/MonitorDetailsControlHeader/status.jsx b/client/src/Components/MonitorDetailsControlHeader/status.jsx new file mode 100644 index 000000000..910a22210 --- /dev/null +++ b/client/src/Components/MonitorDetailsControlHeader/status.jsx @@ -0,0 +1,44 @@ +// Components +import Stack from "@mui/material/Stack"; +import PulseDot from "../../Components/Animated/PulseDot"; +import Typography from "@mui/material/Typography"; +import Dot from "../../Components/Dot"; +// Utils +import { formatDurationRounded } from "../../Utils/timeUtils"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; + +const Status = ({ monitor }) => { + const theme = useTheme(); + const { statusColor, determineState } = useUtils(); + + return ( + + {monitor?.name} + + + + {monitor?.url?.replace(/^https?:\/\//, "") || "..."} + + + + Checking every {formatDurationRounded(monitor?.interval)}. + + + + ); +}; + +Status.propTypes = { + monitor: PropTypes.object, +}; + +export default Status; From 54db96a03e9c29642b7b3cebcfde011a72a11fb5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 May 2025 10:49:38 -0700 Subject: [PATCH 15/97] add trigger to useFetchUptimeMonitorDetails --- client/src/Hooks/useFetchUptimeMonitorDetails.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/Hooks/useFetchUptimeMonitorDetails.js b/client/src/Hooks/useFetchUptimeMonitorDetails.js index c8cb81e65..3c0ac7ee6 100644 --- a/client/src/Hooks/useFetchUptimeMonitorDetails.js +++ b/client/src/Hooks/useFetchUptimeMonitorDetails.js @@ -3,13 +3,12 @@ import { networkService } from "../main"; import { useNavigate } from "react-router-dom"; import { createToast } from "../Utils/toastUtils"; -export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => { +export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => { const [networkError, setNetworkError] = useState(false); const [isLoading, setIsLoading] = useState(true); const [monitor, setMonitor] = useState(undefined); const [monitorStats, setMonitorStats] = useState(undefined); const navigate = useNavigate(); - useEffect(() => { const fetchMonitors = async () => { try { @@ -29,7 +28,7 @@ export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => { } }; fetchMonitors(); - }, [dateRange, monitorId, navigate]); + }, [dateRange, monitorId, navigate, trigger]); return [monitor, monitorStats, isLoading, networkError]; }; From f9fcd4ff3a037f163b342b0e42b26f23f670b178 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 May 2025 10:50:21 -0700 Subject: [PATCH 16/97] remove pause from uptime config page --- client/src/Pages/Uptime/Configure/index.jsx | 49 --------------------- 1 file changed, 49 deletions(-) diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx index 5afbc4a27..d52780eea 100644 --- a/client/src/Pages/Uptime/Configure/index.jsx +++ b/client/src/Pages/Uptime/Configure/index.jsx @@ -158,23 +158,6 @@ const Configure = () => { } }; - const handlePause = async () => { - try { - const action = await dispatch(pauseUptimeMonitor({ monitorId })); - if (pauseUptimeMonitor.fulfilled.match(action)) { - const monitor = action.payload.data; - setMonitor(monitor); - const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; - createToast({ body: `Monitor ${state} successfully.` }); - } else if (pauseUptimeMonitor.rejected.match(action)) { - throw new Error(action.error.message); - } - } catch (error) { - logger.error("Error pausing monitor: " + monitorId); - createToast({ body: "Failed to pause monitor" }); - } - }; - const handleSubmit = async (event) => { event.preventDefault(); const action = await dispatch(updateUptimeMonitor({ monitor: monitor })); @@ -329,38 +312,6 @@ const Configure = () => { ml: "auto", }} > -