{
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
+ isPublic={isPublic}
/>
{t("statusPageStatusServiceStatus")}
diff --git a/client/src/Pages/Uptime/BulkImport/Upload.jsx b/client/src/Pages/Uptime/BulkImport/Upload.jsx
new file mode 100644
index 000000000..9f49b54bc
--- /dev/null
+++ b/client/src/Pages/Uptime/BulkImport/Upload.jsx
@@ -0,0 +1,73 @@
+import { useTheme } from "@emotion/react";
+import { useState, useRef } from "react";
+import { Button, Typography } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import PropTypes from "prop-types";
+
+const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
+ const theme = useTheme();
+ const [file, setFile] = useState();
+ const [error, setError] = useState("");
+ const inputRef = useRef();
+ const { t } = useTranslation();
+
+ const handleSelectFile = () => {
+ inputRef.current.click();
+ };
+
+ const handleFileChange = (e) => {
+ setError("");
+ const selectedFile = e.target.files[0];
+
+ // Basic file validation
+ if (!selectedFile) return;
+
+ if (!selectedFile.name.endsWith('.csv')) {
+ setError(t("bulkImport.invalidFileType"));
+ return;
+ }
+
+ setFile(selectedFile);
+ onFileSelect(selectedFile); // Pass the file directly to parent
+ };
+
+ return (
+
+
+
+ {file?.name || t("bulkImport.noFileSelected")}
+
+
+ {error}
+
+
+
+ );
+}
+
+
+UploadFile.prototype = {
+ onFileSelect: PropTypes.func.isRequired,
+};
+
+export default UploadFile;
\ No newline at end of file
diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/Uptime/BulkImport/index.jsx
new file mode 100644
index 000000000..4d2e716f3
--- /dev/null
+++ b/client/src/Pages/Uptime/BulkImport/index.jsx
@@ -0,0 +1,112 @@
+// React, Redux, Router
+import { useTheme } from "@emotion/react";
+import { useState } from "react";
+// MUI
+import { Box, Stack, Typography, Button, Link } from "@mui/material";
+
+//Components
+import { createToast } from "../../../Utils/toastUtils";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import ConfigBox from "../../../Components/ConfigBox";
+import UploadFile from "./Upload";
+import { useSelector } from "react-redux";
+import { useNavigate } from "react-router";
+import { Trans, useTranslation } from "react-i18next";
+import { useBulkMonitors } from "../../../Hooks/useBulkMonitors";
+
+const BulkImport = () => {
+ const theme = useTheme();
+ const { user } = useSelector((state) => state.auth);
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [selectedFile, setSelectedFile] = useState(null);
+
+ const crumbs = [
+ { name: t("uptime"), path: "/uptime" },
+ { name: t("bulkImport.title"), path: `/uptime/bulk-import` },
+ ];
+
+ const [createBulkMonitors, hookLoading] = useBulkMonitors();
+
+ const handleSubmit = async () => {
+ if (!selectedFile) {
+ createToast({ body: t("bulkImport.noFileSelected") });
+ return;
+ }
+
+ const [success, data, error] = await createBulkMonitors(selectedFile, user);
+
+ if (success) {
+ // You can use `data` here if needed
+ createToast({ body: t("bulkImport.uploadSuccess") });
+ navigate("/uptime");
+ } else {
+ createToast({ body: error || t("bulkImport.uploadFailed") });
+ }
+ };
+
+ return (
+
+
+
+
+ {t("bulkImport.title")}
+
+
+
+ {t("bulkImport.selectFileTips")}
+
+
+ ),
+ sample: (
+
+ ),
+ }}
+ />
+
+
+
+
+ setSelectedFile(file)} />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BulkImport;
diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx
index 37d8511c7..f4b332e04 100644
--- a/client/src/Pages/Uptime/Configure/index.jsx
+++ b/client/src/Pages/Uptime/Configure/index.jsx
@@ -25,6 +25,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import SkeletonLayout from "./skeleton";
import "./index.css";
import Dialog from "../../../Components/Dialog";
+import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
/**
* Parses a URL string and returns a URL object.
@@ -196,6 +197,18 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
+ // Notification modal state
+ const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
+
+ const handleOpenNotificationModal = () => {
+ setIsNotificationModalOpen(true);
+ };
+
+ const handleClosenNotificationModal = () => {
+ setIsNotificationModalOpen(false);
+ };
+
+
const statusColor = {
true: theme.palette.success.main,
false: theme.palette.error.main,
@@ -371,6 +384,17 @@ const Configure = () => {
value={parsedUrl?.host || monitor?.url || ""}
disabled={true}
/>
+ handleChange(event, "port")}
+ error={errors["port"] ? true : false}
+ helperText={errors["port"]}
+ hidden={monitor.type !== "port"}
+ />
{
value={user?.email}
onChange={(event) => handleChange(event)}
/>
+
+
+
{/* {
onConfirm={handleRemove}
isLoading={isLoading}
/>
+
+
);
};
diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx
index 19b39c64a..60a308141 100644
--- a/client/src/Pages/Uptime/Monitors/index.jsx
+++ b/client/src/Pages/Uptime/Monitors/index.jsx
@@ -189,8 +189,9 @@ const UptimeMonitors = () => {
{
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -78,6 +79,11 @@ const Routes = () => {
element={}
/>
+ }
+ />
+
}
@@ -90,16 +96,16 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={}
/>
-
{" "}
}
- />
+ /> */}
-
@@ -114,15 +120,15 @@ const Routes = () => {
}
- />
- */}
+ {/*
}
- />
+ /> */}
{
path="infrastructure/create"
element={}
/>
- }
+ }
/>
{
element={}
/>
-
}
- />
+ /> */}
}
/>
-
}
- />
+ /> */}
}
/>
-
}
- />
+ /> */}
{
path="/status/uptime/public/:url"
element={}
/>
- }
- />
+ /> */}
response,
(error) => {
+ if (error.code === "ERR_NETWORK") {
+ // Do error handling here
+ }
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
@@ -266,6 +269,7 @@ class NetworkService {
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
jsonPath: monitor.jsonPath,
+ ...(monitor.type === "port" && { port: monitor.port }),
};
return this.axiosInstance.put(`/monitors/${monitorId}`, payload, {
headers: {
@@ -1086,6 +1090,19 @@ class NetworkService {
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {});
}
+ // ************************************
+ // Create bulk monitors
+ // ************************************
+
+ async createBulkMonitors(formData) {
+ const response = await this.axiosInstance.post(`/monitors/bulk`, formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ return response.data;
+ }
+
// ************************************
// Fetch monitors with summary by TeamID
// ************************************
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 6e8ff76cb..38092c9e6 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -7,18 +7,11 @@ const nameSchema = joi
.string()
.max(50)
.trim()
- .pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u, {
- name: "name.containsLetterOrSymbol",
- })
- .pattern(/^[\p{L}\p{Sc}\s']+$/u, {
- name: "name.validCharacters",
- })
+ .pattern(/^[\p{L}\p{M}''\- ]+$/u)
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
- "string.pattern.name": "Name must contain at least 1 letter or currency symbol",
- "string.pattern.base.validCharacters":
- "Can only contain letters, spaces, apostrophes, and currency symbols",
+ "string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
});
const passwordSchema = joi
@@ -159,7 +152,20 @@ const monitorValidation = joi.object({
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.pattern.base": "Please enter a valid container ID.",
}),
- port: joi.number(),
+ port: joi.number()
+ .integer()
+ .min(1)
+ .max(65535)
+ .when("type", {
+ is: "port",
+ then: joi.required().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.",
}),
@@ -366,5 +372,5 @@ export {
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
- logoImageValidation,
+ logoImageValidation
};
diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json
index 4d5ac89c1..3ce6cb3dc 100644
--- a/client/src/locales/gb.json
+++ b/client/src/locales/gb.json
@@ -54,9 +54,9 @@
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "must match",
"authRegisterCreateAccount": "Create your account to get started",
- "authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started",
- "authRegisterSignUpWithEmail": "Sign up with Email",
- "authRegisterBySigningUp": "By signing up, you agree to our",
+ "authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
+ "authRegisterSignUpWithEmail": "Create super admin account",
+ "authRegisterBySigningUp": "By creating an account, you agree to our Terms of Service and Privacy Policy.",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
@@ -396,5 +396,22 @@
"DeleteAccount": "Delete account",
"DeleteDescriptionText": "Note that deleting your account will remove all data from the server. This is permanent and non-recoverable.",
"DeleteAccountWarning": "If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action.",
- "DeleteWarningTitle": "Really delete this account?"
-}
\ No newline at end of file
+ "DeleteWarningTitle": "Really delete this account?",
+ "authRegisterFirstName": "Name",
+ "authRegisterLastName": "Surname",
+ "authRegisterEmail": "Email",
+ "authRegisterEmailRequired": "Email is required",
+ "bulkImport": {
+ "title": "Bulk Import",
+ "selectFileTips": "Select CSV file to upload",
+ "selectFileDescription": "You can download our template or sample",
+ "selectFile": "Select File",
+ "parsingFailed": "Parsing failed",
+ "uploadSuccess": "Monitors created successfully!",
+ "validationFailed": "Validation failed",
+ "noFileSelected": "No file selected",
+ "fallbackPage": "Import a file to upload a list of servers in bulk"
+ },
+ "publicLink": "Public link"
+}
+
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index 76c8c28bd..4ee3f249e 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -55,8 +55,12 @@
"authPasswordMustMatch": "должен совпадать",
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
"authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
- "authRegisterSignUpWithEmail": "Зарегистрироваться по электронной почте",
- "authRegisterBySigningUp": "Регистрируясь, вы соглашаетесь с нашими",
+ "authRegisterSignUpWithEmail": "Создать учетную запись суперадминистратора",
+ "authRegisterBySigningUp": "Создавая учетную запись, вы соглашаетесь с нашими Условиями использования и Политикой конфиденциальности.",
+ "authRegisterFirstName": "Имя",
+ "authRegisterLastName": "Фамилия",
+ "authRegisterEmail": "Эл. почта",
+ "authRegisterEmailRequired": "Эл. почта обязательна",
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 6d89cab94..e942a2263 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -396,5 +396,20 @@
"DeleteAccount": "",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
- "DeleteWarningTitle": ""
-}
\ No newline at end of file
+ "DeleteWarningTitle": "",
+ "authRegisterFirstName": "",
+ "authRegisterLastName": "",
+ "authRegisterEmail": "",
+ "authRegisterEmailRequired": "",
+ "bulkImport": {
+ "title": "",
+ "selectFileTips": "",
+ "selectFileDescription": "",
+ "selectFile": "",
+ "parsingFailed": "",
+ "uploadSuccess": "",
+ "validationFailed": "",
+ "noFileSelected": "",
+ "fallbackPage": ""
+ }
+}
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index 2cd2d9678..6295a9aba 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -24,13 +24,15 @@ import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
+import pkg from "papaparse";
class MonitorController {
- constructor(db, settingsService, jobQueue, stringService) {
+ constructor(db, settingsService, jobQueue, stringService, emailService) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
+ this.emailService = emailService;
}
/**
@@ -243,56 +245,119 @@ class MonitorController {
};
/**
- * Creates bulk monitors and adds them to the job queue.
+ * Creates bulk monitors and adds them to the job queue after parsing CSV.
* @async
* @param {Object} req - The Express request object.
- * @property {Object} req.body - The body of the request.
+ * @property {Object} req.file - The uploaded CSV file.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
+ * @returns {Object} The response object with a success status and message.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
createBulkMonitors = async (req, res, next) => {
try {
- await createMonitorsBodyValidation.validateAsync(req.body);
- } catch (error) {
- next(handleValidationError(error, SERVICE_NAME));
- return;
- }
+ const { parse } = pkg;
- try {
- // create monitors
- const monitors = await this.db.createBulkMonitors(req);
+ // validate the file
+ if (!req.file) {
+ throw new Error("No file uploaded");
+ }
- // create notifications for each monitor
- await Promise.all(
- monitors.map(async (monitor, index) => {
- const notifications = req.body[index].notifications;
+ // Check if the file is a CSV
+ if (!req.file.mimetype.includes("csv")) {
+ throw new Error("File is not a CSV");
+ }
- if (notifications?.length) {
- monitor.notifications = await Promise.all(
- notifications.map(async (notification) => {
- notification.monitorId = monitor._id;
- return await this.db.createNotification(notification);
- })
- );
- await monitor.save();
+ // Validate if the file is empty
+ if (req.file.size === 0) {
+ throw new Error("File is empty");
+ }
+
+ const { userId, teamId } = req.body;
+
+ if (!userId || !teamId) {
+ throw new Error("Missing userId or teamId in form data");
+ }
+
+ // Get file buffer from memory and convert to string
+ const fileData = req.file.buffer.toString("utf-8");
+
+ // Parse the CSV data
+ parse(fileData, {
+ header: true,
+ skipEmptyLines: true,
+ transform: (value, header) => {
+ if (value === "") return undefined; // Empty fields become undefined
+
+ // Handle 'port' and 'interval' fields, check if they're valid numbers
+ if (["port", "interval"].includes(header)) {
+ const num = parseInt(value, 10);
+ if (isNaN(num)) {
+ throw new Error(`${header} should be a valid number, got: ${value}`);
+ }
+ return num;
}
- // Add monitor to job queue
- this.jobQueue.addJob(monitor._id, monitor);
- })
- );
+ return value;
+ },
+ complete: async ({ data, errors }) => {
+ try {
+ if (errors.length > 0) {
+ throw new Error("Error parsing CSV");
+ }
- return res.success({
- msg: this.stringService.bulkMonitorsCreate,
- data: monitors,
+ if (!data || data.length === 0) {
+ throw new Error("CSV file contains no data rows");
+ }
+
+ const enrichedData = data.map((monitor) => ({
+ userId,
+ teamId,
+ ...monitor,
+ description: monitor.description || monitor.name || monitor.url,
+ name: monitor.name || monitor.url,
+ type: monitor.type || "http",
+ }));
+
+ await createMonitorsBodyValidation.validateAsync(enrichedData);
+
+ try {
+ const monitors = await this.db.createBulkMonitors(enrichedData);
+
+ await Promise.all(
+ monitors.map(async (monitor, index) => {
+ const notifications = enrichedData[index].notifications;
+
+ if (notifications?.length) {
+ monitor.notifications = await Promise.all(
+ notifications.map(async (notification) => {
+ notification.monitorId = monitor._id;
+ return await this.db.createNotification(notification);
+ })
+ );
+ await monitor.save();
+ }
+
+ this.jobQueue.addJob(monitor._id, monitor);
+ })
+ );
+
+ return res.success({
+ msg: this.stringService.bulkMonitorsCreate,
+ data: monitors,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
+ }
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
+ }
+ },
});
} catch (error) {
- next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
+ return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
}
};
-
/**
* Checks if the endpoint can be resolved
* @async
@@ -572,6 +637,50 @@ class MonitorController {
}
};
+ /**
+ * Sends a test email to verify email delivery functionality.
+ * @async
+ * @param {Object} req - The Express request object.
+ * @property {Object} req.body - The body of the request.
+ * @property {string} req.body.to - The email address to send the test email to.
+ * @param {Object} res - The Express response object.
+ * @param {function} next - The next middleware function.
+ * @returns {Object} The response object with a success status and the email delivery message ID.
+ * @throws {Error} If there is an error while sending the test email.
+ */
+ sendTestEmail = async (req, res, next) => {
+ try {
+ const { to } = req.body;
+
+ if (!to || typeof to !== "string") {
+ return res.error({ msg: this.stringService.errorForValidEmailAddress });
+ }
+
+ const subject = this.stringService.testEmailSubject;
+ const context = { testName: "Monitoring System" };
+
+ const messageId = await this.emailService.buildAndSendEmail(
+ "testEmailTemplate",
+ context,
+ to,
+ subject
+ );
+
+ if (!messageId) {
+ return res.error({
+ msg: "Failed to send test email.",
+ });
+ }
+
+ return res.success({
+ msg: this.stringService.sendTestEmail,
+ data: { messageId },
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "sendTestEmail"));
+ }
+ };
+
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js
index 1cea930ac..fe8e3c2fc 100755
--- a/server/db/mongo/modules/monitorModule.js
+++ b/server/db/mongo/modules/monitorModule.js
@@ -751,7 +751,7 @@ const createMonitor = async (req, res) => {
*/
const createBulkMonitors = async (req) => {
try {
- const monitors = req.body.map(
+ const monitors = req.map(
(item) => new Monitor({ ...item, notifications: undefined })
);
await Monitor.bulkSave(monitors);
diff --git a/server/index.js b/server/index.js
index 520e5cab2..153660de1 100755
--- a/server/index.js
+++ b/server/index.js
@@ -231,7 +231,8 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME),
- ServiceRegistry.get(StringService.SERVICE_NAME)
+ ServiceRegistry.get(StringService.SERVICE_NAME),
+ ServiceRegistry.get(EmailService.SERVICE_NAME),
);
const settingsController = new SettingsController(
diff --git a/server/locales/en.json b/server/locales/en.json
index 449ab5a8e..6cd956c37 100755
--- a/server/locales/en.json
+++ b/server/locales/en.json
@@ -158,5 +158,8 @@
"platformRequired": "Platform is required",
"testNotificationFailed": "Failed to send test notification",
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n",
- "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
+ "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
+ "sendTestEmail": "Test email sent successfully",
+ "errorForValidEmailAddress": "A valid recipient email address is required.",
+ "testEmailSubject": "Test Email from Monitoring System"
}
diff --git a/server/package.json b/server/package.json
index c95fb6f62..097cf4443 100755
--- a/server/package.json
+++ b/server/package.json
@@ -33,8 +33,9 @@
"mailersend": "^2.2.0",
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
- "multer": "1.4.5-lts.1",
+ "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
+ "papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js
index 36eed3911..f9ef460c6 100755
--- a/server/routes/monitorRoute.js
+++ b/server/routes/monitorRoute.js
@@ -1,7 +1,12 @@
import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
+import multer from "multer";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
+const upload = multer({
+ storage: multer.memoryStorage() // Store file in memory as Buffer
+ });
+
class MonitorRoutes {
constructor(monitorController) {
this.router = Router();
@@ -89,10 +94,17 @@ class MonitorRoutes {
this.router.post(
"/bulk",
isAllowed(["admin", "superadmin"]),
+ upload.single("csvFile"),
this.monitorController.createBulkMonitors
);
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
+
+ this.router.post(
+ "/test-email",
+ isAllowed(["admin", "superadmin"]),
+ this.monitorController.sendTestEmail
+ );
}
getRouter() {
diff --git a/server/service/emailService.js b/server/service/emailService.js
index ac2e077c0..c9e34eeda 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -67,6 +67,7 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
+ testEmailTemplate: this.loadTemplate("testEmailTemplate")
};
/**
diff --git a/server/templates/testEmailTemplate.mjml b/server/templates/testEmailTemplate.mjml
new file mode 100644
index 000000000..e9fd3221e
--- /dev/null
+++ b/server/templates/testEmailTemplate.mjml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Hello!
+
+
+ This is a test email from the Monitoring System.
+
+
+ If you're receiving this, your email configuration is working correctly.
+
+
+
+
+
diff --git a/server/validation/joi.js b/server/validation/joi.js
index 8f17df0d1..d4f2ceade 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -210,6 +210,7 @@ const editMonitorBodyValidation = joi.object({
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),
+ port: joi.number().min(1).max(65535),
thresholds: joi.object().keys({
usage_cpu: joi.number(),
usage_memory: joi.number(),