From f6be4317cd4fb3ed669afadc27f47a6733dd9543 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 10:36:20 +0800 Subject: [PATCH 01/22] remove user and teamid from validation --- server/validation/joi.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server/validation/joi.js b/server/validation/joi.js index 5eb24e758..725fcf218 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -558,14 +558,6 @@ const triggerNotificationBodyValidation = joi.object({ }); const createNotificationBodyValidation = joi.object({ - userId: joi.string().required().messages({ - "number.empty": "User ID is required", - "any.required": "User ID is required", - }), - teamId: joi.string().required().messages({ - "string.empty": "Team ID is required", - "any.required": "Team ID is required", - }), notificationName: joi.string().required().messages({ "string.empty": "Notification name is required", "any.required": "Notification name is required", From 22f8a37f1f93927a2d60c10e5808fedf2297d45f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 10:36:34 +0800 Subject: [PATCH 02/22] add discord type --- server/db/models/Notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js index 34afa1f31..751324131 100755 --- a/server/db/models/Notification.js +++ b/server/db/models/Notification.js @@ -7,7 +7,7 @@ const configSchema = mongoose.Schema( chatId: { type: String }, platform: { type: String, - enum: ["slack", "pager_duty", "webhook"], + enum: ["slack", "pager_duty", "webhook", "discord"], }, routingKey: { type: String }, }, From bfae00b69b56e369c250d35c75158174a69ecbf8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 10:36:45 +0800 Subject: [PATCH 03/22] add strings --- client/src/locales/en.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 2791dd3e7..31db9325b 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -298,6 +298,12 @@ "description": "Configure your webhook here", "webhookLabel": "Webhook URL", "webhookPlaceholder": "https://your-server.com/webhook" + }, + "discordSettings": { + "title": "Discord", + "description": "Configure your Discord webhook here", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" } }, From 5aae16486560bdccba425adc77f8eb8bf551bb90 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 10:37:04 +0800 Subject: [PATCH 04/22] extrat userId and teamId from req.uesr --- server/controllers/notificationController.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index a9c399901..82e2a23ab 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -102,6 +102,7 @@ class NotificationController { success = await this.notificationService.sendTestPagerDutyNotification(notification); } + if (!success) { return res.error({ msg: "Sending notification failed", @@ -128,7 +129,11 @@ class NotificationController { } try { - const notification = await this.db.createNotification(req.body); + const body = req.body; + const { _id, teamId } = req.user; + body.userId = _id; + body.teamId = teamId; + const notification = await this.db.createNotification(body); return res.success({ msg: "Notification created successfully", data: notification, From bea23a94bb6a042faaea8ee3b6ce1b883f5a17ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 10:38:05 +0800 Subject: [PATCH 05/22] handle discord webhooks --- .../src/Pages/Notifications/create/index.jsx | 72 ++++++++++++++----- client/src/Pages/Notifications/utils.js | 3 +- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 3c17c113c..8b5512ca8 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -10,7 +10,6 @@ import TextInput from "../../../Components/Inputs/TextInput"; // Utils import { useState } from "react"; -import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; import { useCreateNotification, @@ -44,7 +43,6 @@ const CreateNotifications = () => { ]; // Redux state - const { user } = useSelector((state) => state.auth); // local state const [notification, setNotification] = useState({ @@ -73,7 +71,7 @@ const CreateNotifications = () => { type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value, }; - if (notification.type === 2) { + if (form.type === "slack" || form.type === "discord") { form.type = "webhook"; } @@ -128,27 +126,33 @@ const CreateNotifications = () => { // Handle config/platform initialization if type is webhook - if (newNotification.type === 1) { + const type = NOTIFICATION_TYPES.find( + (type) => type._id === newNotification.type + ).value; + + if (newNotification.type === "email") { newNotification.config = null; - } else if (newNotification.type === 2) { + } else if (type === "slack" || type === "webhook" || type === "discord") { newNotification.address = ""; newNotification.config = newNotification.config || {}; if (name === "config") { newNotification.config = value; } - newNotification.config.platform = "slack"; - } else if (newNotification.type === 3) { + if (type === "webhook") { + newNotification.config.platform = "webhook"; + } + if (type === "slack") { + newNotification.config.platform = "slack"; + } + if (type === "discord") { + newNotification.config.platform = "discord"; + } + } else if (type === "pager_duty") { newNotification.config = newNotification.config || {}; if (name === "config") { newNotification.config = value; } newNotification.config.platform = "pager_duty"; - } else if (newNotification.type === 4) { - newNotification.config = newNotification.config || {}; - if (name === "config") { - newNotification.config = value; - } - newNotification.config.platform = "webhook"; } // Field-level validation @@ -159,12 +163,15 @@ const CreateNotifications = () => { fieldError = error?.message; } - if (newNotification.type === 1 && name === "address") { + if (type === "email" && name === "address") { const { error } = notificationEmailValidation.extract(name).validate(value); fieldError = error?.message; } - if (newNotification.type === 2 && name === "config") { + if ( + (type === "slack" || type === "webhook" || type === "discord") && + name === "config" + ) { // Validate only webhookUrl inside config const { error } = notificationWebhookValidation.extract("config").validate(value); fieldError = error?.message; @@ -185,7 +192,7 @@ const CreateNotifications = () => { type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value, }; - if (notification.type === 2) { + if (form.type === "slack" || form.type === "discord") { form.type = "webhook"; } @@ -405,6 +412,39 @@ const CreateNotifications = () => { )} + {notification.type === 5 && ( + + + + {t("createNotifications.discordSettings.title")} + + + {t("createNotifications.discordSettings.description")} + + + + { + const updatedConfig = { + ...notification.config, + webhookUrl: e.target.value, + }; + + onChange({ + target: { + name: "config", + value: updatedConfig, + }, + }); + }} + /> + + + )} Date: Mon, 16 Jun 2025 13:30:27 +0800 Subject: [PATCH 06/22] fix email type --- client/src/Pages/Notifications/create/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 8b5512ca8..2fd83e4e1 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -130,7 +130,7 @@ const CreateNotifications = () => { (type) => type._id === newNotification.type ).value; - if (newNotification.type === "email") { + if (type === "email") { newNotification.config = null; } else if (type === "slack" || type === "webhook" || type === "discord") { newNotification.address = ""; From 3df6b451e7074e8ed05ba183eb35c0613a8e8943 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Jun 2025 13:53:08 +0800 Subject: [PATCH 07/22] fix hardware notifications --- server/service/notificationService.js | 88 ++++++++++++++++++++------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/server/service/notificationService.js b/server/service/notificationService.js index 6e12722c2..a8dfa4729 100755 --- a/server/service/notificationService.js +++ b/server/service/notificationService.js @@ -95,6 +95,19 @@ class NotificationService { return MESSAGE_FORMATTERS[platform](messageText, chatId); } + formatHardwareNotificationMessage = ( + monitor, + status, + platform, + chatId, + code, + timestamp, + alerts + ) => { + const messageText = alerts.map((alert) => alert).join("\n"); + return MESSAGE_FORMATTERS[platform](messageText, chatId); + }; + sendTestWebhookNotification = async (notification) => { const config = notification.config; const response = await this.networkService.requestWebhook( @@ -121,7 +134,7 @@ class NotificationService { * @returns {Promise} A promise that resolves to true if the notification was sent successfully, otherwise false. */ - async sendWebhookNotification(networkResponse, notification) { + async sendWebhookNotification(networkResponse, notification, alerts = []) { const { monitor, status, code } = networkResponse; const { webhookUrl, platform, botToken, chatId } = notification.config; @@ -152,14 +165,27 @@ class NotificationService { url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; } - const message = this.formatNotificationMessage( - monitor, - status, - platform, - chatId, - code, // Pass the code field directly - networkResponse.timestamp - ); + let message; + if (monitor.type === "hardware") { + message = this.formatHardwareNotificationMessage( + monitor, + status, + platform, + chatId, + code, + networkResponse.timestamp, + alerts + ); + } else { + message = this.formatNotificationMessage( + monitor, + status, + platform, + chatId, + code, // Pass the code field directly + networkResponse.timestamp + ); + } try { const response = await this.networkService.requestWebhook(platform, url, message); @@ -247,19 +273,31 @@ class NotificationService { return response; } - async sendPagerDutyNotification(networkResponse, notification) { + async sendPagerDutyNotification(networkResponse, notification, alerts = []) { const { monitor, status, code } = networkResponse; const { routingKey, platform } = notification.config; - const message = this.formatNotificationMessage( - monitor, - status, - platform, - null, - code, // Pass the code field directly - networkResponse.timestamp - ); - + let message; + if (monitor.type === "hardware") { + message = this.formatHardwareNotificationMessage( + monitor, + status, + platform, + null, + code, + networkResponse.timestamp, + alerts + ); + } else { + message = this.formatNotificationMessage( + monitor, + status, + platform, + null, + code, // Pass the code field directly + networkResponse.timestamp + ); + } try { const response = await this.networkService.requestPagerDuty({ message, @@ -350,9 +388,9 @@ class NotificationService { ) ?? false, }; - const notifications = await this.db.getNotificationsByMonitorId( - networkResponse.monitorId - ); + const notificationIDs = networkResponse.monitor?.notifications ?? []; + const notifications = await this.db.getNotificationsByIds(notificationIDs); + for (const notification of notifications) { const alertsToSend = []; const alertTypes = ["cpu", "memory", "disk"]; @@ -388,7 +426,11 @@ class NotificationService { if (alertsToSend.length === 0) continue; // No alerts to send, we're done if (notification.type === "email") { - this.sendHardwareEmail(networkResponse, notification.address, alertsToSend); + await this.sendHardwareEmail(networkResponse, notification.address, alertsToSend); + } else if (notification.type === "webhook") { + await this.sendWebhookNotification(networkResponse, notification, alertsToSend); + } else if (notification.type === "pager_duty") { + await this.sendPagerDutyNotification(networkResponse, notification, alertsToSend); } } return true; From 7d333c6d93607d76bd38d34b3358c107db0de1f5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 17 Jun 2025 10:58:30 +0800 Subject: [PATCH 08/22] return updated settings --- server/controllers/settingsController.js | 18 ++++++++++++------ server/db/mongo/modules/settingsModule.js | 6 ++++-- server/service/settingsService.js | 14 ++++++-------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index d1010c093..dd5c1b6d7 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -11,8 +11,7 @@ class SettingsController { this.emailService = emailService; } - getAppSettings = async (req, res, next) => { - const dbSettings = await this.settingsService.getDBSettings(); + buildAppSettings = (dbSettings) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; @@ -30,6 +29,14 @@ class SettingsController { delete sanitizedSettings.systemEmailPassword; } returnSettings.settings = sanitizedSettings; + return returnSettings; + }; + + getAppSettings = async (req, res, next) => { + const dbSettings = await this.settingsService.getDBSettings(); + + const returnSettings = this.buildAppSettings(dbSettings); + console.log(returnSettings); return res.success({ msg: this.stringService.getAppSettings, data: returnSettings, @@ -45,12 +52,11 @@ class SettingsController { } try { - await this.db.updateAppSettings(req.body); - const updatedSettings = { ...(await this.settingsService.reloadSettings()) }; - delete updatedSettings.jwtSecret; + const updatedSettings = await this.db.updateAppSettings(req.body); + const returnSettings = this.buildAppSettings(updatedSettings); return res.success({ msg: this.stringService.updateAppSettings, - data: updatedSettings, + data: returnSettings, }); } catch (error) { next(handleError(error, SERVICE_NAME, "updateAppSettings")); diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index d3568ad32..4fd0d1634 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -26,10 +26,12 @@ const updateAppSettings = async (newSettings) => { delete update.$set.systemEmailPassword; } - const settings = await AppSettings.findOneAndUpdate({}, update, { - new: true, + await AppSettings.findOneAndUpdate({}, update, { upsert: true, }); + const settings = await AppSettings.findOne() + .select("-__v -_id -createdAt -updatedAt -singleton") + .lean(); return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/service/settingsService.js b/server/service/settingsService.js index bc314abff..de035922c 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -28,8 +28,8 @@ class SettingsService { * Constructs a new SettingsService * @constructor * @throws {Error} - */ constructor(appSettings) { - this.appSettings = appSettings; + */ constructor(AppSettings) { + this.AppSettings = AppSettings; this.settings = { ...envConfig }; } /** @@ -60,16 +60,14 @@ class SettingsService { async getDBSettings() { // Remove any old settings - await this.appSettings.deleteMany({ version: { $exists: false } }); + await this.AppSettings.deleteMany({ version: { $exists: false } }); - let settings = await this.appSettings - .findOne({ singleton: true }) + let settings = await this.AppSettings.findOne({ singleton: true }) .select("-__v -_id -createdAt -updatedAt -singleton") .lean(); if (settings === null) { - await this.appSettings.create({}); - settings = await this.appSettings - .findOne({ singleton: true }) + await this.AppSettings.create({}); + settings = await this.AppSettings.findOne({ singleton: true }) .select("-__v -_id -createdAt -updatedAt -singleton") .lean(); } From 456798ca1519cd56289f61eccca657152c49a05a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 17 Jun 2025 10:58:41 +0800 Subject: [PATCH 09/22] move state to parent --- client/src/Hooks/settingsHooks.js | 24 ++++++++++++++--- client/src/Pages/Settings/SettingsEmail.jsx | 14 +++++----- .../src/Pages/Settings/SettingsPagespeed.jsx | 12 ++++++--- client/src/Pages/Settings/index.jsx | 26 +++++++++++++++---- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/client/src/Hooks/settingsHooks.js b/client/src/Hooks/settingsHooks.js index 7d3ff28fc..98a21c1fa 100644 --- a/client/src/Hooks/settingsHooks.js +++ b/client/src/Hooks/settingsHooks.js @@ -3,7 +3,7 @@ import { networkService } from "../main"; import { createToast } from "../Utils/toastUtils"; import { useTranslation } from "react-i18next"; -const useFetchSettings = ({ setSettingsData }) => { +const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); useEffect(() => { @@ -12,6 +12,8 @@ const useFetchSettings = ({ setSettingsData }) => { try { const response = await networkService.getAppSettings(); setSettingsData(response?.data?.data); + setIsApiKeySet(response?.data?.data?.pagespeedKeySet); + setIsEmailPasswordSet(response?.data?.data?.emailPasswordSet); } catch (error) { createToast({ body: "Failed to fetch settings" }); setError(error); @@ -20,12 +22,18 @@ const useFetchSettings = ({ setSettingsData }) => { } }; fetchSettings(); - }, []); + }, [setSettingsData]); return [isLoading, error]; }; -const useSaveSettings = () => { +const useSaveSettings = ({ + setSettingsData, + setIsApiKeySet, + setApiKeyHasBeenReset, + setIsEmailPasswordSet, + setEmailPasswordHasBeenReset, +}) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const { t } = useTranslation(); @@ -39,7 +47,15 @@ const useSaveSettings = () => { ttl: settings.checkTTL, }); } - console.log({ settingsResponse }); + setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet); + setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet); + if (settingsResponse.data.data.pagespeedKeySet === true) { + setApiKeyHasBeenReset(false); + } + if (settingsResponse.data.data.emailPasswordSet === true) { + setEmailPasswordHasBeenReset(false); + } + setSettingsData(settingsResponse.data.data); createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index e7ae88154..4bd6ff3ad 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -7,7 +7,7 @@ import Stack from "@mui/material/Stack"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; @@ -20,7 +20,9 @@ const SettingsEmail = ({ handleChange, settingsData, setSettingsData, - isPasswordSet, + isEmailPasswordSet, + emailPasswordHasBeenReset, + setEmailPasswordHasBeenReset, }) => { // Setup const { t } = useTranslation(); @@ -43,7 +45,6 @@ const SettingsEmail = ({ } = settingsData?.settings || {}; // Local state const [password, setPassword] = useState(""); - const [hasBeenReset, setHasBeenReset] = useState(false); // Network const [isSending, , sendTestEmail] = useSendTestEmail(); // Using empty placeholder for unused error variable @@ -152,7 +153,7 @@ const SettingsEmail = ({ onChange={handleChange} /> - {(isPasswordSet === false || hasBeenReset === true) && ( + {(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && ( {t("settingsEmailPassword")} )} - {isPasswordSet === true && hasBeenReset === false && ( + + {isEmailPasswordSet === true && emailPasswordHasBeenReset === false && ( {t("settingsEmailFieldResetLabel")}