diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js
index f12758715..f6d40c38a 100644
--- a/Client/src/Features/Auth/authSlice.js
+++ b/Client/src/Features/Auth/authSlice.js
@@ -67,7 +67,7 @@ export const update = createAsyncThunk(
fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser(token, user._id, fd);
-
+ console.log(res);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx
index a9f11f072..ac25c480d 100644
--- a/Client/src/Pages/Settings/index.jsx
+++ b/Client/src/Pages/Settings/index.jsx
@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
-import { Box, Stack, styled, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
@@ -12,20 +12,92 @@ import {
addDemoMonitors,
deleteAllMonitors,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import { update } from "../../Features/Auth/authSlice";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
+import { useState } from "react";
+import { ConfigBox } from "./styled";
+import { networkService } from "../../main";
+import { settingsValidation } from "../../Validation/validation";
+
+const SECONDS_PER_DAY = 86400;
const Settings = ({ isAdmin }) => {
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
+ const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
+ const { isLoading: authIsLoading } = useSelector((state) => state.auth);
const { timezone } = useSelector((state) => state.ui);
-
+ const [checksIsLoading, setChecksIsLoading] = useState(false);
+ const [form, setForm] = useState({
+ ttl: (checkTTL / SECONDS_PER_DAY).toString(),
+ });
+ const [errors, setErrors] = useState({});
const dispatch = useDispatch();
+
+ const handleChange = (event) => {
+ const { value, id } = event.target;
+ const { error } = settingsValidation.validate(
+ { [id]: value },
+ {
+ abortEarly: false,
+ }
+ );
+ if (!error || error.details.length === 0) {
+ setErrors({});
+ } else {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ console.log(newErrors);
+ logger.error("Validation errors:", error.details);
+ }
+ let inputValue = value;
+ id === "ttl" && (inputValue = value.replace(/[^0-9]/g, ""));
+ setForm((prev) => ({
+ ...prev,
+ [id]: inputValue,
+ }));
+ };
+
// TODO Handle saving
- const handleSave = async () => {};
+ const handleSave = async () => {
+ try {
+ setChecksIsLoading(true);
+ await networkService.updateChecksTTL(authToken, form.ttl);
+ const updatedUser = { ...user, checkTTL: form.ttl };
+ const action = await dispatch(
+ update({ authToken, localData: updatedUser })
+ );
+ if (action.payload.success) {
+ createToast({
+ body: "Settings saved successfully",
+ });
+ } else {
+ if (action.payload) {
+ // dispatch errors
+ createToast({
+ body: action.payload.msg,
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: "Unknown error.",
+ });
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ createToast({ body: "Failed to save settings" });
+ } finally {
+ setChecksIsLoading(false);
+ }
+ };
const handleClearStats = async () => {
try {
@@ -72,33 +144,6 @@ const Settings = ({ isAdmin }) => {
}
};
- const ConfigBox = styled("div")({
- display: "flex",
- flexDirection: "row",
- justifyContent: "space-between",
- gap: theme.spacing(20),
- paddingTop: theme.spacing(12),
- paddingInline: theme.spacing(15),
- paddingBottom: theme.spacing(25),
- backgroundColor: theme.palette.background.main,
- border: 1,
- borderStyle: "solid",
- borderColor: theme.palette.border.light,
- borderRadius: theme.spacing(2),
- "& > div:first-of-type": {
- flex: 0.7,
- },
- "& > div:last-of-type": {
- flex: 1,
- },
- "& h1, & h2": {
- color: theme.palette.text.secondary,
- },
- "& p": {
- color: theme.palette.text.tertiary,
- },
- });
-
return (
{
logger.warn("Disabled")}
+ value={form.ttl}
+ onChange={handleChange}
+ error={errors.ttl}
/>
Clear all stats. This is irreversible.
@@ -181,7 +224,7 @@ const Settings = ({ isAdmin }) => {
@@ -193,7 +236,7 @@ const Settings = ({ isAdmin }) => {
@@ -223,7 +266,8 @@ const Settings = ({ isAdmin }) => {
0}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
diff --git a/Client/src/Pages/Settings/styled.jsx b/Client/src/Pages/Settings/styled.jsx
new file mode 100644
index 000000000..19da4815b
--- /dev/null
+++ b/Client/src/Pages/Settings/styled.jsx
@@ -0,0 +1,28 @@
+import { Stack, styled } from "@mui/material";
+
+export const ConfigBox = styled(Stack)(({ theme }) => ({
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ gap: theme.spacing(20),
+ paddingTop: theme.spacing(12),
+ paddingInline: theme.spacing(15),
+ paddingBottom: theme.spacing(25),
+ backgroundColor: theme.palette.background.main,
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.border.light,
+ borderRadius: theme.spacing(2),
+ "& > div:first-of-type": {
+ flex: 0.7,
+ },
+ "& > div:last-of-type": {
+ flex: 1,
+ },
+ "& h1, & h2": {
+ color: theme.palette.text.secondary,
+ },
+ "& p": {
+ color: theme.palette.text.tertiary,
+ },
+}));
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index a9d93b301..683ce1aba 100644
--- a/Client/src/Utils/NetworkService.js
+++ b/Client/src/Utils/NetworkService.js
@@ -582,6 +582,19 @@ class NetworkService {
}
);
}
+
+ async updateChecksTTL(authToken, ttl) {
+ return this.axiosInstance.put(
+ `/checks/ttl`,
+ { ttl },
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
}
export default NetworkService;
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js
index 948c228cc..3d11482d1 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -146,9 +146,16 @@ const maintenanceWindowValidation = joi.object({
}),
});
+const settingsValidation = joi.object({
+ ttl: joi.number().required().messages({
+ "string.empty": "TTL is required",
+ }),
+});
+
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
+ settingsValidation,
};
diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js
index 49dfad1af..591703f57 100644
--- a/Server/controllers/checkController.js
+++ b/Server/controllers/checkController.js
@@ -10,6 +10,8 @@ const {
updateChecksTTLBodyValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
+const jwt = require("jsonwebtoken");
+const { getTokenFromHeaders } = require("../utils/utils");
const SERVICE_NAME = "checkController";
const createCheck = async (req, res, next) => {
@@ -144,6 +146,8 @@ const deleteChecksByTeamId = async (req, res, next) => {
};
const updateChecksTTL = async (req, res, next) => {
+ const SECONDS_PER_DAY = 86400;
+
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
@@ -157,8 +161,11 @@ const updateChecksTTL = async (req, res, next) => {
}
try {
- const ttl = req.body.ttl;
- await req.db.updateChecksTTL(1, ttl);
+ // Get user's teamId
+ const token = getTokenFromHeaders(req.headers);
+ const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
+ const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
+ await req.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,
diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js
index 845a431ee..4bc405835 100644
--- a/Server/db/mongo/modules/checkModule.js
+++ b/Server/db/mongo/modules/checkModule.js
@@ -1,5 +1,6 @@
const Check = require("../../../models/Check");
const Monitor = require("../../../models/Monitor");
+const User = require("../../../models/user");
const logger = require("../../../utils/logger");
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
@@ -248,6 +249,14 @@ const deleteChecksByTeamId = async (teamId) => {
const updateChecksTTL = async (teamId, ttl) => {
try {
await Check.collection.dropIndex("expiry_1");
+ } catch (error) {
+ logger.error("Failed to drop index", {
+ service: SERVICE_NAME,
+ method: "updateChecksTTL",
+ });
+ }
+
+ try {
await Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
@@ -257,6 +266,14 @@ const updateChecksTTL = async (teamId, ttl) => {
error.method = "updateChecksTTL";
throw error;
}
+ // Update user
+ try {
+ await User.updateMany({ teamId: teamId }, { checkTTL: ttl });
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "updateChecksTTL";
+ throw error;
+ }
};
module.exports = {
diff --git a/Server/models/user.js b/Server/models/user.js
index 888bfd7eb..e56840c48 100644
--- a/Server/models/user.js
+++ b/Server/models/user.js
@@ -45,6 +45,9 @@ const UserSchema = mongoose.Schema(
ref: "Team",
immutable: true,
},
+ checkTTL: {
+ type: Number,
+ },
},
{
timestamps: true,