Merge branch 'develop' into fix/translation-fixes-2015

This commit is contained in:
Alexander Holliday
2025-05-07 09:17:28 -07:00
committed by GitHub
41 changed files with 418 additions and 13790 deletions

View File

@@ -13,9 +13,6 @@ on:
required: false
default: "key_value_json"
# For automatic execution at a specific time (every day at midnight)
schedule:
- cron: "0 0 * * *"
permissions:
contents: write

13698
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,12 +22,6 @@
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@reduxjs/toolkit": "2.7.0",
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -52,6 +46,14 @@
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
"unusedDepencies": {
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",

View File

@@ -1,43 +1,18 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
import { useTranslation } from "react-i18next";
import { setLanguage } from "./Features/UI/uiSlice";
function App() {
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const { i18n } = useTranslation();
useEffect(() => {
if (authToken) {
dispatch(getAppSettings({ authToken })).then((action) => {
if (action.payload && action.payload.success) {
const { language } = action.payload.data;
const availableLanguages = Object.keys(i18n.options.resources || {});
if (language && availableLanguages.includes(language)) {
dispatch(setLanguage(language));
i18n.changeLanguage(language);
} else {
dispatch(setLanguage(availableLanguages[0]));
i18n.changeLanguage(availableLanguages[0]);
}
}
});
}
}, [dispatch, authToken, i18n]);
// Cleanup
useEffect(() => {

View File

@@ -1,14 +1,13 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({
isAdmin,
label = "Create new",
shouldRender = true,
isLoading = true,
path,
bulkPath,
}) => {
@@ -17,7 +16,6 @@ const CreateMonitorHeader = ({
const theme = useTheme();
if (!isAdmin) return null;
if (!shouldRender) return <SkeletonLayout />;
return (
<Stack
@@ -27,6 +25,7 @@ const CreateMonitorHeader = ({
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
@@ -35,6 +34,7 @@ const CreateMonitorHeader = ({
</Button>
{bulkPath && (
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => {
@@ -52,7 +52,7 @@ export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
isLoading: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
bulkPath: PropTypes.string,

View File

@@ -1,46 +1,50 @@
import { useMemo } from "react";
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
UnsafeBurnerWalletAdapter,
PhantomWalletAdapter,
} from "@solana/wallet-adapter-wallets";
// import { useMemo } from "react";
// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
// import {
// UnsafeBurnerWalletAdapter,
// PhantomWalletAdapter,
// } from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import PropTypes from "prop-types";
import "./index.css";
// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
// import { clusterApiUrl } from "@solana/web3.js";
// import PropTypes from "prop-types";
// import "./index.css";
// Default styles that can be overridden by your app
import "@solana/wallet-adapter-react-ui/styles.css";
// // Default styles that can be overridden by your app
// import "@solana/wallet-adapter-react-ui/styles.css";
export const Wallet = ({ children }) => {
// The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
const network = WalletAdapterNetwork.Mainnet;
// export const Wallet = ({ children }) => {
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
// const network = WalletAdapterNetwork.Mainnet;
// You can also provide a custom RPC endpoint.
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
// // You can also provide a custom RPC endpoint.
// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter()],
// eslint-disable-next-line react-hooks/exhaustive-deps
[network]
);
// const wallets = useMemo(
// () => [new PhantomWalletAdapter()],
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [network]
// );
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider
wallets={wallets}
autoConnect
>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
// return (
// <ConnectionProvider endpoint={endpoint}>
// <WalletProvider
// wallets={wallets}
// autoConnect
// >
// <WalletModalProvider>{children}</WalletModalProvider>
// </WalletProvider>
// </ConnectionProvider>
// );
// };
Wallet.propTypes = {
children: PropTypes.node,
// Wallet.propTypes = {
// children: PropTypes.node,
// };
const Wallet = ({ children }) => {
return children;
};
export default Wallet;

View File

@@ -101,7 +101,7 @@ export const updatePageSpeed = createAsyncThunk(
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
// notifications: monitor.notifications,
notifications: monitor.notifications,
};
const res = await networkService.updateMonitor({
monitorId: monitor._id,

View File

@@ -5,7 +5,7 @@ const initialState = {
isLoading: false,
apiBaseUrl: "",
logLevel: "debug",
language: "",
language: "gb",
};
export const getAppSettings = createAsyncThunk(

View File

@@ -2,7 +2,7 @@ import { useState } from "react";
import { networkService } from "../main";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
const useGetInviteToken = () => {
const { t } = useTranslation();

View File

@@ -17,6 +17,7 @@ import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
import LanguageSelector from "../../../Components/LanguageSelector";
import { useTranslation } from "react-i18next";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -29,7 +30,6 @@ const Login = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
@@ -65,13 +65,17 @@ const Login = () => {
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue = name === idMap["login-email-input"]? value?.toLowerCase()||value : value
const lowerCasedValue =
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
}));
const { error } = credentials.validate({ [name]: lowerCasedValue }, { abortEarly: false });
const { error } = credentials.validate(
{ [name]: lowerCasedValue },
{ abortEarly: false }
);
setErrors((prev) => {
const prevErrors = { ...prev };
@@ -118,7 +122,7 @@ const Login = () => {
if (action.payload.success) {
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
body: t("welcomeBack"),
});
} else {
if (action.payload) {

View File

@@ -94,7 +94,7 @@ const InfrastructureMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/infrastructure/create"
/>
<Stack direction={"row"}>

View File

@@ -62,7 +62,7 @@ const PageSpeed = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader

View File

@@ -6,10 +6,10 @@ import Select from "../../Components/Inputs/Select";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import Dialog from "../../Components/Dialog";
import ConfigBox from "../../Components/ConfigBox";
import {
WalletMultiButton,
WalletDisconnectButton,
} from "@solana/wallet-adapter-react-ui";
// import {
// WalletMultiButton,
// WalletDisconnectButton,
// } from "@solana/wallet-adapter-react-ui";
//Utils
import { useTheme } from "@emotion/react";
@@ -254,7 +254,7 @@ const Settings = () => {
></Select>
</Stack>
</ConfigBox>
{isAdmin && (
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
@@ -276,8 +276,8 @@ const Settings = () => {
: t("settingsDisabled")}
</Box>
</ConfigBox>
)}
{isAdmin && (
)} */}
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
@@ -302,7 +302,7 @@ const Settings = () => {
</Stack>
</Box>
</ConfigBox>
)}
)} */}
{isAdmin && (
<ConfigBox>
<Box>

View File

@@ -3,6 +3,7 @@ import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
import ThemeSwitch from "../../../../../Components/ThemeSwitch";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
@@ -86,6 +87,8 @@ const ControlsHeader = ({
type = "uptime",
}) => {
const theme = useTheme();
const { t } = useTranslation();
const publicUrl = `/status/uptime/public/${url}`;
return (
<Stack
@@ -118,6 +121,26 @@ const ControlsHeader = ({
>
{statusPage?.companyName}
</Typography>
{statusPage?.isPublished && !isPublic && (
<Stack
direction="row"
alignItems="center"
justifyContent="center"
onClick={() => {
window.open(publicUrl, "_blank", "noopener,noreferrer");
}}
sx={{
display: "inline-flex",
":hover": {
cursor: "pointer",
borderBottom: 1,
},
}}
>
<Typography>{t("publicLink")}</Typography>
<ArrowOutwardIcon />
</Stack>
)}
</Stack>
<Controls
isDeleting={isDeleting}

View File

@@ -139,6 +139,7 @@ const PublicStatus = () => {
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
isPublic={isPublic}
/>
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
<StatusBar monitors={monitors} />

View File

@@ -61,6 +61,7 @@ const StatusPages = () => {
label="Create status page"
isAdmin={isAdmin}
path="/status/uptime/create"
isLoading={isLoading}
/>
<StatusPagesTable data={statusPages} />
</Stack>

View File

@@ -189,7 +189,7 @@ const UptimeMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/uptime/create"
bulkPath="/uptime/bulk-import"
/>

View File

@@ -96,16 +96,16 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={<UptimeConfigure />}
/>
<Route
{/* <Route
path="/distributed-uptime"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeMonitors />{" "}
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
{/* <Route
path="/distributed-uptime/create"
element={
<ProtectedDistributedUptimeRoute>
@@ -120,15 +120,15 @@ const Routes = () => {
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
/> */}
{/* <Route
path="/distributed-uptime/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeDetails />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="pagespeed"
@@ -154,9 +154,9 @@ const Routes = () => {
path="infrastructure/create"
element={<InfrastructureCreate />}
/>
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
/>
<Route
path="infrastructure/:monitorId"
@@ -177,42 +177,42 @@ const Routes = () => {
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/:url"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/create"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/create/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/configure/:url"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/configure/:url"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="integrations"
@@ -280,10 +280,10 @@ const Routes = () => {
path="/status/uptime/public/:url"
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/public/:url"
element={<DistributedUptimeStatus />}
/>
/> */}
<Route
path="*"

View File

@@ -32,7 +32,7 @@ class NetworkService {
config.headers = {
Authorization: `Bearer ${authToken}`,
"Accept-Language": currentLanguage,
"Accept-Language": currentLanguage === "gb" ? "en" : currentLanguage,
...config.headers,
};
@@ -45,6 +45,9 @@ class NetworkService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.code === "ERR_NETWORK") {
// Do error handling here
}
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
@@ -921,7 +924,7 @@ class NetworkService {
onOpen?.();
};
this.eventSource.addEventListener("open", (e) => {});
this.eventSource.addEventListener("open", (e) => { });
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);

View File

@@ -16,7 +16,7 @@ Object.keys(translations).forEach((path) => {
});
const savedLanguage = store.getState()?.ui?.language;
const initialLanguage = savedLanguage || primaryLanguage;
const initialLanguage = savedLanguage;
i18n.use(initReactI18next).init({
resources,

View File

@@ -19,6 +19,7 @@
"authRegisterAlreadyHaveAccount": "Already have an account?",
"authRegisterLoginLink": "Log In",
"commonAppName": "Checkmate",
"welcomeBack": "Welcome back! You're successfully logged in.",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
@@ -418,5 +419,7 @@
"validationFailed": "Validation failed",
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
}
},
"publicLink": "Public link"
}

View File

@@ -19,6 +19,7 @@
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
"authRegisterLoginLink": "Войти",
"commonAppName": "Checkmate",
"welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
"authLoginEnterEmail": "Введите свой email",
"authRegisterTitle": "Создать аккаунт",
"authRegisterStepOneTitle": "Создайте свой аккаут",
@@ -383,4 +384,4 @@
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
"pageSpeedAddApiKey": "как добавить ваш ключ API."
}
}

View File

@@ -19,6 +19,7 @@
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
"authRegisterLoginLink": "Giriş Yap",
"commonAppName": "Checkmate",
"welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
"authLoginEnterEmail": "E-posta adresinizi girin",
"authRegisterTitle": "Hesap oluştur",
"authRegisterStepOneTitle": "Hesabınızı oluşturun",

View File

@@ -4,6 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_APP_CLIENT_HOST: "http://localhost"
ports:
- "80:80"
- "443:443"

View File

@@ -4,7 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"

View File

@@ -4,7 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"

View File

@@ -27,11 +27,12 @@ const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
class MonitorController {
constructor(db, settingsService, jobQueue, stringService) {
constructor(db, settingsService, jobQueue, stringService, emailService) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
}
/**
@@ -537,18 +538,16 @@ class MonitorController {
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
// Get notifications from the request body
const notifications = req.body.notifications;
const notifications = req.body.notifications ?? [];
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
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)
@@ -636,6 +635,50 @@ class MonitorController {
}
};
/**
* Sends a test email to verify email delivery functionality.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.to - The email address to send the test email to.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and the email delivery message ID.
* @throws {Error} If there is an error while sending the test email.
*/
sendTestEmail = async (req, res, next) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
return res.error({ msg: this.stringService.errorForValidEmailAddress });
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
const messageId = await this.emailService.buildAndSendEmail(
"testEmailTemplate",
context,
to,
subject
);
if (!messageId) {
return res.error({
msg: "Failed to send test email.",
});
}
return res.success({
msg: this.stringService.sendTestEmail,
data: { messageId },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
}
};
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);

View File

@@ -7,10 +7,6 @@ const AppSettingsSchema = mongoose.Schema(
required: true,
default: "http://localhost:5000/api/v1",
},
language: {
type: String,
default: "en",
},
logLevel: {
type: String,
default: "debug",

View File

@@ -39,6 +39,10 @@ const MonitorSchema = mongoose.Schema(
"distributed_test",
],
},
ignoreTlsErrors: {
type: Boolean,
default: false,
},
jsonPath: {
type: String,
},

View File

@@ -81,7 +81,9 @@ import ServiceRegistry from "./service/serviceRegistry.js";
import MongoDB from "./db/mongo/MongoDB.js";
// Redis Service and dependencies
import IORedis from "ioredis";
import RedisService from "./service/redisService.js";
import TranslationService from "./service/translationService.js";
import languageMiddleware from "./middleware/languageMiddleware.js";
@@ -113,21 +115,16 @@ const shutdown = async () => {
method: "shutdown",
});
// flush Redis
const settings =
ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings() || {};
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
await redisService.flushall();
const { redisUrl } = settings;
const redis = new IORedis(redisUrl, { maxRetriesPerRequest: null });
logger.info({ message: "Flushing Redis" });
await redis.flushall();
logger.info({ message: "Redis flushed" });
process.exit(1);
}, SHUTDOWN_TIMEOUT);
try {
server.close();
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
} catch (error) {
@@ -185,7 +182,13 @@ const startApp = async () => {
stringService
);
const jobQueue = new JobQueue(
const redisService = await RedisService.createInstance({
logger,
IORedis,
SettingsService: settingsService,
});
const jobQueue = new JobQueue({
db,
statusService,
networkService,
@@ -194,8 +197,9 @@ const startApp = async () => {
stringService,
logger,
Queue,
Worker
);
Worker,
redisService,
});
// Register services
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
@@ -207,6 +211,7 @@ const startApp = async () => {
ServiceRegistry.register(StatusService.SERVICE_NAME, statusService);
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService);
ServiceRegistry.register(RedisService.SERVICE_NAME, redisService);
await translationService.initialize();
@@ -231,7 +236,8 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME),
ServiceRegistry.get(StringService.SERVICE_NAME)
ServiceRegistry.get(StringService.SERVICE_NAME),
ServiceRegistry.get(EmailService.SERVICE_NAME),
);
const settingsController = new SettingsController(
@@ -343,7 +349,7 @@ const startApp = async () => {
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
app.use("/api/v1/health", (req, res) => {
res.json({

View File

@@ -158,5 +158,8 @@
"platformRequired": "Platform is required",
"testNotificationFailed": "Failed to send test notification",
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: UP\n📟 Status Code: {code}\n\u200B\n",
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test Email from Monitoring System"
}

View File

@@ -1,16 +1,10 @@
import logger from "../utils/logger.js";
const languageMiddleware =
(stringService, translationService, settingsService) => async (req, res, next) => {
(stringService, translationService) => async (req, res, next) => {
try {
const settings = await settingsService.getSettings();
let language = settings && settings.language ? settings.language : null;
if (!language) {
const acceptLanguage = req.headers["accept-language"] || "en";
language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
}
const acceptLanguage = req.headers["accept-language"] || "en";
const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
translationService.setLanguage(language);
stringService.setLanguage(language);

View File

@@ -99,6 +99,12 @@ class MonitorRoutes {
);
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
this.router.post(
"/test-email",
isAllowed(["admin", "superadmin"]),
this.monitorController.sendTestEmail
);
}
getRouter() {

View File

@@ -67,6 +67,7 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
testEmailTemplate: this.loadTemplate("testEmailTemplate")
};
/**

View File

@@ -1,5 +1,3 @@
import IORedis from "ioredis";
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
const SERVICE_NAME = "JobQueue";
const JOBS_PER_WORKER = 5;
@@ -18,7 +16,7 @@ const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
class NewJobQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor(
constructor({
db,
statusService,
networkService,
@@ -27,16 +25,14 @@ class NewJobQueue {
stringService,
logger,
Queue,
Worker
) {
const settings = settingsService.getSettings() || {};
const { redisUrl } = settings;
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
Worker,
redisService,
}) {
this.connection = redisService.getConnection();
this.queues = {};
this.workers = {};
this.lastJobProcessedTime = {};
this.connection = connection;
this.db = db;
this.networkService = networkService;
this.statusService = statusService;
@@ -47,7 +43,7 @@ class NewJobQueue {
this.stringService = stringService;
QUEUE_NAMES.forEach((name) => {
const q = new Queue(name, { connection });
const q = new Queue(name, { connection: this.connection });
this.lastJobProcessedTime[q.name] = Date.now();
q.on("error", (error) => {
this.logger.error({

View File

@@ -1,4 +1,6 @@
import jmespath from "jmespath";
import https from "https";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
@@ -133,6 +135,7 @@ class NetworkService {
name,
teamId,
type,
ignoreTlsErrors,
jsonPath,
matchMethod,
expectedValue,
@@ -141,6 +144,12 @@ class NetworkService {
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
if (ignoreTlsErrors === true) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.get(url, config)
);

View File

@@ -0,0 +1,72 @@
class RedisService {
static SERVICE_NAME = "RedisService";
constructor({ logger, IORedis, SettingsService }) {
this.logger = logger;
this.IORedis = IORedis;
this.SettingsService = SettingsService;
this.connection = null;
}
static async createInstance({ logger, IORedis, SettingsService }) {
const instance = new RedisService({ logger, IORedis, SettingsService });
await instance.connect();
return instance;
}
async connect() {
const settings = this.SettingsService.getSettings();
const { redisUrl } = settings;
this.connection = new this.IORedis(redisUrl, {
maxRetriesPerRequest: null,
retryStrategy: (times) => {
if (times >= 5) {
throw new Error("Failed to connect to Redis");
}
this.logger.debug({
message: "Retrying Redis connection",
service: RedisService.SERVICE_NAME,
details: { times },
});
return Math.min(times * 100, 2000);
},
});
await new Promise((resolve, reject) => {
let errorOccurred = false;
this.connection.on("ready", () => {
if (!errorOccurred) {
this.logger.info({
message: "Redis connection established",
service: RedisService.SERVICE_NAME,
});
resolve();
}
});
this.connection.on("error", (err) => {
errorOccurred = true;
this.logger.error({
message: "Redis connection error",
service: RedisService.SERVICE_NAME,
error: err,
});
setTimeout(() => reject(err), 5000);
});
});
}
async flushall() {
this.logger.debug({
message: "Flushing all Redis data",
service: RedisService.SERVICE_NAME,
});
await this.connection.flushall();
}
getConnection() {
return this.connection;
}
}
export default RedisService;

View File

@@ -3,7 +3,6 @@ import dotenv from "dotenv";
dotenv.config();
const envConfig = {
logLevel: process.env.LOG_LEVEL,
language: process.env.LANGUAGE,
clientHost: process.env.CLIENT_HOST,
jwtSecret: process.env.JWT_SECRET,
dbType: process.env.DB_TYPE,

View File

@@ -0,0 +1,17 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="20px" font-family="Helvetica Neue">
Hello!
</mj-text>
<mj-text font-size="16px" font-family="Helvetica Neue">
This is a test email from the Monitoring System.
</mj-text>
<mj-text font-size="14px" font-family="Helvetica Neue">
If you're receiving this, your email configuration is working correctly.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View File

@@ -1,4 +1,6 @@
import { createLogger, format, transports } from "winston";
import dotenv from "dotenv";
dotenv.config();
class Logger {
constructor() {
@@ -40,8 +42,10 @@ class Logger {
}
);
const logLevel = process.env.LOG_LEVEL || "info";
this.logger = createLogger({
level: "info",
level: logLevel,
format: format.combine(format.timestamp()),
transports: [
new transports.Console({
@@ -107,6 +111,22 @@ class Logger {
stack: config.stack,
});
}
/**
* Logs a debug message.
* @param {Object} config - The configuration object.
* @param {string} config.message - The message to log.
* @param {string} config.service - The service name.
* @param {string} config.method - The method name.
* @param {Object} config.details - Additional details.
*/
debug(config) {
this.logger.debug(config.message, {
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
});
}
}
const logger = new Logger();

View File

@@ -183,6 +183,7 @@ const createMonitorBodyValidation = joi.object({
description: joi.string().required(),
type: joi.string().required(),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
@@ -207,6 +208,7 @@ const editMonitorBodyValidation = joi.object({
interval: joi.number(),
notifications: joi.array().items(joi.object()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),