diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..d4b59627d Binary files /dev/null and b/.DS_Store differ diff --git a/Client/package-lock.json b/Client/package-lock.json index 556caf1bc..febb13eb1 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,10 +11,10 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@hello-pangea/dnd": "^17.0.0", - "@mui/icons-material": "6.4.3", - "@mui/lab": "6.0.0-beta.26", - "@mui/material": "6.4.3", + "@hello-pangea/dnd": "^18.0.0", + "@mui/icons-material": "6.4.4", + "@mui/lab": "6.0.0-beta.27", + "@mui/material": "6.4.4", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.26.0", "@mui/x-date-pickers": "7.26.0", @@ -285,9 +285,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1002,21 +1002,20 @@ } }, "node_modules/@hello-pangea/dnd": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-17.0.0.tgz", - "integrity": "sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.25.6", + "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", - "memoize-one": "^6.0.0", "raf-schd": "^4.0.3", - "react-redux": "^9.1.2", - "redux": "^5.0.1", - "use-memo-one": "^1.1.3" + "react-redux": "^9.2.0", + "redux": "^5.0.1" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -1215,9 +1214,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz", - "integrity": "sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.4.tgz", + "integrity": "sha512-r+J0EditrekkTtO2CnCBCOGpNaDYwJqz8lH4rj6o/anDcskZFJodBlG8aCJkS8DL/CF/9EHS+Gz53EbmYEnQbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -1225,9 +1224,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.3.tgz", - "integrity": "sha512-3IY9LpjkwIJVgL/SkZQKKCUcumdHdQEsJaIavvsQze2QEztBt0HJ17naToN0DBBdhKdtwX5xXrfD6ZFUeWWk8g==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.4.tgz", + "integrity": "sha512-uF1chGaoFmYdRUomK6f8kgJfWosk9A3HXWiVD0vQm+2mE7f25eTQ1E8RRO11LXpnUBqu8Rbv/uGlpnjT/u1Ksg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1240,7 +1239,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.4.3", + "@mui/material": "^6.4.4", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1251,9 +1250,9 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.26", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.26.tgz", - "integrity": "sha512-auu2dXp6jslzW4Cp0tfYWv0xO9FmuwROsjyWcB9wPlAsEoWhh5N1FW8dqESDwaSKqFz5LwV+Y2vsYjYsYX9aOw==", + "version": "6.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.27.tgz", + "integrity": "sha512-weLxPsCs2wJKgWKf46shXHE+x7qlf5VxMK3P+4HsWasMakV/uTmxsoT7PG3QCvakGQ2TdpZtQLE2umJKC0mvKQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1274,7 +1273,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.4.3", + "@mui/material": "^6.4.4", "@mui/material-pigment-css": "^6.4.3", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -1296,13 +1295,13 @@ } }, "node_modules/@mui/material": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.3.tgz", - "integrity": "sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.4.tgz", + "integrity": "sha512-ISVPrIsPQsxnwvS40C4u03AuNSPigFeS2+n1qpuEZ94hDsdMi19dQM2JcC9CHEhXecSIQjP1RTyY0mPiSpSrFQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.3", + "@mui/core-downloads-tracker": "^6.4.4", "@mui/system": "^6.4.3", "@mui/types": "^7.2.21", "@mui/utils": "^6.4.3", @@ -5333,11 +5332,6 @@ "node": ">= 0.4" } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6886,14 +6880,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", diff --git a/Client/package.json b/Client/package.json index d4c26e1c5..7ecc398c0 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,10 +14,10 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@hello-pangea/dnd": "^17.0.0", - "@mui/icons-material": "6.4.3", - "@mui/lab": "6.0.0-beta.26", - "@mui/material": "6.4.3", + "@hello-pangea/dnd": "^18.0.0", + "@mui/icons-material": "6.4.4", + "@mui/lab": "6.0.0-beta.27", + "@mui/material": "6.4.4", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.26.0", "@mui/x-date-pickers": "7.26.0", diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx index 7a809065f..ca8e9207c 100644 --- a/Client/src/Components/LanguageSelector.jsx +++ b/Client/src/Components/LanguageSelector.jsx @@ -1,17 +1,16 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Box, MenuItem, Select, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import "flag-icons/css/flag-icons.min.css"; +import { useSelector } from "react-redux"; const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); - const [language, setLanguage] = useState(i18n.language || "gb"); + const { language } = useSelector((state) => state.ui); const handleChange = (event) => { const newLang = event.target.value; - setLanguage(newLang); i18n.changeLanguage(newLang); }; diff --git a/Client/src/Components/Skeletons/FullPage/index.jsx b/Client/src/Components/Skeletons/FullPage/index.jsx new file mode 100644 index 000000000..233fc3c06 --- /dev/null +++ b/Client/src/Components/Skeletons/FullPage/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +export const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index 4565c6c5d..5731307ec 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -23,6 +23,7 @@ const initialState = { greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", distributedUptimeEnabled: false, + language: "gb", }; const uiSlice = createSlice({ @@ -51,6 +52,9 @@ const uiSlice = createSlice({ setTimezone(state, action) { state.timezone = action.payload.timezone; }, + setLanguage(state, action) { + state.language = action.payload; + }, }, }); @@ -62,4 +66,5 @@ export const { setGreeting, setTimezone, setDistributedUptimeEnabled, + setLanguage, } = uiSlice.actions; diff --git a/Client/src/Pages/DistributedUptimeStatus/Status/index.jsx b/Client/src/Pages/DistributedUptimeStatus/Status/index.jsx index 89f97a4d0..ef77b3032 100644 --- a/Client/src/Pages/DistributedUptimeStatus/Status/index.jsx +++ b/Client/src/Pages/DistributedUptimeStatus/Status/index.jsx @@ -75,9 +75,9 @@ const DistributedUptimeStatus = () => { marginY={theme.spacing(4)} color={theme.palette.primary.contrastTextTertiary} > - A public status page is not set up. + A status page is not set up. - Please contact to your administrator + Please contact your administrator ); diff --git a/Client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/Client/src/Pages/Maintenance/MaintenanceTable/index.jsx index 9bdce5887..ea8a1d7a3 100644 --- a/Client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/Client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -34,7 +34,6 @@ const MaintenanceTable = ({ updateCallback, }) => { const { rowsPerPage } = useSelector((state) => state.ui.maintenance); - console.log(rowsPerPage); const dispatch = useDispatch(); const handleChangePage = (event, newPage) => { @@ -175,8 +174,6 @@ const MaintenanceTable = ({ setSort({ field, order }); }; - console.log(handleChangePage); - return ( <> { if (typeof error === "undefined") { const success = await createStatusPage({ form }); if (success) { - createToast({ body: "Status page created successfully" }); + createToast({ + body: isCreate + ? "Status page created successfully" + : "Status page updated successfully", + }); navigate(`/status/uptime/${form.url}`); } return; diff --git a/Client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/Client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx new file mode 100644 index 000000000..746d10f42 --- /dev/null +++ b/Client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -0,0 +1,85 @@ +import DataTable from "../../../../../Components/Table"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import { ColoredLabel } from "../../../../../Components/Label"; +import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import { Stack, Typography } from "@mui/material"; +const StatusPagesTable = ({ data }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const headers = [ + { + id: "name", + content: "Status page name", + render: (row) => { + return row.companyName; + }, + }, + { + id: "url", + content: "URL", + render: (row) => { + return ( + + {`/${row.url}`} + + + ); + }, + }, + { + id: "type", + content: "Type", + render: (row) => { + return row.type; + }, + }, + { + id: "status", + content: "Status", + render: (row) => { + return ( + + ); + }, + }, + ]; + + const handleRowClick = (statusPage) => { + if (statusPage.type === "distributed") { + navigate(`/status/distributed/${statusPage.url}`); + } else if (statusPage.type === "uptime") { + navigate(`/status/uptime/${statusPage.url}`); + } + }; + + return ( + { + handleRowClick(row); + }, + }} + headers={headers} + data={data} + /> + ); +}; + +export default StatusPagesTable; diff --git a/Client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/Client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx index 7565f129a..26eec8104 100644 --- a/Client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ b/Client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx @@ -20,7 +20,9 @@ const useStatusPagesFetch = () => { setStatusPages(res?.data?.data); } catch (error) { setNetworkError(true); - createToast(error.message, "error"); + createToast({ + body: error.message, + }); } finally { setIsLoading(false); } diff --git a/Client/src/Pages/StatusPage/StatusPages/index.jsx b/Client/src/Pages/StatusPage/StatusPages/index.jsx index cf5b83289..9027467a4 100644 --- a/Client/src/Pages/StatusPage/StatusPages/index.jsx +++ b/Client/src/Pages/StatusPage/StatusPages/index.jsx @@ -4,11 +4,12 @@ import Breadcrumbs from "../../../Components/Breadcrumbs"; import Fallback from "../../../Components/Fallback"; import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; import GenericFallback from "../../../Components/GenericFallback"; +import StatusPagesTable from "./Components/StatusPagesTable"; +import SkeletonLayout from "../../../Components/Skeletons/FullPage"; // Utils import { useTheme } from "@emotion/react"; import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useNavigate } from "react-router"; const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; const StatusPages = () => { @@ -16,29 +17,9 @@ const StatusPages = () => { const theme = useTheme(); const isAdmin = useIsAdmin(); const [isLoading, networkError, statusPages] = useStatusPagesFetch(); - const navigate = useNavigate(); - // Handlers - const handleStatusPageClick = (statusPage) => { - if (statusPage.type === "distributed") { - navigate(`/status/distributed/${statusPage.url}`); - } else if (statusPage.type === "uptime") { - navigate(`/status/uptime/${statusPage.url}`); - } - }; - - if (!isLoading && typeof statusPages === "undefined") { - return ( - - ); + if (isLoading) { + return ; } if (networkError === true) { @@ -55,6 +36,21 @@ const StatusPages = () => { ); } + + if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) { + return ( + + ); + } return ( @@ -63,19 +59,7 @@ const StatusPages = () => { isAdmin={isAdmin} path="/status/uptime/create" /> - {statusPages?.map((statusPage) => { - return ( - handleStatusPageClick(statusPage)} - sx={{ cursor: "pointer" }} - > - Company Name: {statusPage.companyName} - Status page URL: {statusPage.url} - Type: {statusPage.type} - - ); - })} + ); }; diff --git a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx index e94af8441..776b66a6f 100644 --- a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx @@ -85,9 +85,9 @@ const ResponseGaugeChart = ({ avgResponseTime }) => { {responseTime} ms diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index ebb726a2c..515ae5930 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -1015,11 +1015,12 @@ class NetworkService { async createStatusPage(config) { const { authToken, user, form, isCreate } = config; + const fd = new FormData(); fd.append("teamId", user.teamId); fd.append("userId", user._id); fd.append("type", form.type); - form.isPublished && fd.append("isPublished", form.isPublished); + form.isPublished !== undefined && fd.append("isPublished", form.isPublished); form.companyName && fd.append("companyName", form.companyName); form.url && fd.append("url", form.url); form.timezone && fd.append("timezone", form.timezone); diff --git a/Client/src/Utils/i18n.js b/Client/src/Utils/i18n.js index f7528f4b5..6243a2599 100644 --- a/Client/src/Utils/i18n.js +++ b/Client/src/Utils/i18n.js @@ -1,5 +1,7 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import { setLanguage } from "../Features/UI/uiSlice"; +import store from "../store"; const primaryLanguage = "gb"; @@ -13,11 +15,12 @@ Object.keys(translations).forEach((path) => { }; }); -const savedLanguage = localStorage.getItem("language") || primaryLanguage; +const savedLanguage = store.getState()?.ui?.language; +const initialLanguage = savedLanguage || primaryLanguage; i18n.use(initReactI18next).init({ resources, - lng: savedLanguage, + lng: initialLanguage, fallbackLng: primaryLanguage, debug: import.meta.env.MODE === "development", ns: ["translation"], @@ -28,7 +31,7 @@ i18n.use(initReactI18next).init({ }); i18n.on("languageChanged", (lng) => { - localStorage.setItem("language", lng); + store.dispatch(setLanguage(lng)); }); export default i18n; diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 0ffa612f3..94952e3c7 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -8,7 +8,6 @@ import { newPasswordValidation, } from "../validation/joi.js"; import logger from "../utils/logger.js"; -import { errorMessages, successMessages } from "../utils/messages.js"; import jwt from "jsonwebtoken"; import { getTokenFromHeaders, tokenType } from "../utils/utils.js"; import crypto from "crypto"; @@ -16,11 +15,12 @@ import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "authController"; class AuthController { - constructor(db, settingsService, emailService, jobQueue) { + constructor(db, settingsService, emailService, jobQueue, stringService) { this.db = db; this.settingsService = settingsService; this.emailService = emailService; this.jobQueue = jobQueue; + this.stringService = stringService; } /** @@ -85,7 +85,7 @@ class AuthController { const newUser = await this.db.insertUser({ ...req.body }, req.file); logger.info({ - message: successMessages.AUTH_CREATE_USER, + message: this.stringService.authCreateUser, service: SERVICE_NAME, details: newUser._id, }); @@ -116,7 +116,7 @@ class AuthController { }); res.success({ - msg: successMessages.AUTH_CREATE_USER, + msg: this.stringService.authCreateUser, data: { user: newUser, token: token, refreshToken: refreshToken }, }); } catch (error) { @@ -153,7 +153,7 @@ class AuthController { // Compare password const match = await user.comparePassword(password); if (match !== true) { - const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD); + const error = new Error(this.stringService.authIncorrectPassword); error.status = 401; next(error); return; @@ -176,7 +176,7 @@ class AuthController { userWithoutPassword.avatarImage = user.avatarImage; return res.success({ - msg: successMessages.AUTH_LOGIN_USER, + msg: this.stringService.authLoginUser, data: { user: userWithoutPassword, token: token, @@ -200,13 +200,14 @@ class AuthController { * @throws {Error} If there is an error during the process such as any of the token is not received */ refreshAuthToken = async (req, res, next) => { + try { // check for refreshToken const refreshToken = req.headers["x-refresh-token"]; if (!refreshToken) { // No refresh token provided - const error = new Error(errorMessages.NO_REFRESH_TOKEN); + const error = new Error(this.stringService.noRefreshToken); error.status = 401; error.service = SERVICE_NAME; error.method = "refreshAuthToken"; @@ -221,8 +222,8 @@ class AuthController { // Invalid or expired refresh token, trigger logout const errorMessage = refreshErr.name === "TokenExpiredError" - ? errorMessages.EXPIRED_REFRESH_TOKEN - : errorMessages.INVALID_REFRESH_TOKEN; + ? this.stringService.expiredAuthToken + : this.stringService.invalidAuthToken; const error = new Error(errorMessage); error.status = 401; error.service = SERVICE_NAME; @@ -243,7 +244,7 @@ class AuthController { ); return res.success({ - msg: successMessages.AUTH_TOKEN_REFRESHED, + msg: this.stringService.authTokenRefreshed, data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken }, }); } catch (error) { @@ -265,6 +266,7 @@ class AuthController { * @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403). */ editUser = async (req, res, next) => { + try { await editUserParamValidation.validateAsync(req.params); await editUserBodyValidation.validateAsync(req.body); @@ -276,7 +278,7 @@ class AuthController { // TODO is this neccessary any longer? Verify ownership middleware should handle this if (req.params.userId !== req.user._id.toString()) { - const error = new Error(errorMessages.AUTH_UNAUTHORIZED); + const error = new Error(this.stringService.unauthorized); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -300,7 +302,7 @@ class AuthController { // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { - const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD); + const error = new Error(this.stringService.authIncorrectPassword); error.status = 403; next(error); return; @@ -311,7 +313,7 @@ class AuthController { const updatedUser = await this.db.updateUser(req, res); res.success({ - msg: successMessages.AUTH_UPDATE_USER, + msg: this.stringService.authUpdateUser, data: updatedUser, }); } catch (error) { @@ -333,7 +335,7 @@ class AuthController { const superAdminExists = await this.db.checkSuperadmin(req, res); return res.success({ - msg: successMessages.AUTH_ADMIN_EXISTS, + msg: this.stringService.authAdminExists, data: superAdminExists, }); } catch (error) { @@ -379,7 +381,7 @@ class AuthController { ); return res.success({ - msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, + msg: this.stringService.authCreateRecoveryToken, data: msgId, }); } catch (error) { @@ -410,7 +412,7 @@ class AuthController { await this.db.validateRecoveryToken(req, res); return res.success({ - msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, + msg: this.stringService.authVerifyRecoveryToken, }); } catch (error) { next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); @@ -443,7 +445,7 @@ class AuthController { const token = this.issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings); return res.success({ - msg: successMessages.AUTH_RESET_PASSWORD, + msg: this.stringService.authResetPassword, data: { user, token }, }); } catch (error) { @@ -497,7 +499,7 @@ class AuthController { await this.db.deleteUser(user._id); return res.success({ - msg: successMessages.AUTH_DELETE_USER, + msg: this.stringService.authDeleteUser, }); } catch (error) { next(handleError(error, SERVICE_NAME, "deleteUserController")); @@ -509,7 +511,7 @@ class AuthController { const allUsers = await this.db.getAllUsers(req, res); return res.success({ - msg: successMessages.AUTH_GET_ALL_USERS, + msg: this.stringService.authGetAllUsers, data: allUsers, }); } catch (error) { diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index ae6b96dbd..2addd7359 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -9,7 +9,6 @@ import { deleteChecksByTeamIdParamValidation, updateChecksTTLBodyValidation, } from "../validation/joi.js"; -import { successMessages } from "../utils/messages.js"; import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; import { handleValidationError, handleError } from "./controllerUtils.js"; @@ -17,9 +16,10 @@ import { handleValidationError, handleError } from "./controllerUtils.js"; const SERVICE_NAME = "checkController"; class CheckController { - constructor(db, settingsService) { + constructor(db, settingsService, stringService) { this.db = db; this.settingsService = settingsService; + this.stringService = stringService; } createCheck = async (req, res, next) => { @@ -36,7 +36,7 @@ class CheckController { const check = await this.db.createCheck(checkData); return res.success({ - msg: successMessages.CHECK_CREATE, + msg: this.stringService.checkCreate, data: check, }); } catch (error) { @@ -57,7 +57,7 @@ class CheckController { const result = await this.db.getChecksByMonitor(req); return res.success({ - msg: successMessages.CHECK_GET, + msg: this.stringService.checkGet, data: result, }); } catch (error) { @@ -77,7 +77,7 @@ class CheckController { const checkData = await this.db.getChecksByTeam(req); return res.success({ - msg: successMessages.CHECK_GET, + msg: this.stringService.checkGet, data: checkData, }); } catch (error) { @@ -97,7 +97,7 @@ class CheckController { const deletedCount = await this.db.deleteChecks(req.params.monitorId); return res.success({ - msg: successMessages.CHECK_DELETE, + msg: this.stringService.checkDelete, data: { deletedCount }, }); } catch (error) { @@ -117,7 +117,7 @@ class CheckController { const deletedCount = await this.db.deleteChecksByTeamId(req.params.teamId); return res.success({ - msg: successMessages.CHECK_DELETE, + msg: this.stringService.checkDelete, data: { deletedCount }, }); } catch (error) { @@ -144,7 +144,7 @@ class CheckController { await this.db.updateChecksTTL(teamId, ttl); return res.success({ - msg: successMessages.CHECK_UPDATE_TTL, + msg: this.stringService.checkUpdateTTL, }); } catch (error) { next(handleError(error, SERVICE_NAME, "updateTTL")); diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js index a40f43a32..40d045432 100644 --- a/Server/controllers/inviteController.js +++ b/Server/controllers/inviteController.js @@ -7,14 +7,15 @@ import logger from "../utils/logger.js"; import jwt from "jsonwebtoken"; import { handleError, handleValidationError } from "./controllerUtils.js"; import { getTokenFromHeaders } from "../utils/utils.js"; -import { successMessages } from "../utils/messages.js"; + const SERVICE_NAME = "inviteController"; class InviteController { - constructor(db, settingsService, emailService) { + constructor(db, settingsService, emailService, stringService) { this.db = db; this.settingsService = settingsService; this.emailService = emailService; + this.stringService = stringService; } /** @@ -66,7 +67,7 @@ class InviteController { }); return res.success({ - msg: successMessages.INVITE_ISSUED, + msg: this.stringService.inviteIssued, data: inviteToken, }); } catch (error) { @@ -86,7 +87,7 @@ class InviteController { const invite = await this.db.getInviteToken(req.body.token); return res.success({ - msg: successMessages.INVITE_VERIFIED, + msg: this.stringService.inviteVerified, data: invite, }); } catch (error) { diff --git a/Server/controllers/maintenanceWindowController.js b/Server/controllers/maintenanceWindowController.js index b301ba94c..a11f957ed 100644 --- a/Server/controllers/maintenanceWindowController.js +++ b/Server/controllers/maintenanceWindowController.js @@ -9,14 +9,15 @@ import { } from "../validation/joi.js"; import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; -import { successMessages } from "../utils/messages.js"; import { handleValidationError, handleError } from "./controllerUtils.js"; + const SERVICE_NAME = "maintenanceWindowController"; class MaintenanceWindowController { - constructor(db, settingsService) { + constructor(db, settingsService, stringService) { this.db = db; this.settingsService = settingsService; + this.stringService = stringService; } createMaintenanceWindows = async (req, res, next) => { @@ -45,7 +46,7 @@ class MaintenanceWindowController { await Promise.all(dbTransactions); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_CREATE, + msg: this.stringService.maintenanceWindowCreate, }); } catch (error) { next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); @@ -63,7 +64,7 @@ class MaintenanceWindowController { const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID, + msg: this.stringService.maintenanceWindowGetById, data: maintenanceWindow, }); } catch (error) { @@ -89,7 +90,7 @@ class MaintenanceWindowController { ); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM, + msg: this.stringService.maintenanceWindowGetByTeam, data: maintenanceWindows, }); } catch (error) { @@ -111,7 +112,7 @@ class MaintenanceWindowController { ); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER, + msg: this.stringService.maintenanceWindowGetByUser, data: maintenanceWindows, }); } catch (error) { @@ -129,7 +130,7 @@ class MaintenanceWindowController { try { await this.db.deleteMaintenanceWindowById(req.params.id); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_DELETE, + msg: this.stringService.maintenanceWindowDelete, }); } catch (error) { next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); @@ -150,7 +151,7 @@ class MaintenanceWindowController { req.body ); return res.success({ - msg: successMessages.MAINTENANCE_WINDOW_EDIT, + msg: this.stringService.maintenanceWindowEdit, data: editedMaintenanceWindow, }); } catch (error) { diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index ccd8ec26f..f65113cd7 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -14,7 +14,6 @@ import { getHardwareDetailsByIdQueryValidation, } from "../validation/joi.js"; import sslChecker from "ssl-checker"; -import { successMessages } from "../utils/messages.js"; import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; import logger from "../utils/logger.js"; @@ -24,10 +23,11 @@ import seedDb from "../db/mongo/utils/seedDb.js"; const SERVICE_NAME = "monitorController"; class MonitorController { - constructor(db, settingsService, jobQueue) { + constructor(db, settingsService, jobQueue, stringService) { this.db = db; this.settingsService = settingsService; this.jobQueue = jobQueue; + this.stringService = stringService; } /** @@ -43,7 +43,7 @@ class MonitorController { try { const monitors = await this.db.getAllMonitors(); return res.success({ - msg: successMessages.MONITOR_GET_ALL, + msg: this.stringService.monitorGetAll, data: monitors, }); } catch (error) { @@ -64,7 +64,7 @@ class MonitorController { try { const monitors = await this.db.getAllMonitorsWithUptimeStats(); return res.success({ - msg: successMessages.MONITOR_GET_ALL, + msg: this.stringService.monitorGetAll, data: monitors, }); } catch (error) { @@ -76,7 +76,7 @@ class MonitorController { try { const monitor = await this.db.getUptimeDetailsById(req); return res.success({ - msg: successMessages.MONITOR_GET_BY_ID, + msg: this.stringService.monitorGetById, data: monitor, }); } catch (error) { @@ -105,7 +105,7 @@ class MonitorController { try { const monitorStats = await this.db.getMonitorStatsById(req); return res.success({ - msg: successMessages.MONITOR_STATS_BY_ID, + msg: this.stringService.monitorStatsById, data: monitorStats, }); } catch (error) { @@ -133,7 +133,7 @@ class MonitorController { try { const monitor = await this.db.getHardwareDetailsById(req); return res.success({ - msg: successMessages.MONITOR_GET_BY_ID, + msg: this.stringService.monitorGetById, data: monitor, }); } catch (error) { @@ -154,7 +154,7 @@ class MonitorController { const certificate = await fetchMonitorCertificate(sslChecker, monitor); return res.success({ - msg: successMessages.MONITOR_CERTIFICATE, + msg: this.stringService.monitorCertificate, data: { certificateDate: new Date(certificate.validTo), }, @@ -187,7 +187,7 @@ class MonitorController { try { const monitor = await this.db.getMonitorById(req.params.monitorId); return res.success({ - msg: successMessages.MONITOR_GET_BY_ID, + msg: this.stringService.monitorGetById, data: monitor, }); } catch (error) { @@ -231,7 +231,7 @@ class MonitorController { // Add monitor to job queue this.jobQueue.addJob(monitor._id, monitor); return res.success({ - msg: successMessages.MONITOR_CREATE, + msg: this.stringService.monitorCreate, data: monitor, }); } catch (error) { @@ -309,7 +309,7 @@ class MonitorController { stack: error.stack, }); } - return res.success({ msg: successMessages.MONITOR_DELETE }); + return res.success({ msg: this.stringService.monitorDelete }); } catch (error) { next(handleError(error, SERVICE_NAME, "deleteMonitor")); } @@ -390,10 +390,10 @@ class MonitorController { await Promise.all( notifications && - notifications.map(async (notification) => { - notification.monitorId = editedMonitor._id; - await this.db.createNotification(notification); - }) + notifications.map(async (notification) => { + notification.monitorId = editedMonitor._id; + await this.db.createNotification(notification); + }) ); // Delete the old job(editedMonitor has the same ID as the old monitor) @@ -401,7 +401,7 @@ class MonitorController { // Add the new job back to the queue await this.jobQueue.addJob(editedMonitor._id, editedMonitor); return res.success({ - msg: successMessages.MONITOR_EDIT, + msg: this.stringService.monitorEdit, data: editedMonitor, }); } catch (error) { @@ -438,8 +438,8 @@ class MonitorController { monitor.save(); return res.success({ msg: monitor.isActive - ? successMessages.MONITOR_RESUME - : successMessages.MONITOR_PAUSE, + ? this.stringService.monitorResume + : this.stringService.monitorPause, data: monitor, }); } catch (error) { @@ -469,7 +469,7 @@ class MonitorController { ); return res.success({ - msg: successMessages.MONITOR_DEMO_ADDED, + msg: this.stringService.monitorDemoAdded, data: demoMonitors.length, }); } catch (error) { @@ -488,7 +488,7 @@ class MonitorController { try { const monitors = await this.db.getMonitorsByTeamId(req); return res.success({ - msg: successMessages.MONITOR_GET_BY_TEAM_ID, + msg: this.stringService.monitorGetByTeamId, data: monitors, }); } catch (error) { diff --git a/Server/controllers/queueController.js b/Server/controllers/queueController.js index f62b9fdeb..330ade532 100644 --- a/Server/controllers/queueController.js +++ b/Server/controllers/queueController.js @@ -1,18 +1,18 @@ import { handleError } from "./controllerUtils.js"; -import { successMessages } from "../utils/messages.js"; const SERVICE_NAME = "JobQueueController"; class JobQueueController { - constructor(jobQueue) { + constructor(jobQueue, stringService) { this.jobQueue = jobQueue; + this.stringService = stringService; } getMetrics = async (req, res, next) => { try { const metrics = await this.jobQueue.getMetrics(); res.success({ - msg: successMessages.QUEUE_GET_METRICS, + msg: this.stringService.queueGetMetrics, data: metrics, }); } catch (error) { @@ -25,7 +25,7 @@ class JobQueueController { try { const jobs = await this.jobQueue.getJobStats(); return res.success({ - msg: successMessages.QUEUE_GET_METRICS, + msg: this.stringService.queueGetMetrics, data: jobs, }); } catch (error) { @@ -38,7 +38,7 @@ class JobQueueController { try { await this.jobQueue.addJob(Math.random().toString(36).substring(7)); return res.success({ - msg: successMessages.QUEUE_ADD_JOB, + msg: this.stringService.queueAddJob, }); } catch (error) { next(handleError(error, SERVICE_NAME, "addJob")); @@ -50,7 +50,7 @@ class JobQueueController { try { await this.jobQueue.obliterate(); return res.success({ - msg: successMessages.QUEUE_OBLITERATE, + msg: this.stringService.queueObliterate, }); } catch (error) { next(handleError(error, SERVICE_NAME, "obliterateQueue")); diff --git a/Server/controllers/settingsController.js b/Server/controllers/settingsController.js index 496ed057a..54643dfbb 100644 --- a/Server/controllers/settingsController.js +++ b/Server/controllers/settingsController.js @@ -1,12 +1,13 @@ -import { successMessages } from "../utils/messages.js"; import { updateAppSettingsBodyValidation } from "../validation/joi.js"; import { handleValidationError, handleError } from "./controllerUtils.js"; + const SERVICE_NAME = "SettingsController"; class SettingsController { - constructor(db, settingsService) { + constructor(db, settingsService, stringService) { this.db = db; this.settingsService = settingsService; + this.stringService = stringService; } getAppSettings = async (req, res, next) => { @@ -14,7 +15,7 @@ class SettingsController { const settings = { ...(await this.settingsService.getSettings()) }; delete settings.jwtSecret; return res.success({ - msg: successMessages.GET_APP_SETTINGS, + msg: this.stringService.getAppSettings, data: settings, }); } catch (error) { @@ -35,7 +36,7 @@ class SettingsController { const updatedSettings = { ...(await this.settingsService.reloadSettings()) }; delete updatedSettings.jwtSecret; return res.success({ - msg: successMessages.UPDATE_APP_SETTINGS, + msg: this.stringService.updateAppSettings, data: updatedSettings, }); } catch (error) { diff --git a/Server/controllers/statusPageController.js b/Server/controllers/statusPageController.js index 1a69c92ec..eb3e69484 100644 --- a/Server/controllers/statusPageController.js +++ b/Server/controllers/statusPageController.js @@ -5,13 +5,13 @@ import { getStatusPageQueryValidation, imageValidation, } from "../validation/joi.js"; -import { successMessages, errorMessages } from "../utils/messages.js"; const SERVICE_NAME = "statusPageController"; class StatusPageController { - constructor(db) { + constructor(db, stringService) { this.db = db; + this.stringService = stringService; } createStatusPage = async (req, res, next) => { @@ -26,7 +26,7 @@ class StatusPageController { try { const statusPage = await this.db.createStatusPage(req.body, req.file); return res.success({ - msg: successMessages.STATUS_PAGE_CREATE, + msg: this.stringService.statusPageCreate, data: statusPage, }); } catch (error) { @@ -46,12 +46,12 @@ class StatusPageController { try { const statusPage = await this.db.updateStatusPage(req.body, req.file); if (statusPage === null) { - const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND); + const error = new Error(this.stringService.statusPageNotFound); error.status = 404; throw error; } return res.success({ - msg: successMessages.STATUS_PAGE_UPDATE, + msg: this.stringService.statusPageUpdate, data: statusPage, }); } catch (error) { @@ -63,7 +63,7 @@ class StatusPageController { try { const statusPage = await this.db.getStatusPage(); return res.success({ - msg: successMessages.STATUS_PAGE, + msg: this.stringService.statusPageByUrl, data: statusPage, }); } catch (error) { @@ -83,7 +83,7 @@ class StatusPageController { try { const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type); return res.success({ - msg: successMessages.STATUS_PAGE_BY_URL, + msg: this.stringService.statusPageByUrl, data: statusPage, }); } catch (error) { @@ -95,8 +95,9 @@ class StatusPageController { try { const teamId = req.params.teamId; const statusPages = await this.db.getStatusPagesByTeamId(teamId); + return res.success({ - msg: successMessages.STATUS_PAGE_BY_TEAM_ID, + msg: this.stringService.statusPageByTeamId, data: statusPages, }); } catch (error) { @@ -108,7 +109,7 @@ class StatusPageController { try { await this.db.deleteStatusPage(req.params.url); return res.success({ - msg: successMessages.STATUS_PAGE_DELETE, + msg: this.stringService.statusPageDelete, }); } catch (error) { next(handleError(error, SERVICE_NAME, "deleteStatusPage")); diff --git a/Server/db/models/Monitor.js b/Server/db/models/Monitor.js index 90cbb1d62..eb5f67c1b 100644 --- a/Server/db/models/Monitor.js +++ b/Server/db/models/Monitor.js @@ -38,6 +38,20 @@ const MonitorSchema = mongoose.Schema( "distributed_http", ], }, + jsonPath: { + type: String, + }, + expectedValue: { + type: String, + }, + matchMethod: { + type: String, + enum: [ + "equal", + "include", + "regex", + ], + }, url: { type: String, required: true, diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index 0cbf0ab04..f5c960697 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -1,6 +1,7 @@ import InviteToken from "../../models/InviteToken.js"; import crypto from "crypto"; -import { errorMessages } from "../../../utils/messages.js"; +import ServiceRegistry from "../../../service/serviceRegistry.js"; +import StringService from "../../../service/stringService.js"; const SERVICE_NAME = "inviteModule"; /** @@ -42,12 +43,13 @@ const requestInviteToken = async (userData) => { * @throws {Error} If the invite token is not found or there is another error. */ const getInviteToken = async (token) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const invite = await InviteToken.findOne({ token, }); if (invite === null) { - throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND); + throw new Error(stringService.authInviteNotFound); } return invite; } catch (error) { @@ -68,12 +70,13 @@ const getInviteToken = async (token) => { * @throws {Error} If the invite token is not found or there is another error. */ const getInviteTokenAndDelete = async (token) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const invite = await InviteToken.findOneAndDelete({ token, }); if (invite === null) { - throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND); + throw new Error(stringService.authInviteNotFound); } return invite; } catch (error) { diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 9a8e98f4e..e1db09f0a 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -3,9 +3,10 @@ import Check from "../../models/Check.js"; import PageSpeedCheck from "../../models/PageSpeedCheck.js"; import HardwareCheck from "../../models/HardwareCheck.js"; import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js"; -import { errorMessages } from "../../../utils/messages.js"; import Notification from "../../models/Notification.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js"; +import ServiceRegistry from "../../../service/serviceRegistry.js"; +import StringService from "../../../service/stringService.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; @@ -312,7 +313,7 @@ const calculateGroupStats = (group) => { avgResponseTime: checksWithResponseTime.length > 0 ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / - checksWithResponseTime.length + checksWithResponseTime.length : 0, }; }; @@ -326,11 +327,12 @@ const calculateGroupStats = (group) => { * @throws {Error} */ const getUptimeDetailsById = async (req) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const { monitorId } = req.params; const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { - throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + throw new Error(stringService.dbFindMonitorById(monitorId)); } const { dateRange, normalize } = req.query; @@ -373,7 +375,7 @@ const getDistributedUptimeDetailsById = async (req) => { const { monitorId } = req.params; const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { - throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + throw new Error(this.stringService.dbFindMonitorById(monitorId)); } const { dateRange, normalize } = req.query; @@ -419,13 +421,14 @@ const getDistributedUptimeDetailsById = async (req) => { * @throws {Error} */ const getMonitorStatsById = async (req) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const { monitorId } = req.params; // Get monitor, if we can't find it, abort with error const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { - throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + throw new Error(stringService.getDbFindMonitorById(monitorId)); } // Get query params @@ -516,10 +519,11 @@ const getHardwareDetailsById = async (req) => { * @throws {Error} */ const getMonitorById = async (monitorId) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { - const error = new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + const error = new Error(stringService.getDbFindMonitorById(monitorId)); error.status = 404; throw error; } @@ -601,98 +605,98 @@ const getMonitorsByTeamId = async (req) => { filteredMonitors: [ ...(filter !== undefined ? [ - { - $match: { - $or: [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ], - }, + { + $match: { + $or: [ + { name: { $regex: filter, $options: "i" } }, + { url: { $regex: filter, $options: "i" } }, + ], }, - ] + }, + ] : []), { $sort: sort }, { $skip: skip }, ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), ...(limit ? [ - { - $lookup: { - from: "checks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, + { + $lookup: { + from: "checks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "standardchecks", - }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "standardchecks", }, - ] + }, + ] : []), ...(limit ? [ - { - $lookup: { - from: "pagespeedchecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, + { + $lookup: { + from: "pagespeedchecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "pagespeedchecks", - }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "pagespeedchecks", }, - ] + }, + ] : []), ...(limit ? [ - { - $lookup: { - from: "hardwarechecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, + { + $lookup: { + from: "hardwarechecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "hardwarechecks", - }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "hardwarechecks", }, - ] + }, + ] : []), ...(limit ? [ - { - $lookup: { - from: "distributeduptimechecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, + { + $lookup: { + from: "distributeduptimechecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "distributeduptimechecks", - }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "distributeduptimechecks", }, - ] + }, + ] : []), { @@ -783,11 +787,13 @@ const createMonitor = async (req, res) => { * @throws {Error} */ const deleteMonitor = async (req, res) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + const monitorId = req.params.monitorId; try { const monitor = await Monitor.findByIdAndDelete(monitorId); if (!monitor) { - throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); + throw new Error(stringService.getDbFindMonitorById(monitorId)); } return monitor; } catch (error) { diff --git a/Server/db/mongo/modules/recoveryModule.js b/Server/db/mongo/modules/recoveryModule.js index 40dd66c56..3b39e847c 100644 --- a/Server/db/mongo/modules/recoveryModule.js +++ b/Server/db/mongo/modules/recoveryModule.js @@ -1,7 +1,8 @@ import UserModel from "../../models/User.js"; import RecoveryToken from "../../models/RecoveryToken.js"; import crypto from "crypto"; -import { errorMessages } from "../../../utils/messages.js"; +import serviceRegistry from "../../../service/serviceRegistry.js"; +import StringService from "../../../service/stringService.js"; const SERVICE_NAME = "recoveryModule"; @@ -31,6 +32,7 @@ const requestRecoveryToken = async (req, res) => { }; const validateRecoveryToken = async (req, res) => { + const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { const candidateToken = req.body.recoveryToken; const recoveryToken = await RecoveryToken.findOne({ @@ -39,7 +41,7 @@ const validateRecoveryToken = async (req, res) => { if (recoveryToken !== null) { return recoveryToken; } else { - throw new Error(errorMessages.DB_TOKEN_NOT_FOUND); + throw new Error(stringService.dbTokenNotFound); } } catch (error) { error.service = SERVICE_NAME; @@ -49,6 +51,7 @@ const validateRecoveryToken = async (req, res) => { }; const resetPassword = async (req, res) => { + const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { const newPassword = req.body.password; @@ -57,12 +60,12 @@ const resetPassword = async (req, res) => { const user = await UserModel.findOne({ email: recoveryToken.email }); if (user === null) { - throw new Error(errorMessages.DB_USER_NOT_FOUND); + throw new Error(stringService.dbUserNotFound); } const match = await user.comparePassword(newPassword); if (match === true) { - throw new Error(errorMessages.DB_RESET_PASSWORD_BAD_MATCH); + throw new Error(stringService.dbResetPasswordBadMatch); } user.password = newPassword; diff --git a/Server/db/mongo/modules/statusPageModule.js b/Server/db/mongo/modules/statusPageModule.js index 8687f06ed..aa32e49f5 100644 --- a/Server/db/mongo/modules/statusPageModule.js +++ b/Server/db/mongo/modules/statusPageModule.js @@ -1,10 +1,13 @@ import StatusPage from "../../models/StatusPage.js"; -import { errorMessages } from "../../../utils/messages.js"; import { NormalizeData } from "../../../utils/dataUtils.js"; +import ServiceRegistry from "../../../service/serviceRegistry.js"; +import StringService from "../../../service/stringService.js"; const SERVICE_NAME = "statusPageModule"; const createStatusPage = async (statusPageData, image) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + try { const statusPage = new StatusPage({ ...statusPageData }); if (image) { @@ -19,7 +22,7 @@ const createStatusPage = async (statusPageData, image) => { if (error?.code === 11000) { // Handle duplicate URL errors error.status = 400; - error.message = errorMessages.STATUS_PAGE_URL_NOT_UNIQUE; + error.message = stringService.statusPageUrlNotUnique; } error.service = SERVICE_NAME; error.method = "createStatusPage"; @@ -67,13 +70,10 @@ const getStatusPageByUrl = async (url, type) => { }; const getStatusPagesByTeamId = async (teamId) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + try { const statusPages = await StatusPage.find({ teamId }); - if (statusPages.length === 0) { - const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND); - error.status = 404; - throw error; - } return statusPages; } catch (error) { error.service = SERVICE_NAME; @@ -83,6 +83,8 @@ const getStatusPagesByTeamId = async (teamId) => { }; const getStatusPage = async (url) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + try { const statusPageQuery = await StatusPage.aggregate([ { $match: { url: url } }, @@ -156,7 +158,7 @@ const getStatusPage = async (url) => { }, ]); if (!statusPageQuery.length) { - const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND); + const error = new Error(stringService.statusPageNotFound); error.status = 404; throw error; } diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index 5b951ff64..4069be8c6 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -1,10 +1,11 @@ import UserModel from "../../models/User.js"; import TeamModel from "../../models/Team.js"; -import { errorMessages } from "../../../utils/messages.js"; import { GenerateAvatarImage } from "../../../utils/imageProcessing.js"; const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key import { ParseBoolean } from "../../../utils/utils.js"; +import ServiceRegistry from "../../../service/serviceRegistry.js"; +import StringService from "../../../service/stringService.js"; const SERVICE_NAME = "userModule"; /** @@ -20,6 +21,7 @@ const insertUser = async ( imageFile, generateAvatarImage = GenerateAvatarImage ) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { if (imageFile) { // 1. Save the full size image @@ -50,7 +52,7 @@ const insertUser = async ( .select("-profileImage"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { - error.message = errorMessages.DB_USER_EXISTS; + error.message = stringService.dbUserExists; } error.service = SERVICE_NAME; error.method = "insertUser"; @@ -70,12 +72,14 @@ const insertUser = async ( * @throws {Error} */ const getUserByEmail = async (email) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + try { // Need the password to be able to compare, removed .select() // We can strip the hash before returning the user const user = await UserModel.findOne({ email: email }).select("-profileImage"); if (!user) { - throw new Error(errorMessages.DB_USER_NOT_FOUND); + throw new Error(stringService.dbUserNotFound); } return user; } catch (error) { @@ -150,10 +154,12 @@ const updateUser = async ( * @throws {Error} */ const deleteUser = async (userId) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + try { const deletedUser = await UserModel.findByIdAndDelete(userId); if (!deletedUser) { - throw new Error(errorMessages.DB_USER_NOT_FOUND); + throw new Error(stringService.dbUserNotFound); } return deletedUser; } catch (error) { diff --git a/Server/index.js b/Server/index.js index 8fb58481c..090498de2 100644 --- a/Server/index.js +++ b/Server/index.js @@ -78,6 +78,10 @@ import MongoDB from "./db/mongo/MongoDB.js"; import IORedis from "ioredis"; +import TranslationService from './service/translationService.js'; +import languageMiddleware from './middleware/languageMiddleware.js'; +import StringService from './service/stringService.js'; + const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; @@ -160,11 +164,17 @@ const startApp = async () => { } } + // Create and Register Primary services + const translationService = new TranslationService(logger); + const stringService = new StringService(translationService); + ServiceRegistry.register(StringService.SERVICE_NAME, stringService); + // Create DB const db = new MongoDB(); await db.connect(); // Create services + const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService); const settingsService = new SettingsService(AppSettings); await settingsService.loadSettings(); const emailService = new EmailService( @@ -176,16 +186,17 @@ const startApp = async () => { nodemailer, logger ); - const networkService = new NetworkService(axios, ping, logger, http, Docker, net); const statusService = new StatusService(db, logger); const notificationService = new NotificationService(emailService, db, logger, networkService); + const jobQueue = new JobQueue( db, statusService, networkService, notificationService, settingsService, + stringService, logger, Queue, Worker @@ -199,6 +210,10 @@ const startApp = async () => { ServiceRegistry.register(NetworkService.SERVICE_NAME, networkService); ServiceRegistry.register(StatusService.SERVICE_NAME, statusService); ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService); + ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService); + + await translationService.initialize(); + server = app.listen(PORT, () => { logger.info({ message: `server started on port:${PORT}` }); }); @@ -212,40 +227,50 @@ const startApp = async () => { ServiceRegistry.get(MongoDB.SERVICE_NAME), ServiceRegistry.get(SettingsService.SERVICE_NAME), ServiceRegistry.get(EmailService.SERVICE_NAME), - ServiceRegistry.get(JobQueue.SERVICE_NAME) + ServiceRegistry.get(JobQueue.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const monitorController = new MonitorController( ServiceRegistry.get(MongoDB.SERVICE_NAME), ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(JobQueue.SERVICE_NAME) + ServiceRegistry.get(JobQueue.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const settingsController = new SettingsController( ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME) + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const checkController = new CheckController( ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME) + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const inviteController = new InviteController( ServiceRegistry.get(MongoDB.SERVICE_NAME), ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME) + ServiceRegistry.get(EmailService.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const maintenanceWindowController = new MaintenanceWindowController( ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME) + ServiceRegistry.get(SettingsService.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); - const queueController = new QueueController(ServiceRegistry.get(JobQueue.SERVICE_NAME)); + const queueController = new QueueController( + ServiceRegistry.get(JobQueue.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) + ); const statusPageController = new StatusPageController( - ServiceRegistry.get(MongoDB.SERVICE_NAME) + ServiceRegistry.get(MongoDB.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const notificationController = new NotificationController( @@ -278,12 +303,10 @@ const startApp = async () => { // Init job queue await jobQueue.initJobQueue(); // Middleware - app.use( - cors() - //We will add configuration later - ); + app.use(cors()); app.use(express.json()); app.use(helmet()); + app.use(languageMiddleware(stringService, translationService)); // Swagger UI app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); diff --git a/Server/locales/en.json b/Server/locales/en.json new file mode 100644 index 000000000..8657deb73 --- /dev/null +++ b/Server/locales/en.json @@ -0,0 +1,154 @@ +{ + "dontHaveAccount": "Don't have account", + "email": "E-mail", + "forgotPassword": "Forgot Password", + "password": "password", + "signUp": "Sign up", + "submit": "Submit", + "title": "Title", + "continue": "Continue", + "enterEmail": "Enter your email", + "authLoginTitle": "Log In", + "authLoginEnterPassword": "Enter your password", + "commonPassword": "Password", + "commonBack": "Back", + "authForgotPasswordTitle": "Forgot password?", + "authForgotPasswordResetPassword": "Reset password", + "createPassword": "Create your password", + "createAPassword": "Create a password", + "authRegisterAlreadyHaveAccount": "Already have an account?", + "commonAppName": "BlueWave Uptime", + "authLoginEnterEmail": "Enter your email", + "authRegisterTitle": "Create an account", + "authRegisterStepOneTitle": "Create your account", + "authRegisterStepOneDescription": "Enter your details to get started", + "authRegisterStepTwoTitle": "Set up your profile", + "authRegisterStepTwoDescription": "Tell us more about yourself", + "authRegisterStepThreeTitle": "Almost done!", + "authRegisterStepThreeDescription": "Review your information", + "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", + "authForgotPasswordSendInstructions": "Send instructions", + "authForgotPasswordBackTo": "Back to", + "authCheckEmailTitle": "Check your email", + "authCheckEmailDescription": "We sent a password reset link to {{email}}", + "authCheckEmailResendEmail": "Resend email", + "authCheckEmailBackTo": "Back to", + "goBackTo": "Go back to", + "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", + "authCheckEmailClickToResend": "Click to resend", + "authSetNewPasswordTitle": "Set new password", + "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", + "authSetNewPasswordNewPassword": "New password", + "authSetNewPasswordConfirmPassword": "Confirm password", + "confirmPassword": "Confirm your password", + "authSetNewPasswordResetPassword": "Reset password", + "authSetNewPasswordBackTo": "Back to", + "authPasswordMustBeAtLeast": "Must be at least", + "authPasswordCharactersLong": "8 characters long", + "authPasswordMustContainAtLeast": "Must contain at least", + "authPasswordSpecialCharacter": "one special character", + "authPasswordOneNumber": "one number", + "authPasswordUpperCharacter": "one upper character", + "authPasswordLowerCharacter": "one lower character", + "authPasswordConfirmAndPassword": "Confirm password and password", + "authPasswordMustMatch": "must match", + "friendlyError": "Something went wrong...", + "unknownError": "An unknown error occurred", + "unauthorized": "Unauthorized access", + "authAdminExists": "Admin already exists", + "authInviteNotFound": "Invite not found", + "unknownService": "Unknown service", + "noAuthToken": "No auth token provided", + "invalidAuthToken": "Invalid auth token", + "expiredAuthToken": "Token expired", + "noRefreshToken": "No refresh token provided", + "invalidRefreshToken": "Invalid refresh token", + "expiredRefreshToken": "Refresh token expired", + "requestNewAccessToken": "Request new access token", + "invalidPayload": "Invalid payload", + "verifyOwnerNotFound": "Document not found", + "verifyOwnerUnauthorized": "Unauthorized access", + "insufficientPermissions": "Insufficient permissions", + "dbUserExists": "User already exists", + "dbUserNotFound": "User not found", + "dbTokenNotFound": "Token not found", + "dbResetPasswordBadMatch": "New password must be different from old password", + "dbFindMonitorById": "Monitor with id ${monitorId} not found", + "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", + "authIncorrectPassword": "Incorrect password", + "authUnauthorized": "Unauthorized access", + "monitorGetById": "Monitor not found", + "monitorGetByUserId": "No monitors found for user", + "jobQueueWorkerClose": "Error closing worker", + "jobQueueDeleteJob": "Job not found in queue", + "jobQueueObliterate": "Error obliterating queue", + "pingCannotResolve": "No response", + "statusPageNotFound": "Status page not found", + "statusPageUrlNotUnique": "Status page url must be unique", + "dockerFail": "Failed to fetch Docker container information", + "dockerNotFound": "Docker container not found", + "portFail": "Failed to connect to port", + "alertCreate": "Alert created successfully", + "alertGetByUser": "Got alerts successfully", + "alertGetByMonitor": "Got alerts by Monitor successfully", + "alertGetById": "Got alert by Id successfully", + "alertEdit": "Alert edited successfully", + "alertDelete": "Alert deleted successfully", + "authCreateUser": "User created successfully", + "authLoginUser": "User logged in successfully", + "authLogoutUser": "User logged out successfully", + "authUpdateUser": "User updated successfully", + "authCreateRecoveryToken": "Recovery token created successfully", + "authVerifyRecoveryToken": "Recovery token verified successfully", + "authResetPassword": "Password reset successfully", + "authAdminCheck": "Admin check completed successfully", + "authDeleteUser": "User deleted successfully", + "authTokenRefreshed": "Auth token is refreshed", + "authGetAllUsers": "Got all users successfully", + "inviteIssued": "Invite sent successfully", + "inviteVerified": "Invite verified successfully", + "checkCreate": "Check created successfully", + "checkGet": "Got checks successfully", + "checkDelete": "Checks deleted successfully", + "checkUpdateTtl": "Checks TTL updated successfully", + "monitorGetAll": "Got all monitors successfully", + "monitorStatsById": "Got monitor stats by Id successfully", + "monitorGetByIdSuccess": "Got monitor by Id successfully", + "monitorGetByTeamId": "Got monitors by Team Id successfully", + "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", + "monitorCreate": "Monitor created successfully", + "monitorDelete": "Monitor deleted successfully", + "monitorEdit": "Monitor edited successfully", + "monitorCertificate": "Got monitor certificate successfully", + "monitorDemoAdded": "Successfully added demo monitors", + "queueGetMetrics": "Got metrics successfully", + "queueAddJob": "Job added successfully", + "queueObliterate": "Queue obliterated", + "jobQueueDeleteJobSuccess": "Job removed successfully", + "jobQueuePauseJob": "Job paused successfully", + "jobQueueResumeJob": "Job resumed successfully", + "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", + "maintenanceWindowCreate": "Maintenance Window created successfully", + "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", + "maintenanceWindowDelete": "Maintenance Window deleted successfully", + "maintenanceWindowEdit": "Maintenance Window edited successfully", + "pingSuccess": "Success", + "getAppSettings": "Got app settings successfully", + "updateAppSettings": "Updated app settings successfully", + "statusPageByUrl": "Got status page by url successfully", + "statusPageCreate": "Status page created successfully", + "newTermsAdded": "New terms added to POEditor", + "dockerSuccess": "Docker container status fetched successfully", + "portSuccess": "Port connected successfully", + "monitorPause": "Monitor paused successfully", + "monitorResume": "Monitor resumed successfully", + "statusPageDelete": "Status page deleted successfully", + "statusPageUpdate": "Status page updated successfully", + "statusPageByTeamId": "Got status pages by team id successfully", + "httpNetworkError": "Network error", + "httpNotJson": "Response data is not json", + "httpJsonPathError": "Failed to parse json data", + "httpEmptyResult": "Result is empty", + "httpMatchSuccess": "Response data match successfully", + "httpMatchFail": "Failed to match response data" +} \ No newline at end of file diff --git a/Server/locales/en.json.bak b/Server/locales/en.json.bak new file mode 100644 index 000000000..eaef58eef --- /dev/null +++ b/Server/locales/en.json.bak @@ -0,0 +1,147 @@ +{ + "dontHaveAccount": "Don't have account", + "email": "E-mail", + "forgotPassword": "Forgot Password", + "password": "password", + "signUp": "Sign up", + "submit": "Submit", + "title": "Title", + "continue": "Continue", + "enterEmail": "Enter your email", + "authLoginTitle": "Log In", + "authLoginEnterPassword": "Enter your password", + "commonPassword": "Password", + "commonBack": "Back", + "authForgotPasswordTitle": "Forgot password?", + "authForgotPasswordResetPassword": "Reset password", + "createPassword": "Create your password", + "createAPassword": "Create a password", + "authRegisterAlreadyHaveAccount": "Already have an account?", + "commonAppName": "BlueWave Uptime", + "authLoginEnterEmail": "Enter your email", + "authRegisterTitle": "Create an account", + "authRegisterStepOneTitle": "Create your account", + "authRegisterStepOneDescription": "Enter your details to get started", + "authRegisterStepTwoTitle": "Set up your profile", + "authRegisterStepTwoDescription": "Tell us more about yourself", + "authRegisterStepThreeTitle": "Almost done!", + "authRegisterStepThreeDescription": "Review your information", + "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", + "authForgotPasswordSendInstructions": "Send instructions", + "authForgotPasswordBackTo": "Back to", + "authCheckEmailTitle": "Check your email", + "authCheckEmailDescription": "We sent a password reset link to {{email}}", + "authCheckEmailResendEmail": "Resend email", + "authCheckEmailBackTo": "Back to", + "goBackTo": "Go back to", + "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", + "authCheckEmailClickToResend": "Click to resend", + "authSetNewPasswordTitle": "Set new password", + "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", + "authSetNewPasswordNewPassword": "New password", + "authSetNewPasswordConfirmPassword": "Confirm password", + "confirmPassword": "Confirm your password", + "authSetNewPasswordResetPassword": "Reset password", + "authSetNewPasswordBackTo": "Back to", + "authPasswordMustBeAtLeast": "Must be at least", + "authPasswordCharactersLong": "8 characters long", + "authPasswordMustContainAtLeast": "Must contain at least", + "authPasswordSpecialCharacter": "one special character", + "authPasswordOneNumber": "one number", + "authPasswordUpperCharacter": "one upper character", + "authPasswordLowerCharacter": "one lower character", + "authPasswordConfirmAndPassword": "Confirm password and password", + "authPasswordMustMatch": "must match", + "friendlyError": "Something went wrong...", + "unknownError": "An unknown error occurred", + "unauthorized": "Unauthorized access", + "authAdminExists": "Admin already exists", + "authInviteNotFound": "Invite not found", + "unknownService": "Unknown service", + "noAuthToken": "No auth token provided", + "invalidAuthToken": "Invalid auth token", + "expiredAuthToken": "Token expired", + "noRefreshToken": "No refresh token provided", + "invalidRefreshToken": "Invalid refresh token", + "expiredRefreshToken": "Refresh token expired", + "requestNewAccessToken": "Request new access token", + "invalidPayload": "Invalid payload", + "verifyOwnerNotFound": "Document not found", + "verifyOwnerUnauthorized": "Unauthorized access", + "insufficientPermissions": "Insufficient permissions", + "dbUserExists": "User already exists", + "dbUserNotFound": "User not found", + "dbTokenNotFound": "Token not found", + "dbResetPasswordBadMatch": "New password must be different from old password", + "dbFindMonitorById": "Monitor with id ${monitorId} not found", + "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", + "authIncorrectPassword": "Incorrect password", + "authUnauthorized": "Unauthorized access", + "monitorGetById": "Monitor not found", + "monitorGetByUserId": "No monitors found for user", + "jobQueueWorkerClose": "Error closing worker", + "jobQueueDeleteJob": "Job not found in queue", + "jobQueueObliterate": "Error obliterating queue", + "pingCannotResolve": "No response", + "statusPageNotFound": "Status page not found", + "statusPageUrlNotUnique": "Status page url must be unique", + "dockerFail": "Failed to fetch Docker container information", + "dockerNotFound": "Docker container not found", + "portFail": "Failed to connect to port", + "alertCreate": "Alert created successfully", + "alertGetByUser": "Got alerts successfully", + "alertGetByMonitor": "Got alerts by Monitor successfully", + "alertGetById": "Got alert by Id successfully", + "alertEdit": "Alert edited successfully", + "alertDelete": "Alert deleted successfully", + "authCreateUser": "User created successfully", + "authLoginUser": "User logged in successfully", + "authLogoutUser": "User logged out successfully", + "authUpdateUser": "User updated successfully", + "authCreateRecoveryToken": "Recovery token created successfully", + "authVerifyRecoveryToken": "Recovery token verified successfully", + "authResetPassword": "Password reset successfully", + "authAdminCheck": "Admin check completed successfully", + "authDeleteUser": "User deleted successfully", + "authTokenRefreshed": "Auth token is refreshed", + "authGetAllUsers": "Got all users successfully", + "inviteIssued": "Invite sent successfully", + "inviteVerified": "Invite verified successfully", + "checkCreate": "Check created successfully", + "checkGet": "Got checks successfully", + "checkDelete": "Checks deleted successfully", + "checkUpdateTtl": "Checks TTL updated successfully", + "monitorGetAll": "Got all monitors successfully", + "monitorStatsById": "Got monitor stats by Id successfully", + "monitorGetByIdSuccess": "Got monitor by Id successfully", + "monitorGetByTeamId": "Got monitors by Team Id successfully", + "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", + "monitorCreate": "Monitor created successfully", + "monitorDelete": "Monitor deleted successfully", + "monitorEdit": "Monitor edited successfully", + "monitorCertificate": "Got monitor certificate successfully", + "monitorDemoAdded": "Successfully added demo monitors", + "queueGetMetrics": "Got metrics successfully", + "queueAddJob": "Job added successfully", + "queueObliterate": "Queue obliterated", + "jobQueueDeleteJobSuccess": "Job removed successfully", + "jobQueuePauseJob": "Job paused successfully", + "jobQueueResumeJob": "Job resumed successfully", + "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", + "maintenanceWindowCreate": "Maintenance Window created successfully", + "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", + "maintenanceWindowDelete": "Maintenance Window deleted successfully", + "maintenanceWindowEdit": "Maintenance Window edited successfully", + "pingSuccess": "Success", + "getAppSettings": "Got app settings successfully", + "updateAppSettings": "Updated app settings successfully", + "statusPageByUrl": "Got status page by url successfully", + "statusPageCreate": "Status page created successfully", + "newTermsAdded": "New terms added to POEditor", + "dockerSuccess": "Docker container status fetched successfully", + "portSuccess": "Port connected successfully", + "monitorPause": "Monitor paused successfully", + "monitorResume": "Monitor resumed successfully", + "statusPageDelete": "Status page deleted successfully", + "statusPageUpdate": "Status page updated successfully" +} diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js index fa9af4c0b..b64cda897 100644 --- a/Server/middleware/handleErrors.js +++ b/Server/middleware/handleErrors.js @@ -1,10 +1,12 @@ import logger from "../utils/logger.js"; -import { errorMessages } from "../utils/messages.js"; +import ServiceRegistry from "../service/serviceRegistry.js"; +import StringService from "../service/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; - const message = error.message || errorMessages.FRIENDLY_ERROR; - const service = error.service || errorMessages.UNKNOWN_SERVICE; + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); + const message = error.message || stringService.friendlyError; + const service = error.service || stringService.unknownService; logger.error({ message: message, service: service, diff --git a/Server/middleware/isAllowed.js b/Server/middleware/isAllowed.js index bd75b00ad..03a6b9d16 100644 --- a/Server/middleware/isAllowed.js +++ b/Server/middleware/isAllowed.js @@ -2,17 +2,17 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; import ServiceRegistry from "../service/serviceRegistry.js"; +import StringService from "../service/stringService.js"; import SettingsService from "../service/settingsService.js"; -import { errorMessages } from "../utils/messages.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { const token = req.headers["authorization"]; - + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); // If no token is pressent, return an error if (!token) { - const error = new Error(errorMessages.NO_AUTH_TOKEN); + const error = new Error(stringService.noAuthToken); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -21,7 +21,7 @@ const isAllowed = (allowedRoles) => { // If the token is improperly formatted, return an error if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(errorMessages.INVALID_AUTH_TOKEN); + const error = new Error(stringService.invalidAuthToken); error.status = 400; error.service = SERVICE_NAME; next(error); @@ -41,7 +41,7 @@ const isAllowed = (allowedRoles) => { next(); return; } else { - const error = new Error(errorMessages.INSUFFICIENT_PERMISSIONS); + const error = new Error(stringService.insufficientPermissions); error.status = 401; error.service = SERVICE_NAME; next(error); diff --git a/Server/middleware/languageMiddleware.js b/Server/middleware/languageMiddleware.js new file mode 100644 index 000000000..11c2f2aec --- /dev/null +++ b/Server/middleware/languageMiddleware.js @@ -0,0 +1,11 @@ +const languageMiddleware = (stringService, translationService) => (req, res, next) => { + const acceptLanguage = req.headers['accept-language'] || 'en'; + const language = acceptLanguage.split(',')[0].slice(0, 2).toLowerCase(); + + translationService.setLanguage(language); + stringService.setLanguage(language); + + next(); +}; + +export default languageMiddleware; \ No newline at end of file diff --git a/Server/middleware/verifyJWT.js b/Server/middleware/verifyJWT.js index 87fa53bd6..9ee42e809 100644 --- a/Server/middleware/verifyJWT.js +++ b/Server/middleware/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import { errorMessages } from "../utils/messages.js"; import ServiceRegistry from "../service/serviceRegistry.js"; import SettingsService from "../service/settingsService.js"; +import StringService from "../service/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; @@ -14,10 +14,11 @@ const TOKEN_PREFIX = "Bearer "; * @returns {express.Response} */ const verifyJWT = (req, res, next) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - const error = new Error(errorMessages.NO_AUTH_TOKEN); + const error = new Error(stringService.noAuthToken); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -25,7 +26,7 @@ const verifyJWT = (req, res, next) => { } // Make sure it is properly formatted if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(errorMessages.INVALID_AUTH_TOKEN); // Instantiate a new Error object for improperly formatted token + const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token error.status = 400; error.service = SERVICE_NAME; error.method = "verifyJWT"; @@ -43,7 +44,7 @@ const verifyJWT = (req, res, next) => { handleExpiredJwtToken(req, res, next); } else { // Invalid token (signature or token altered or other issue) - const errorMessage = errorMessages.INVALID_AUTH_TOKEN; + const errorMessage = stringService.invalidAuthToken; return res.status(401).json({ success: false, msg: errorMessage }); } } else { @@ -55,12 +56,13 @@ const verifyJWT = (req, res, next) => { }; function handleExpiredJwtToken(req, res, next) { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); // check for refreshToken const refreshToken = req.headers["x-refresh-token"]; if (!refreshToken) { // No refresh token provided - const error = new Error(errorMessages.NO_REFRESH_TOKEN); + const error = new Error(stringService.noRefreshToken); error.status = 401; error.service = SERVICE_NAME; error.method = "handleExpiredJwtToken"; @@ -76,8 +78,8 @@ function handleExpiredJwtToken(req, res, next) { // Invalid or expired refresh token, trigger logout const errorMessage = refreshErr.name === "TokenExpiredError" - ? errorMessages.EXPIRED_REFRESH_TOKEN - : errorMessages.INVALID_REFRESH_TOKEN; + ? stringService.expiredRefreshToken + : stringService.invalidRefreshToken; const error = new Error(errorMessage); error.status = 401; error.service = SERVICE_NAME; @@ -87,7 +89,7 @@ function handleExpiredJwtToken(req, res, next) { // Refresh token is valid and unexpired, request for new access token res.status(403).json({ success: false, - msg: errorMessages.REQUEST_NEW_ACCESS_TOKEN, + msg: stringService.requestNewAccessToken, }); }); } diff --git a/Server/middleware/verifyOwnership.js b/Server/middleware/verifyOwnership.js index d812dd543..ca2476f54 100644 --- a/Server/middleware/verifyOwnership.js +++ b/Server/middleware/verifyOwnership.js @@ -1,8 +1,10 @@ import logger from "../utils/logger.js"; -import { errorMessages } from "../utils/messages.js"; +import ServiceRegistry from "../service/serviceRegistry.js"; +import StringService from "../service/stringService.js"; const SERVICE_NAME = "verifyOwnership"; const verifyOwnership = (Model, paramName) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); return async (req, res, next) => { const userId = req.user._id; const documentId = req.params[paramName]; @@ -11,11 +13,11 @@ const verifyOwnership = (Model, paramName) => { //If the document is not found, return a 404 error if (!doc) { logger.error({ - message: errorMessages.VERIFY_OWNER_NOT_FOUND, + message: stringService.verifyOwnerNotFound, service: SERVICE_NAME, method: "verifyOwnership", }); - const error = new Error(errorMessages.VERIFY_OWNER_NOT_FOUND); + const error = new Error(stringService.verifyOwnerNotFound); error.status = 404; throw error; } @@ -23,7 +25,7 @@ const verifyOwnership = (Model, paramName) => { // Special case for User model, as it will not have a `userId` field as other docs will if (Model.modelName === "User") { if (userId.toString() !== doc._id.toString()) { - const error = new Error(errorMessages.VERIFY_OWNER_UNAUTHORIZED); + const error = new Error(stringService.verifyOwnerUnauthorized); error.status = 403; throw error; } @@ -33,7 +35,7 @@ const verifyOwnership = (Model, paramName) => { // If the userID does not match the document's userID, return a 403 error if (userId.toString() !== doc.userId.toString()) { - const error = new Error(errorMessages.VERIFY_OWNER_UNAUTHORIZED); + const error = new Error(stringService.verifyOwnerUnauthorized); error.status = 403; throw error; } diff --git a/Server/middleware/verifySuperAdmin.js b/Server/middleware/verifySuperAdmin.js index bf4c780a7..98c336fad 100644 --- a/Server/middleware/verifySuperAdmin.js +++ b/Server/middleware/verifySuperAdmin.js @@ -2,9 +2,9 @@ const jwt = require("jsonwebtoken"); const logger = require("../utils/logger"); const SERVICE_NAME = "verifyAdmin"; const TOKEN_PREFIX = "Bearer "; -const { errorMessages } = require("../utils/messages"); import ServiceRegistry from "../service/serviceRegistry.js"; import SettingsService from "../service/settingsService.js"; +import StringService from "../service/stringService.js"; /** * Verifies the JWT token * @function @@ -14,10 +14,11 @@ import SettingsService from "../service/settingsService.js"; * @returns {express.Response} */ const verifySuperAdmin = (req, res, next) => { + const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - const error = new Error(errorMessages.NO_AUTH_TOKEN); + const error = new Error(stringService.noAuthToken); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -25,7 +26,7 @@ const verifySuperAdmin = (req, res, next) => { } // Make sure it is properly formatted if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(errorMessages.INVALID_AUTH_TOKEN); // Instantiate a new Error object for improperly formatted token + const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token error.status = 400; error.service = SERVICE_NAME; error.method = "verifySuperAdmin"; @@ -44,21 +45,21 @@ const verifySuperAdmin = (req, res, next) => { service: SERVICE_NAME, method: "verifySuperAdmin", stack: err.stack, - details: errorMessages.INVALID_AUTH_TOKEN, + details: stringService.invalidAuthToken, }); return res .status(401) - .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); + .json({ success: false, msg: stringService.invalidAuthToken }); } if (decoded.role.includes("superadmin") === false) { logger.error({ - message: errorMessages.INVALID_AUTH_TOKEN, + message: stringService.invalidAuthToken, service: SERVICE_NAME, method: "verifySuperAdmin", stack: err.stack, }); - return res.status(401).json({ success: false, msg: errorMessages.UNAUTHORIZED }); + return res.status(401).json({ success: false, msg: stringService.unauthorized }); } next(); }); diff --git a/Server/nodemon.json b/Server/nodemon.json new file mode 100644 index 000000000..b43c80fc1 --- /dev/null +++ b/Server/nodemon.json @@ -0,0 +1,12 @@ +{ + "ignore": [ + "locales/*", + "*.log", + "node_modules/*" + ], + "watch": [ + "*.js", + "*.json" + ], + "ext": "js,json" +} \ No newline at end of file diff --git a/Server/package-lock.json b/Server/package-lock.json index d7cb00d6c..76cb601a3 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.40.2", + "bullmq": "5.40.3", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -19,6 +19,7 @@ "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "jmespath": "^0.16.0", "joi": "^17.13.1", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", @@ -1748,9 +1749,9 @@ } }, "node_modules/bullmq": { - "version": "5.40.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.2.tgz", - "integrity": "sha512-Cn4NUpwGAF4WnuXR2kTZCTAUEUHajSCn/IqiDG9ry1kVvAwwwg1Ati3J5HN2uZjqD5PBfNDXYnsc2+0PzakDwg==", + "version": "5.40.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.3.tgz", + "integrity": "sha512-ZHGHilK/A8wo5HHlPbwyOWMCEMRsyEmwFubEu98Xb7IyELyJCDn2hXYQVemG8dqFovkgIxXmerf4y3OHcmwmOQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -3071,9 +3072,9 @@ } }, "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { @@ -3868,10 +3869,11 @@ } }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -4441,6 +4443,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", diff --git a/Server/package.json b/Server/package.json index 6695a444a..3020ac0e9 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.40.2", + "bullmq": "5.40.3", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -26,6 +26,7 @@ "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", + "jmespath": "^0.16.0", "joi": "^17.13.1", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index f7c70f0a7..be36383d2 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -12,7 +12,7 @@ const QUEUE_LOOKUP = { }; const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`; -import { successMessages, errorMessages } from "../utils/messages.js"; + class NewJobQueue { static SERVICE_NAME = SERVICE_NAME; @@ -22,6 +22,7 @@ class NewJobQueue { networkService, notificationService, settingsService, + stringService, logger, Queue, Worker @@ -44,6 +45,7 @@ class NewJobQueue { this.settingsService = settingsService; this.logger = logger; this.Worker = Worker; + this.stringService = stringService; QUEUE_NAMES.forEach((name) => { this.queues[name] = new Queue(name, { connection }); @@ -455,7 +457,7 @@ class NewJobQueue { if (wasDeleted === true) { this.logger.info({ - message: successMessages.JOB_QUEUE_DELETE_JOB, + message: this.stringService.jobQueueDeleteJob, service: SERVICE_NAME, method: "deleteJob", details: `Deleted job ${monitor._id}`, @@ -464,7 +466,7 @@ class NewJobQueue { await this.scaleWorkers(workerStats, queue); } else { this.logger.error({ - message: errorMessages.JOB_QUEUE_DELETE_JOB, + message: this.stringService.jobQueueDeleteJob, service: SERVICE_NAME, method: "deleteJob", details: `Failed to delete job ${monitor._id}`, @@ -587,7 +589,7 @@ class NewJobQueue { const metrics = await this.getMetrics(); this.logger.info({ - message: successMessages.JOB_QUEUE_OBLITERATE, + message: this.stringService.jobQueueObliterate, service: SERVICE_NAME, method: "obliterate", details: metrics, diff --git a/Server/service/networkService.js b/Server/service/networkService.js index fd6fff522..c76712de7 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,4 +1,4 @@ -import { errorMessages, successMessages } from "../utils/messages.js"; +import jmespath from 'jmespath'; const SERVICE_NAME = "NetworkService"; const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push"; @@ -13,7 +13,8 @@ const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push"; */ class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor(axios, ping, logger, http, Docker, net) { + + constructor(axios, ping, logger, http, Docker, net, stringService) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -30,6 +31,7 @@ class NetworkService { this.http = http; this.Docker = Docker; this.net = net; + this.stringService = stringService; } /** @@ -87,13 +89,13 @@ class NetworkService { if (error) { pingResponse.status = false; pingResponse.code = this.PING_ERROR; - pingResponse.message = errorMessages.PING_CANNOT_RESOLVE; + pingResponse.message = "No response"; return pingResponse; } pingResponse.code = 200; pingResponse.status = response.alive; - pingResponse.message = successMessages.PING_SUCCESS; + pingResponse.message = "Success"; return pingResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -121,20 +123,19 @@ class NetworkService { */ async requestHttp(job) { try { - const url = job.data.url; + const { url, secret, _id, name, teamId, type, jsonPath, matchMethod, expectedValue } = job.data; const config = {}; - job.data.secret !== undefined && - (config.headers = { Authorization: `Bearer ${job.data.secret}` }); + secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` }); const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config) ); const httpResponse = { - monitorId: job.data._id, - teamId: job.data.teamId, - type: job.data.type, + monitorId: _id, + teamId, + type, responseTime, payload: response?.data, }; @@ -143,12 +144,62 @@ class NetworkService { const code = error.response?.status || this.NETWORK_ERROR; httpResponse.code = code; httpResponse.status = false; - httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; return httpResponse; } - httpResponse.status = true; + httpResponse.code = response.status; - httpResponse.message = this.http.STATUS_CODES[response.status]; + + if (!expectedValue) { + // not configure expected value, return + httpResponse.status = true; + httpResponse.message = this.http.STATUS_CODES[response.status]; + return httpResponse; + } + + // validate if response data match expected value + let result = response?.data; + + this.logger.info({ + service: this.SERVICE_NAME, + method: "requestHttp", + message: `Job: [${name}](${_id}) match result with expected value`, + details: { expectedValue, result, jsonPath, matchMethod } + }); + + if (jsonPath) { + const contentType = response.headers['content-type']; + + const isJson = contentType?.includes('application/json'); + if (!isJson) { + httpResponse.status = false; + httpResponse.message = this.stringService.httpNotJson; + return httpResponse; + } + + try { + result = jmespath.search(result, jsonPath); + } catch (error) { + httpResponse.status = false; + httpResponse.message = this.stringService.httpJsonPathError; + return httpResponse; + } + } + + if (result === null || result === undefined) { + httpResponse.status = false; + httpResponse.message = this.stringService.httpEmptyResult; + return httpResponse; + } + + let match; + result = typeof result === "object" ? JSON.stringify(result) : result.toString(); + if (matchMethod === "include") match = result.includes(expectedValue); + else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result); + else match = result === expectedValue; + + httpResponse.status = match; + httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail; return httpResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -240,7 +291,7 @@ class NetworkService { const containers = await docker.listContainers({ all: true }); const containerExists = containers.some((c) => c.Id.startsWith(job.data.url)); if (!containerExists) { - throw new Error(errorMessages.DOCKER_NOT_FOUND); + throw new Error(this.stringService.dockerNotFound); } const container = docker.getContainer(job.data.url); @@ -257,12 +308,12 @@ class NetworkService { if (error) { dockerResponse.status = false; dockerResponse.code = error.statusCode || this.NETWORK_ERROR; - dockerResponse.message = error.reason || errorMessages.DOCKER_FAIL; + dockerResponse.message = error.reason || "Failed to fetch Docker container information"; return dockerResponse; } dockerResponse.status = response?.State?.Status === "running" ? true : false; dockerResponse.code = 200; - dockerResponse.message = successMessages.DOCKER_SUCCESS; + dockerResponse.message = "Docker container status fetched successfully"; return dockerResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -310,13 +361,13 @@ class NetworkService { if (error) { portResponse.status = false; portResponse.code = this.NETWORK_ERROR; - portResponse.message = errorMessages.PORT_FAIL; + portResponse.message = this.stringService.portFail; return portResponse; } portResponse.status = response.success; portResponse.code = 200; - portResponse.message = successMessages.PORT_SUCCESS; + portResponse.message = this.stringService.portSuccess; return portResponse; } catch (error) { error.service = this.SERVICE_NAME; diff --git a/Server/service/stringService.js b/Server/service/stringService.js new file mode 100644 index 000000000..1ef208d6c --- /dev/null +++ b/Server/service/stringService.js @@ -0,0 +1,358 @@ +class StringService { + static SERVICE_NAME = "StringService"; + + constructor(translationService) { + if (StringService.instance) { + return StringService.instance; + } + + this.translationService = translationService; + this._language = 'en'; // default language + StringService.instance = this; + } + + setLanguage(language) { + this._language = language; + } + + get language() { + return this._language; + } + + // Auth Messages + get dontHaveAccount() { + return this.translationService.getTranslation('dontHaveAccount'); + } + + get email() { + return this.translationService.getTranslation('email'); + } + + get forgotPassword() { + return this.translationService.getTranslation('forgotPassword'); + } + + get password() { + return this.translationService.getTranslation('password'); + } + + get signUp() { + return this.translationService.getTranslation('signUp'); + } + + get submit() { + return this.translationService.getTranslation('submit'); + } + + get title() { + return this.translationService.getTranslation('title'); + } + + get continue() { + return this.translationService.getTranslation('continue'); + } + + get enterEmail() { + return this.translationService.getTranslation('enterEmail'); + } + + get authLoginTitle() { + return this.translationService.getTranslation('authLoginTitle'); + } + + get authLoginEnterPassword() { + return this.translationService.getTranslation('authLoginEnterPassword'); + } + + get commonPassword() { + return this.translationService.getTranslation('commonPassword'); + } + + get commonBack() { + return this.translationService.getTranslation('commonBack'); + } + + get authForgotPasswordTitle() { + return this.translationService.getTranslation('authForgotPasswordTitle'); + } + + get authForgotPasswordResetPassword() { + return this.translationService.getTranslation('authForgotPasswordResetPassword'); + } + + get createPassword() { + return this.translationService.getTranslation('createPassword'); + } + + get createAPassword() { + return this.translationService.getTranslation('createAPassword'); + } + + get authRegisterAlreadyHaveAccount() { + return this.translationService.getTranslation('authRegisterAlreadyHaveAccount'); + } + + get commonAppName() { + return this.translationService.getTranslation('commonAppName'); + } + + get authLoginEnterEmail() { + return this.translationService.getTranslation('authLoginEnterEmail'); + } + + get authRegisterTitle() { + return this.translationService.getTranslation('authRegisterTitle'); + } + + get monitorGetAll() { + return this.translationService.getTranslation('monitorGetAll'); + } + + get monitorGetById() { + return this.translationService.getTranslation('monitorGetById'); + } + + get monitorCreate() { + return this.translationService.getTranslation('monitorCreate'); + } + + get monitorEdit() { + return this.translationService.getTranslation('monitorEdit'); + } + + get monitorDelete() { + return this.translationService.getTranslation('monitorDelete'); + } + + get monitorPause() { + return this.translationService.getTranslation('monitorPause'); + } + + get monitorResume() { + return this.translationService.getTranslation('monitorResume'); + } + + get monitorDemoAdded() { + return this.translationService.getTranslation('monitorDemoAdded'); + } + + get monitorStatsById() { + return this.translationService.getTranslation('monitorStatsById'); + } + + get monitorCertificate() { + return this.translationService.getTranslation('monitorCertificate'); + } + + // Maintenance Window Messages + get maintenanceWindowCreate() { + return this.translationService.getTranslation('maintenanceWindowCreate'); + } + + get maintenanceWindowGetById() { + return this.translationService.getTranslation('maintenanceWindowGetById'); + } + + get maintenanceWindowGetByTeam() { + return this.translationService.getTranslation('maintenanceWindowGetByTeam'); + } + + get maintenanceWindowDelete() { + return this.translationService.getTranslation('maintenanceWindowDelete'); + } + + get maintenanceWindowEdit() { + return this.translationService.getTranslation('maintenanceWindowEdit'); + } + + // Error Messages + get unknownError() { + return this.translationService.getTranslation('unknownError'); + } + + get friendlyError() { + return this.translationService.getTranslation('friendlyError'); + } + + get authIncorrectPassword() { + return this.translationService.getTranslation('authIncorrectPassword'); + } + + get unauthorized() { + return this.translationService.getTranslation('unauthorized'); + } + + get authAdminExists() { + return this.translationService.getTranslation('authAdminExists'); + } + + get authInviteNotFound() { + return this.translationService.getTranslation('authInviteNotFound'); + } + + get unknownService() { + return this.translationService.getTranslation('unknownService'); + } + + get noAuthToken() { + return this.translationService.getTranslation('noAuthToken'); + } + + get invalidAuthToken() { + return this.translationService.getTranslation('invalidAuthToken'); + } + + get expiredAuthToken() { + return this.translationService.getTranslation('expiredAuthToken'); + } + + // Queue Messages + get queueGetMetrics() { + return this.translationService.getTranslation('queueGetMetrics'); + } + + get queueAddJob() { + return this.translationService.getTranslation('queueAddJob'); + } + + get queueObliterate() { + return this.translationService.getTranslation('queueObliterate'); + } + + // Job Queue Messages + get jobQueueDeleteJobSuccess() { + return this.translationService.getTranslation('jobQueueDeleteJobSuccess'); + } + + get jobQueuePauseJob() { + return this.translationService.getTranslation('jobQueuePauseJob'); + } + + get jobQueueResumeJob() { + return this.translationService.getTranslation('jobQueueResumeJob'); + } + + // Status Page Messages + get statusPageByUrl() { + return this.translationService.getTranslation('statusPageByUrl'); + } + + get statusPageCreate() { + return this.translationService.getTranslation('statusPageCreate'); + } + + get statusPageDelete() { + return this.translationService.getTranslation('statusPageDelete'); + } + + get statusPageUpdate() { + return this.translationService.getTranslation('statusPageUpdate'); + } + + get statusPageNotFound() { + return this.translationService.getTranslation('statusPageNotFound'); + } + + get statusPageByTeamId() { + return this.translationService.getTranslation('statusPageByTeamId'); + } + + get statusPageUrlNotUnique() { + return this.translationService.getTranslation('statusPageUrlNotUnique'); + } + + // Docker Messages + get dockerFail() { + return this.translationService.getTranslation('dockerFail'); + } + + get dockerNotFound() { + return this.translationService.getTranslation('dockerNotFound'); + } + + get dockerSuccess() { + return this.translationService.getTranslation('dockerSuccess'); + } + + // Port Messages + get portFail() { + return this.translationService.getTranslation('portFail'); + } + + get portSuccess() { + return this.translationService.getTranslation('portSuccess'); + } + + // Alert Messages + get alertCreate() { + return this.translationService.getTranslation('alertCreate'); + } + + get alertGetByUser() { + return this.translationService.getTranslation('alertGetByUser'); + } + + get alertGetByMonitor() { + return this.translationService.getTranslation('alertGetByMonitor'); + } + + get alertGetById() { + return this.translationService.getTranslation('alertGetById'); + } + + get alertEdit() { + return this.translationService.getTranslation('alertEdit'); + } + + get alertDelete() { + return this.translationService.getTranslation('alertDelete'); + } + + getDeletedCount(count) { + return this.translationService.getTranslation('deletedCount') + .replace('{count}', count); + } + + get pingSuccess() { + return this.translationService.getTranslation('pingSuccess'); + } + + get getAppSettings() { + return this.translationService.getTranslation('getAppSettings'); + } + + get httpNetworkError() { + return this.translationService.getTranslation('httpNetworkError'); + } + + get httpNotJson() { + return this.translationService.getTranslation('httpNotJson'); + } + + get httpJsonPathError() { + return this.translationService.getTranslation('httpJsonPathError'); + } + + get httpEmptyResult() { + return this.translationService.getTranslation('httpEmptyResult'); + } + + get httpMatchSuccess() { + return this.translationService.getTranslation('httpMatchSuccess'); + } + + get httpMatchFail() { + return this.translationService.getTranslation('httpMatchFail'); + } + + get updateAppSettings() { + return this.translationService.getTranslation('updateAppSettings'); + } + + getDbFindMonitorById(monitorId) { + return this.translationService.getTranslation('dbFindMonitorById') + .replace('${monitorId}', monitorId); + } +} + +export default StringService; \ No newline at end of file diff --git a/Server/service/translationService.js b/Server/service/translationService.js new file mode 100644 index 000000000..64c62bff2 --- /dev/null +++ b/Server/service/translationService.js @@ -0,0 +1,90 @@ +import fs from 'fs'; +import path from 'path'; + +class TranslationService { + static SERVICE_NAME = 'TranslationService'; + + constructor(logger) { + this.logger = logger; + this.translations = {}; + this._language = 'en'; + this.localesDir = path.join(process.cwd(), 'locales'); + } + + setLanguage(language) { + this._language = language; + } + + get language() { + return this._language; + } + + async initialize() { + try { + await this.loadFromFiles(); + + } catch (error) { + this.logger.error({ + message: error.message, + service: 'TranslationService', + method: 'initialize', + stack: error.stack + }); + } + } + + async loadFromFiles() { + try { + if (!fs.existsSync(this.localesDir)) { + return false; + } + + const files = fs.readdirSync(this.localesDir).filter(file => file.endsWith('.json')); + + if (files.length === 0) { + return false; + } + + for (const file of files) { + const language = file.replace('.json', ''); + const filePath = path.join(this.localesDir, file); + const content = fs.readFileSync(filePath, 'utf8'); + this.translations[language] = JSON.parse(content); + } + + this.logger.info({ + message: 'Translations loaded from files successfully', + service: 'TranslationService', + method: 'loadFromFiles' + }); + + return true; + } catch (error) { + this.logger.error({ + message: error.message, + service: 'TranslationService', + method: 'loadFromFiles', + stack: error.stack + }); + return false; + } + } + + getTranslation(key) { + let language = this._language; + + try { + return this.translations[language]?.[key] || this.translations['en']?.[key] || key; + } catch (error) { + this.logger.error({ + message: error.message, + service: 'TranslationService', + method: 'getTranslation', + stack: error.stack + }); + return key; + } + } +} + +export default TranslationService; \ No newline at end of file diff --git a/Server/utils/messages.js b/Server/utils/messages.js deleted file mode 100644 index 38b1bf9d1..000000000 --- a/Server/utils/messages.js +++ /dev/null @@ -1,150 +0,0 @@ -const errorMessages = { - // General Errors: - FRIENDLY_ERROR: "Something went wrong...", - UNKNOWN_ERROR: "An unknown error occurred", - - // Auth Controller - UNAUTHORIZED: "Unauthorized access", - AUTH_ADMIN_EXISTS: "Admin already exists", - AUTH_INVITE_NOT_FOUND: "Invite not found", - - //Error handling middleware - UNKNOWN_SERVICE: "Unknown service", - NO_AUTH_TOKEN: "No auth token provided", - INVALID_AUTH_TOKEN: "Invalid auth token", - EXPIRED_AUTH_TOKEN: "Token expired", - NO_REFRESH_TOKEN: "No refresh token provided", - INVALID_REFRESH_TOKEN: "Invalid refresh token", - EXPIRED_REFRESH_TOKEN: "Refresh token expired", - REQUEST_NEW_ACCESS_TOKEN: "Request new access token", - - //Payload - INVALID_PAYLOAD: "Invalid payload", - - //Ownership Middleware - VERIFY_OWNER_NOT_FOUND: "Document not found", - VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access", - - //Permissions Middleware - INSUFFICIENT_PERMISSIONS: "Insufficient permissions", - - //DB Errors - DB_USER_EXISTS: "User already exists", - DB_USER_NOT_FOUND: "User not found", - DB_TOKEN_NOT_FOUND: "Token not found", - DB_RESET_PASSWORD_BAD_MATCH: "New password must be different from old password", - DB_FIND_MONITOR_BY_ID: (monitorId) => `Monitor with id ${monitorId} not found`, - DB_DELETE_CHECKS: (monitorId) => `No checks found for monitor with id ${monitorId}`, - - //Auth errors - AUTH_INCORRECT_PASSWORD: "Incorrect password", - AUTH_UNAUTHORIZED: "Unauthorized access", - - // Monitor Errors - MONITOR_GET_BY_ID: "Monitor not found", - MONITOR_GET_BY_USER_ID: "No monitors found for user", - - // Job Queue Errors - JOB_QUEUE_WORKER_CLOSE: "Error closing worker", - JOB_QUEUE_DELETE_JOB: "Job not found in queue", - JOB_QUEUE_OBLITERATE: "Error obliterating queue", - - // PING Operations - PING_CANNOT_RESOLVE: "No response", - - // Status Page Errors - STATUS_PAGE_NOT_FOUND: "Status page not found", - STATUS_PAGE_URL_NOT_UNIQUE: "Status page url must be unique", - - // Docker - DOCKER_FAIL: "Failed to fetch Docker container information", - DOCKER_NOT_FOUND: "Docker container not found", - - // Port - PORT_FAIL: "Failed to connect to port", -}; - -const successMessages = { - //Alert Controller - ALERT_CREATE: "Alert created successfully", - ALERT_GET_BY_USER: "Got alerts successfully", - ALERT_GET_BY_MONITOR: "Got alerts by Monitor successfully", - ALERT_GET_BY_ID: "Got alert by Id successfully", - ALERT_EDIT: "Alert edited successfully", - ALERT_DELETE: "Alert deleted successfully", - - // Auth Controller - AUTH_CREATE_USER: "User created successfully", - AUTH_LOGIN_USER: "User logged in successfully", - AUTH_LOGOUT_USER: "User logged out successfully", - AUTH_UPDATE_USER: "User updated successfully", - AUTH_CREATE_RECOVERY_TOKEN: "Recovery token created successfully", - AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully", - AUTH_RESET_PASSWORD: "Password reset successfully", - AUTH_ADMIN_CHECK: "Admin check completed successfully", - AUTH_DELETE_USER: "User deleted successfully", - AUTH_TOKEN_REFRESHED: "Auth token is refreshed", - AUTH_GET_ALL_USERS: "Got all users successfully", - - // Invite Controller - INVITE_ISSUED: "Invite sent successfully", - INVITE_VERIFIED: "Invite verified successfully", - - // Check Controller - CHECK_CREATE: "Check created successfully", - CHECK_GET: "Got checks successfully", - CHECK_DELETE: "Checks deleted successfully", - CHECK_UPDATE_TTL: "Checks TTL updated successfully", - - //Monitor Controller - MONITOR_GET_ALL: "Got all monitors successfully", - MONITOR_STATS_BY_ID: "Got monitor stats by Id successfully", - MONITOR_GET_BY_ID: "Got monitor by Id successfully", - MONITOR_GET_BY_TEAM_ID: "Got monitors by Team Id successfully", - MONITOR_GET_BY_USER_ID: (userId) => `Got monitor for ${userId} successfully"`, - MONITOR_CREATE: "Monitor created successfully", - MONITOR_DELETE: "Monitor deleted successfully", - MONITOR_EDIT: "Monitor edited successfully", - MONITOR_CERTIFICATE: "Got monitor certificate successfully", - MONITOR_DEMO_ADDED: "Successfully added demo monitors", - - // Queue Controller - QUEUE_GET_METRICS: "Got metrics successfully", - QUEUE_ADD_JOB: "Job added successfully", - QUEUE_OBLITERATE: "Queue obliterated", - - //Job Queue - JOB_QUEUE_DELETE_JOB: "Job removed successfully", - JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!", - JOB_QUEUE_PAUSE_JOB: "Job paused successfully", - JOB_QUEUE_RESUME_JOB: "Job resumed successfully", - - //Maintenance Window Controller - MAINTENANCE_WINDOW_GET_BY_ID: "Got Maintenance Window by Id successfully", - MAINTENANCE_WINDOW_CREATE: "Maintenance Window created successfully", - MAINTENANCE_WINDOW_GET_BY_TEAM: "Got Maintenance Windows by Team successfully", - MAINTENANCE_WINDOW_DELETE: "Maintenance Window deleted successfully", - MAINTENANCE_WINDOW_EDIT: "Maintenance Window edited successfully", - - //Ping Operations - PING_SUCCESS: "Success", - - // App Settings - GET_APP_SETTINGS: "Got app settings successfully", - UPDATE_APP_SETTINGS: "Updated app settings successfully", - - // Status Page - STATUS_PAGE_BY_URL: "Got status page by url successfully", - STATUS_PAGE: "Got status page successfully", - STATUS_PAGE_CREATE: "Status page created successfully", - STATUS_PAGE_DELETE: "Status page deleted successfully", - STATUS_PAGE_UPDATE: "Status page updated successfully", - STATUS_PAGE_BY_TEAM_ID: "Got status pages by team id successfully", - // Docker - DOCKER_SUCCESS: "Docker container status fetched successfully", - - // Port - PORT_SUCCESS: "Port connected successfully", -}; - -export { errorMessages, successMessages }; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 42c41d8a5..b60022fc5 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -194,6 +194,9 @@ const createMonitorBodyValidation = joi.object({ }), notifications: joi.array().items(joi.object()), secret: joi.string(), + jsonPath: joi.string(), + expectedValue: joi.string(), + matchMethod: joi.string(), }); const editMonitorBodyValidation = joi.object({ @@ -202,6 +205,9 @@ const editMonitorBodyValidation = joi.object({ interval: joi.number(), notifications: joi.array().items(joi.object()), secret: joi.string(), + jsonPath: joi.string(), + expectedValue: joi.string(), + matchMethod: joi.string(), }); const pauseMonitorParamValidation = joi.object({