Merge branch 'develop' of github.com:bluewave-labs/Checkmate into bug/fix-form-errors

This commit is contained in:
sonvir249
2025-05-12 22:37:11 +05:30
31 changed files with 1391 additions and 1055 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ Fixes #123
const { t } = useTranslation();
<div>{t('add')}</div>
```
- [ ] The issue I am working on is assigned to me.
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
@@ -156,7 +156,7 @@ const Select = ({
};
Select.propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
@@ -141,9 +141,9 @@ TextInput.displayName = "TextInput";
TextInput.propTypes = {
type: PropTypes.string,
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
value: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
placeholder: PropTypes.string,
isRequired: PropTypes.bool,
isOptional: PropTypes.bool,
-1
View File
@@ -65,7 +65,6 @@ const DataTable = ({
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
fontSize: "13px",
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
+24
View File
@@ -0,0 +1,24 @@
import { useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const UseDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async ({ teamId }) => {
setIsLoading(true);
try {
const res = await networkService.deleteChecksByTeamId({ teamId });
createToast({ body: t("settingsStatsCleared") });
} catch (error) {
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitorStats, isLoading];
};
export { UseDeleteMonitorStats };
+55
View File
@@ -0,0 +1,55 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchSettings = ({ setSettingsData }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchSettings = async () => {
setIsLoading(true);
try {
const response = await networkService.getAppSettings();
setSettingsData(response?.data?.data);
} catch (error) {
createToast({ body: "Failed to fetch settings" });
setError(error);
} finally {
setIsLoading(false);
}
};
fetchSettings();
}, []);
return [isLoading, error];
};
const useSaveSettings = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
const saveSettings = async (settings) => {
setIsLoading(true);
try {
await networkService.updateAppSettings({ settings });
if (settings.checkTTL) {
await networkService.updateChecksTTL({
ttl: settings.checkTTL,
});
}
createToast({ body: t("settingsSuccessSaved") });
} catch (error) {
createToast({ body: t("settingsFailedToSave") });
setError(error);
} finally {
setIsLoading(false);
}
};
return [isLoading, error, saveSettings];
};
export { useFetchSettings, useSaveSettings };
@@ -68,8 +68,16 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
),
},
{ id: "cpu", content: t("cpu"), render: (row) => <CustomGauge progress={row.cpu} /> },
{ id: "memory", content: t("memory"), render: (row) => <CustomGauge progress={row.mem} /> },
{ id: "disk", content: t("disk"), render: (row) => <CustomGauge progress={row.disk} /> },
{
id: "memory",
content: t("memory"),
render: (row) => <CustomGauge progress={row.mem} />,
},
{
id: "disk",
content: t("disk"),
render: (row) => <CustomGauge progress={row.disk} />,
},
{
id: "actions",
content: t("actions"),
@@ -129,6 +137,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
},
},
onRowClick: (row) => openDetails(row.id),
emptyView: "No monitors found",
}}
/>
);
@@ -73,7 +73,7 @@ const InfrastructureMonitors = () => {
);
}
if (!isLoading && monitors?.length === 0) {
if (!isLoading && typeof summary?.totalMonitors === "undefined" ) {
return (
<Fallback
vowelStart={true}
@@ -0,0 +1,32 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import Link from "../../Components/Link";
const SettingsAbout = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {2.0}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
);
};
export default SettingsAbout;
@@ -0,0 +1,90 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import Dialog from "../../Components/Dialog";
import { useState } from "react";
const SettingsDemoMonitors = ({ HEADER_SX, handleChange, isLoading }) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={() => {
const syntheticEvent = {
target: {
name: "demo",
},
};
handleChange(syntheticEvent);
}}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading}
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteMonitors",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={isLoading}
/>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;
+126
View File
@@ -0,0 +1,126 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
import TextInput from "../../Components/Inputs/TextInput";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
const SettingsEmail = ({
HEADER_SX,
handleChange,
settingsData,
setSettingsData,
isPasswordSet,
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [password, setPassword] = useState("");
const [hasBeenReset, setHasBeenReset] = useState(false);
const handlePasswordChange = (e) => {
setPassword(e.target.value);
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
});
};
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsEmail")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
</Box>
<Box>
<Stack gap={theme.spacing(10)}>
<Box>
<Typography>{t("settingsEmailHost")}</Typography>
<TextInput
name="systemEmailHost"
placeholder="smtp.gmail.com"
value={settingsData?.settings?.systemEmailHost ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailPort")}</Typography>
<TextInput
name="systemEmailPort"
placeholder="425"
type="number"
value={settingsData?.settings?.systemEmailPort ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailUser")}</Typography>
<TextInput
name="systemEmailUser"
placeholder="Leave empty if not required"
value={settingsData?.settings?.systemEmailUser ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailAddress")}</Typography>
<TextInput
name="systemEmailAddress"
placeholder="uptime@bluewavelabs.ca"
value={settingsData?.settings?.systemEmailAddress ?? ""}
onChange={handleChange}
/>
</Box>
{(isPasswordSet === false || hasBeenReset === true) && (
<Box>
<Typography>{t("settingsEmailPassword")}</Typography>
<TextInput
name="systemEmailPassword"
type="password"
placeholder="123 456 789 101112"
value={password}
onChange={handlePasswordChange}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
)}
{isPasswordSet === true && hasBeenReset === false && (
<Box>
<Typography>{t("settingsEmailFieldResetLabel")}</Typography>
<Button
onClick={() => {
setPassword("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</Box>
</ConfigBox>
);
};
SettingsEmail.propTypes = {
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
isPasswordSet: PropTypes.bool,
};
export default SettingsEmail;
@@ -0,0 +1,87 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ConfigBox from "../../Components/ConfigBox";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
const SettingsPagespeed = ({
HEADING_SX,
settingsData,
setSettingsData,
isApiKeySet,
}) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [apiKey, setApiKey] = useState("");
const [hasBeenReset, setHasBeenReset] = useState(false);
// Handler
const handleChange = (e) => {
setApiKey(e.target.value);
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
});
};
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
{(isApiKeySet === false || hasBeenReset === true) && (
<TextInput
name="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={apiKey}
type={"password"}
onChange={handleChange}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
/>
)}
{isApiKeySet === true && hasBeenReset === false && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button
onClick={() => {
setApiKey("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
);
};
SettingsPagespeed.propTypes = {
HEADING_SX: PropTypes.object,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
isApiKeySet: PropTypes.bool,
};
export default SettingsPagespeed;
@@ -0,0 +1,84 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import ConfigBox from "../../Components/ConfigBox";
import TextInput from "../../Components/Inputs/TextInput";
import Dialog from "../../Components/Dialog";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useState } from "react";
const SettingsStats = ({ HEADING_SX, handleChange, settingsData, errors }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
return (
<ConfigBox>
<Box>
<Typography
component="h1"
sx={HEADING_SX}
>
{t("settingsHistoryAndMonitoring")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="checkTTL"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={settingsData?.settings?.checkTTL ?? ""}
onChange={handleChange}
type="number"
error={errors.checkTTL ? true : false}
helperText={errors.checkTTL}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteStats",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={false}
/>
</ConfigBox>
);
};
SettingsStats.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
settingsData: PropTypes.object,
errors: PropTypes.object,
};
export default SettingsStats;
@@ -0,0 +1,43 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
import Select from "../../Components/Inputs/Select";
import timezones from "../../Utils/timezones.json";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
label={t("settingsDisplayTimezone")}
name="timezone"
value={timezone}
onChange={handleChange}
items={timezones}
/>
</Stack>
</ConfigBox>
);
};
SettingsTimeZone.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
timezone: PropTypes.string,
};
export default SettingsTimeZone;
+52
View File
@@ -0,0 +1,52 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
import Select from "../../Components/Inputs/Select";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
const { t, i18n } = useTranslation();
const theme = useTheme();
const languages = Object.keys(i18n.options.resources || {});
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="mode"
label={t("settingsThemeMode")}
value={mode}
onChange={handleChange}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
name="language"
label={t("settingsLanguage")}
value={language}
onChange={handleChange}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
);
};
SettingsUI.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
mode: PropTypes.string,
language: PropTypes.string,
};
export default SettingsUI;
+150 -464
View File
@@ -1,116 +1,69 @@
// Components
import { Box, Stack, Typography, Button } from "@mui/material";
import TextInput from "../../Components/Inputs/TextInput";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import Dialog from "../../Components/Dialog";
import ConfigBox from "../../Components/ConfigBox";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { getAppSettings } from "../../Features/Settings/settingsSlice";
// import {
// WalletMultiButton,
// WalletDisconnectButton,
// } from "@solana/wallet-adapter-react-ui";
//Utils
import { useTheme } from "@emotion/react";
import { logger } from "../../Utils/Logger";
import { useDispatch, useSelector } from "react-redux";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../Components/Breadcrumbs";
import SettingsTimeZone from "./SettingsTimeZone";
import SettingsUI from "./SettingsUI";
import SettingsPagespeed from "./SettingsPagespeed";
import SettingsDemoMonitors from "./SettingsDemoMonitors";
import SettingsAbout from "./SettingsAbout";
import SettingsEmail from "./SettingsEmail";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "../../Validation/validation";
import { createToast } from "../../Utils/toastUtils";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
import SettingsStats from "./SettingsStats";
import {
deleteMonitorChecksByTeamId,
addDemoMonitors,
deleteAllMonitors,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { update } from "../../Features/Auth/authSlice";
import PropTypes from "prop-types";
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState, useEffect } from "react";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
import { updateAppSettings } from "../../Features/Settings/settingsSlice";
import { useTranslation } from "react-i18next";
import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings";
import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats";
// Constants
const SECONDS_PER_DAY = 86400;
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => {
const theme = useTheme();
const { t, i18n } = useTranslation();
const isAdmin = useIsAdmin();
// Redux state
const { mode, language, timezone } = useSelector((state) => state.ui);
const { user } = useSelector((state) => state.auth);
const { language } = useSelector((state) => state.ui);
const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
const { mode } = useSelector((state) => state.ui);
const { pagespeedApiKey } = useSelector((state) => state.settings);
const [checksIsLoading, setChecksIsLoading] = useState(false);
const [form, setForm] = useState({
enableDistributedUptime: distributedUptimeEnabled,
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
pagespeedApiKey: pagespeedApiKey,
// Local state
const [settingsData, setSettingsData] = useState({});
const [errors, setErrors] = useState({});
// Network
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
});
const [version, setVersion] = useState("unknown");
const [apiKeyFieldType, setApiKeyFieldType] = useState("password");
const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false);
const [tempPSKey, setTempPSKey] = useState("");
const [errors, setErrors] = useState({});
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
const [isSaving, saveError, saveSettings] = useSaveSettings();
const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats();
// Setup
const theme = useTheme();
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
//Fetching latest release version from github
useEffect(() => {
const fetchLatestVersion = async () => {
let version = "unknown";
try {
const response = await networkService.fetchGithubLatestRelease();
if (!response.status === 200) {
throw new Error("Failed to fetch latest version");
}
version = response.data.tag_name;
} catch (error) {
createToast({ body: error.message || "Error fetching latest version" }); // Set error message
} finally {
setVersion(version);
}
// Handlers
const handleChange = async (e) => {
const { name, value } = e.target;
// Build next state early
const newSettingsData = {
...settingsData,
settings: { ...settingsData.settings, [name]: value },
};
fetchLatestVersion();
}, []);
useEffect(() => {
dispatch(getAppSettings());
}, [dispatch]);
const handleChange = (event) => {
const { type, checked, value, id } = event.target;
if (type === "checkbox") {
setForm((prev) => ({
...prev,
[id]: checked,
}));
return;
}
if (id === "pagespeedApiKey") {
setTempPSKey(value);
return;
}
let inputValue = value;
if (id === "ttl") {
inputValue = value.replace(/[^0-9]/g, "");
}
const updatedForm = { ...form, [id]: inputValue };
const { error } = settingsValidation.validate(updatedForm, { abortEarly: false });
// Validate
const { error } = settingsValidation.validate(newSettingsData.settings, {
abortEarly: false,
});
if (!error || error.details.length === 0) {
setErrors({});
} else {
@@ -119,397 +72,130 @@ const Settings = () => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
logger.error("Validation errors:", error.details);
}
setForm(updatedForm);
};
// TODO Handle saving
const handleSave = async () => {
try {
setChecksIsLoading(true);
await networkService.updateChecksTTL({
ttl: form.ttl,
});
const updatedUser = { ...user, checkTTL: form.ttl };
const [userAction, settingsAction] = await Promise.all([
dispatch(update({ localData: updatedUser })),
dispatch(
updateAppSettings({
settings: {
language: language,
pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey,
},
})
),
]);
if (userAction.payload.success && settingsAction.payload.success) {
createToast({ body: t("settingsSuccessSaved") });
} else {
throw new Error("Failed to save settings");
}
} catch (error) {
createToast({ body: t("settingsFailedToSave") });
} finally {
setChecksIsLoading(false);
if (name === "timezone") {
dispatch(setTimezone({ timezone: value }));
}
};
const handleClearStats = async () => {
try {
const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId }));
if (deleteMonitorChecksByTeamId.fulfilled.match(action)) {
createToast({ body: t("settingsStatsCleared") });
} else {
createToast({ body: t("settingsFailedToClearStats") });
}
} catch (error) {
logger.error(error);
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsOpen(deleteStatsMonitorsInitState);
if (name === "mode") {
dispatch(setMode(value));
}
};
const handleInsertDemoMonitors = async () => {
try {
const action = await dispatch(addDemoMonitors());
if (addDemoMonitors.fulfilled.match(action)) {
createToast({ body: t("settingsDemoMonitorsAdded") });
} else {
if (name === "language") {
dispatch(setLanguage(value));
i18n.changeLanguage(value);
}
if (name === "deleteStats") {
await deleteMonitorStats({ teamId: user.teamId });
return;
}
if (name === "demo") {
try {
const action = await dispatch(addDemoMonitors());
if (addDemoMonitors.fulfilled.match(action)) {
createToast({ body: t("settingsDemoMonitorsAdded") });
} else {
createToast({ body: t("settingsFailedToAddDemoMonitors") });
}
} catch (error) {
createToast({ body: t("settingsFailedToAddDemoMonitors") });
}
} catch (error) {
logger.error(error);
createToast({ Body: t("settingsFailedToAddDemoMonitors") });
return;
}
};
const handleDeleteAllMonitors = async () => {
try {
const action = await dispatch(deleteAllMonitors());
if (deleteAllMonitors.fulfilled.match(action)) {
createToast({ body: t("settingsMonitorsDeleted") });
} else {
if (name === "deleteMonitors") {
try {
const action = await dispatch(deleteAllMonitors());
if (deleteAllMonitors.fulfilled.match(action)) {
createToast({ body: t("settingsMonitorsDeleted") });
} else {
createToast({ body: t("settingsFailedToDeleteMonitors") });
}
} catch (error) {
createToast({ body: t("settingsFailedToDeleteMonitors") });
}
} catch (error) {
logger.error(error);
createToast({ Body: t("settingsFailedToDeleteMonitors") });
} finally {
setIsOpen(deleteStatsMonitorsInitState);
return;
}
setSettingsData(newSettingsData);
};
const handleResetApiKey = () => {
setIsApiKeySet(false);
setForm((prev) => ({
...prev,
pagespeedApiKey: "",
}));
const handleSave = () => {
const { error } = settingsValidation.validate(settingsData.settings, {
abortEarly: false,
});
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
}
saveSettings(settingsData?.settings);
};
const languages = Object.keys(i18n.options.resources || {});
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">Settings</Typography>
<SettingsTimeZone
HEADING_SX={HEADING_SX}
handleChange={handleChange}
timezone={timezone}
/>
<SettingsUI
HEADING_SX={HEADING_SX}
handleChange={handleChange}
mode={mode}
language={language}
/>
<SettingsPagespeed
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
isApiKeySet={settingsData?.pagespeedKeySet ?? false}
/>
<SettingsStats
HEADING_SX={HEADING_SX}
settingsData={settingsData}
handleChange={handleChange}
errors={errors}
/>
<SettingsDemoMonitors
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={isSettingsLoading || isSaving || isDeletingMonitorStats}
/>
<SettingsEmail
HEADER_SX={HEADING_SX}
handleChange={handleChange}
settingsData={settingsData}
setSettingsData={setSettingsData}
isPasswordSet={settingsData?.emailPasswordSet ?? false}
/>
<SettingsAbout />
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
direction="row"
justifyContent="flex-end"
>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="display-timezones"
label={t("settingsDisplayTimezone")}
value={timezone}
onChange={(e) => {
dispatch(setTimezone({ timezone: e.target.value }));
}}
items={timezones}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsAppearanceDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="theme-mode"
label={t("settingsThemeMode")}
value={mode}
onChange={(e) => {
dispatch(setMode(e.target.value));
}}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
id="language"
label={t("settingsLanguage")}
value={language}
onChange={(e) => {
dispatch(setLanguage(e.target.value));
i18n.changeLanguage(e.target.value);
}}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsDistributedUptimeDescription")}
</Typography>
</Box>
<Box>
<Switch
id="enableDistributedUptime"
color="accent"
checked={distributedUptimeEnabled}
onChange={(e) => {
dispatch(setDistributedUptimeEnabled(e.target.checked));
}}
/>
{distributedUptimeEnabled === true
? t("settingsEnabled")
: t("settingsDisabled")}
</Box>
</ConfigBox>
)} */}
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("pageSpeedApiKeyFieldDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={form.pagespeedApiKey ? form.pagespeedApiKey : tempPSKey}
type={apiKeyFieldType}
onChange={handleChange}
disabled={isApiKeySet}
optionalLabel="(Optional)"
error={!!errors.pagespeedApiKey}
helperText={errors.pagespeedApiKey}
endAdornment={
!isApiKeySet && (
<PasswordEndAdornment
fieldType={apiKeyFieldType}
setFieldType={setApiKeyFieldType}
/>
)
}
/>
{isApiKeySet && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button
onClick={handleResetApiKey}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsWalletDescription")}
</Typography>
</Box>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "flex-start",
gap: 2,
}}
>
<Stack
direction="row"
spacing={2}
>
<WalletMultiButton />
<WalletDisconnectButton />
</Stack>
</Box>
</ConfigBox>
)} */}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsHistoryAndMonitoring")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="ttl"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={form.ttl}
onChange={handleChange}
error={errors.ttl ? true : false}
helperText={errors.ttl}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteStats: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen.deleteStats}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={handleClearStats}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
)}
{isAdmin && (
<>
{/* Demo Monitors Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleInsertDemoMonitors}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
{/* System Reset Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteMonitors: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
</>
)}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {version}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
<Button
loading={isSaving || isDeletingMonitorStats || isSettingsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
<Button
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
{t("settingsSave")}
</Button>
</Stack>
{t("settingsSave")}
</Button>
</Stack>
</Box>
</Stack>
);
};
Settings.propTypes = {
isAdmin: PropTypes.bool,
};
export default Settings;
+5 -2
View File
@@ -420,7 +420,9 @@ const Configure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -491,6 +493,7 @@ const Configure = () => {
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: 0 }}
control={
<Switch
name="ignore-error"
@@ -611,7 +614,7 @@ const Configure = () => {
isLoading={isLoading}
/>
<NotificationIntegrationModal
<NotificationIntegrationModal
open={isNotificationModalOpen}
onClose={handleClosenNotificationModal}
monitor={monitor}
+1
View File
@@ -422,6 +422,7 @@ const CreateMonitor = () => {
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: 0 }}
control={
<Switch
name="ignore-error"
+1
View File
@@ -253,6 +253,7 @@ const baseTheme = (palette) => ({
MuiTableCell: {
styleOverrides: {
root: ({ theme }) => ({
fontSize: typographyLevels.base,
borderBottomColor: theme.palette.primary.lowContrast,
}),
},
+31 -21
View File
@@ -11,7 +11,8 @@ const nameSchema = joi
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
"string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
"string.pattern.base":
"Name must contain only letters, spaces, apostrophes, or hyphens",
});
const lastnameSchema = joi
@@ -163,20 +164,21 @@ const monitorValidation = joi.object({
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.pattern.base": "Please enter a valid container ID.",
}),
port: joi.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
then: joi.required().messages({
"number.base": "Port must be a number.",
"number.min": "Port must be at least 1.",
"number.max": "Port must be at most 65535.",
"any.required": "Port is required for port monitors.",
port: joi
.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
then: joi.required().messages({
"number.base": "Port must be a number.",
"number.min": "Port must be at least 1.",
"number.max": "Port must be at most 65535.",
"any.required": "Port is required for port monitors.",
}),
otherwise: joi.optional(),
}),
otherwise: joi.optional(),
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -264,14 +266,19 @@ const statusPageValidation = joi.object({
showCharts: joi.boolean(),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
checkTTL: joi.number().required().messages({
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
"any.required": "Please enter a value"
"any.required": "Please enter a value",
}),
pagespeedApiKey: joi.string().allow("").optional(),
})
.unknown(true);
language: joi.string().required(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(null, ""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
@@ -374,9 +381,12 @@ const infrastructureMonitorValidation = joi.object({
notifications: joi.array().items(
joi.object({
type: joi.string().valid("email").required(),
address: joi.string().email({ tlds: { allow: false } }).required(),
address: joi
.string()
.email({ tlds: { allow: false } })
.required(),
})
)
),
});
export {
@@ -388,5 +398,5 @@ export {
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation
logoImageValidation,
};
+514 -504
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -24,8 +24,6 @@ services:
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -38,8 +36,6 @@ services:
image: uptime_mongo:latest
restart: always
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
healthcheck:
+1 -5
View File
@@ -14,7 +14,7 @@ services:
image: ghcr.io/bluewave-labs/checkmate:backend-dist
restart: always
ports:
- "5000:5000"
- "52345:52345"
depends_on:
- redis
- mongodb
@@ -27,8 +27,6 @@ services:
redis:
image: ghcr.io/bluewave-labs/checkmate:redis-dist
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -43,8 +41,6 @@ services:
volumes:
- ./mongo/data:/data/db
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
interval: 5s
-4
View File
@@ -36,8 +36,6 @@ services:
redis:
image: ghcr.io/bluewave-labs/checkmate:redis-demo
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -50,8 +48,6 @@ services:
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
restart: always
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
# - ./mongo/init/init.js:/docker-entrypoint-initdb.d/init.js // No longer needed
+1 -2
View File
@@ -649,9 +649,8 @@ class MonitorController {
sendTestEmail = async (req, res, next) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
return res.error({ msg: this.stringService.errorForValidEmailAddress });
throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
+15 -2
View File
@@ -13,12 +13,25 @@ class SettingsController {
getAppSettings = async (req, res, next) => {
const dbSettings = await this.settingsService.getDBSettings();
const sanitizedSettings = { ...dbSettings };
const returnSettings = {
pagespeedKeySet: false,
emailPasswordSet: false,
};
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
sanitizedSettings.pagespeedApiKey = "********";
returnSettings.pagespeedKeySet = true;
delete sanitizedSettings.pagespeedApiKey;
}
if (typeof sanitizedSettings.systemEmailPassword !== "undefined") {
returnSettings.emailPasswordSet = true;
delete sanitizedSettings.systemEmailPassword;
}
returnSettings.settings = sanitizedSettings;
return res.success({
msg: this.stringService.getAppSettings,
data: sanitizedSettings,
data: returnSettings,
});
};
+7 -1
View File
@@ -2,13 +2,16 @@ import mongoose from "mongoose";
const AppSettingsSchema = mongoose.Schema(
{
checkTTL: {
type: Number,
default: 30,
},
language: {
type: String,
default: "gb",
},
pagespeedApiKey: {
type: String,
default: "",
},
systemEmailHost: {
type: String,
@@ -22,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema(
systemEmailPassword: {
type: String,
},
systemEmailUser: {
type: String,
},
singleton: {
type: Boolean,
required: true,
+16 -6
View File
@@ -14,12 +14,22 @@ const getAppSettings = async () => {
const updateAppSettings = async (newSettings) => {
try {
console.log(newSettings);
const settings = await AppSettings.findOneAndUpdate(
{},
{ $set: newSettings },
{ new: true, upsert: true }
);
const update = { $set: { ...newSettings } };
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
if (newSettings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
const settings = await AppSettings.findOneAndUpdate({}, update, {
new: true,
upsert: true,
});
return settings;
} catch (error) {
error.service = SERVICE_NAME;
+27 -22
View File
@@ -29,7 +29,10 @@ class EmailService {
this.mjml2html = mjml2html;
this.nodemailer = nodemailer;
this.logger = logger;
this.init();
}
init = async () => {
/**
* Loads an email template from the filesystem.
*
@@ -67,34 +70,14 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
testEmailTemplate: this.loadTemplate("testEmailTemplate")
testEmailTemplate: this.loadTemplate("testEmailTemplate"),
};
/**
* The email transporter used to send emails.
* @type {Object}
*/
const {
systemEmailHost,
systemEmailPort,
systemEmailUser,
systemEmailAddress,
systemEmailPassword,
} = this.settingsService.getSettings();
const emailConfig = {
host: systemEmailHost,
port: systemEmailPort,
secure: true,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: systemEmailPassword,
},
};
this.transporter = this.nodemailer.createTransport(emailConfig);
}
};
/**
* Asynchronously builds and sends an email using a specified template and context.
@@ -106,6 +89,28 @@ class EmailService {
* @returns {Promise<string>} A promise that resolves to the messageId of the sent email.
*/
buildAndSendEmail = async (template, context, to, subject) => {
// TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change
const {
systemEmailHost,
systemEmailPort,
systemEmailUser,
systemEmailAddress,
systemEmailPassword,
} = await this.settingsService.getDBSettings();
const emailConfig = {
host: systemEmailHost,
port: systemEmailPort,
secure: true,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: systemEmailPassword,
},
connectionTimeout: 5000,
};
this.transporter = this.nodemailer.createTransport(emailConfig);
const buildHtml = async (template, context) => {
try {
const mjml = this.templateLookup[template](context);
+11 -1
View File
@@ -59,7 +59,17 @@ class SettingsService {
}
async getDBSettings() {
const settings = await this.appSettings.findOne({ singleton: true }).lean();
let settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
if (settings === null) {
await this.appSettings.create({});
settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
}
return settings;
}
}
+2 -9
View File
@@ -424,21 +424,14 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({
// SettingsValidation
//****************************************
const updateAppSettingsBodyValidation = joi.object({
apiBaseUrl: joi.string().allow(""),
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
clientHost: joi.string().allow(""),
dbType: joi.string().allow(""),
dbConnectionString: joi.string().allow(""),
redisHost: joi.string().allow(""),
redisPort: joi.number().allow(null, ""),
redisUrl: joi.string().allow(""),
jwtTTL: joi.string().allow(""),
checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
});
//****************************************