From 0ad08f431ee84b6ed2d5baf549a9247fa714c66f Mon Sep 17 00:00:00 2001 From: Mohammad Khalilzadeh Date: Tue, 20 Aug 2024 20:33:28 +0330 Subject: [PATCH 01/35] Marshali data, logging it, creating post req, send to BE --- Client/package-lock.json | 2 +- Client/package.json | 2 +- .../CreateMaintenanceWindow/index.jsx | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 44e276f63..2f1543bba 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -18,7 +18,7 @@ "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "@reduxjs/toolkit": "2.2.5", - "axios": "1.7.4", + "axios": "^1.7.4", "chart.js": "^4.4.3", "dayjs": "1.11.11", "joi": "17.13.1", diff --git a/Client/package.json b/Client/package.json index 769e1f4a6..22f15f7f9 100644 --- a/Client/package.json +++ b/Client/package.json @@ -20,7 +20,7 @@ "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "@reduxjs/toolkit": "2.2.5", - "axios": "1.7.4", + "axios": "^1.7.4", "chart.js": "^4.4.3", "dayjs": "1.11.11", "joi": "17.13.1", diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx index 8fff6e4a1..a5cbfaf56 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -10,6 +10,7 @@ import dayjs from "dayjs"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; import Field from "../../../Components/Inputs/Field"; +import axios from "axios"; const directory = { title: "Create a maintenance window", @@ -71,6 +72,29 @@ const CreateNewMaintenanceWindow = () => { })); }; + const handleSubmit = async () => { + const data = { + repeat: values.repeat, + date: values.date.format("YYYY-MM-DD"), + startTime: values.startTime.format("HH:mm"), + duration: values.duration, + unit: values.unit, + friendlyName: values.friendlyName, + addMonitors: values.AddMonitors, + }; + + console.log("Submitting data: ", data); + + await axios + .post("/address-of-the-route", data) + .then((res) => { + console.log("Response: ", res.data); + }) + .catch((err) => { + console.log("Error: ", err); + }); + }; + const configOptions = [ { title: "Repeat", @@ -246,6 +270,7 @@ const CreateNewMaintenanceWindow = () => { }} level="primary" label="Create" + onClick={handleSubmit} /> From b247fabde0168a8cf639a24f9a1f1d3161edd796 Mon Sep 17 00:00:00 2001 From: Mohammad Khalilzadeh Date: Wed, 21 Aug 2024 09:40:09 +0330 Subject: [PATCH 02/35] adding validations and toasts as validation error --- .../CreateMaintenanceWindow/index.jsx | 33 ++++++++---- Client/src/Validation/validation.js | 52 +++++++++++++++---- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx index a5cbfaf56..c4789f883 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -10,7 +10,9 @@ import dayjs from "dayjs"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; import Field from "../../../Components/Inputs/Field"; -import axios from "axios"; +import { maintenanceWindowValidation } from "../../../Validation/validation"; +import { logger } from "../../../Utils/Logger"; +import { createToast } from "../../../Utils/toastUtils"; const directory = { title: "Create a maintenance window", @@ -63,6 +65,7 @@ const CreateNewMaintenanceWindow = () => { friendlyName: "", AddMonitors: "", }); + const [errors, setErrors] = useState({}); const handleChange = (event, name) => { const { value } = event.target; @@ -83,16 +86,26 @@ const CreateNewMaintenanceWindow = () => { addMonitors: values.AddMonitors, }; - console.log("Submitting data: ", data); - - await axios - .post("/address-of-the-route", data) - .then((res) => { - console.log("Response: ", res.data); - }) - .catch((err) => { - console.log("Error: ", err); + const { error } = maintenanceWindowValidation.validate(data, { + abortEarly: false, + }); + logger.log("error: ", error); + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; }); + setErrors(newErrors); + createToast({ + body: + error.details && error.details.length > 0 + ? error.details[0].message + : "Error validating data", + }); + logger.error("Validation errors:", error.details); + } + + logger.log("Submitting data: ", data); }; const configOptions = [ diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index ab2501199..6719c2c1c 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -72,14 +72,10 @@ const credentials = joi.object({ }); const monitorValidation = joi.object({ - url: joi - .string() - .uri({ allowRelative: true }) - .trim() - .messages({ - "string.empty": "This field is required.", - "string.uri": "The URL you provided is not valid.", - }), + url: joi.string().uri({ allowRelative: true }).trim().messages({ + "string.empty": "This field is required.", + "string.uri": "The URL you provided is not valid.", + }), name: joi.string().trim().max(50).allow("").messages({ "string.max": "This field should not exceed the 50 characters limit.", }), @@ -108,4 +104,42 @@ const imageValidation = joi.object({ }), }); -export { credentials, imageValidation, monitorValidation }; +const maintenanceWindowValidation = joi.object({ + repeat: joi.number().valid(1, 2, 3).required().messages({ + "number.base": "Repeat must be a number.", + "any.only": "Repeat must be one of [1, 2, 3].", + "any.required": "Repeat is required.", + }), + date: joi.date().required().messages({ + "date.base": "Date must be a valid date.", + "any.required": "Date is required.", + }), + startTime: joi.string().required().messages({ + "string.base": "Start time must be a valid time.", + "any.required": "Start time is required.", + }), + duration: joi.number().required().messages({ + "number.base": "Duration must be a number.", + "any.required": "Duration is required.", + }), + unit: joi.string().valid("minutes", "hours", "days").required().messages({ + "string.base": "Unit must be a string.", + "any.only": "Unit must be one of ['minutes', 'hours', 'days'].", + "any.required": "Unit is required.", + }), + friendlyName: joi.string().required().max(100).messages({ + "string.max": "Friendly name must be less than 100 characters.", + "any.required": "Friendly name is required.", + }), + addMonitors: joi.string().required().messages({ + "string.base": "Add monitors must be a string.", + "any.required": "Add monitors is required.", + }), +}); + +export { + credentials, + imageValidation, + monitorValidation, + maintenanceWindowValidation, +}; From aecddb22bbf34907bcf8e7b996a347f62b73829f Mon Sep 17 00:00:00 2001 From: Mohammad Khalilzadeh Date: Wed, 21 Aug 2024 22:50:42 +0330 Subject: [PATCH 03/35] validation helper texts and input type --- .../Maintenance/CreateMaintenanceWindow/index.jsx | 3 +++ Client/src/Validation/validation.js | 13 +++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx index c4789f883..eb19ba3d2 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -163,6 +163,7 @@ const CreateNewMaintenanceWindow = () => { placeholder="60" onChange={(e) => handleChange(e, "duration")} value={values.duration} + error={errors.duration} /> handleChange(e, "unit")} From d81bb8fe89c4bf10adef00ee0bb2ed9c8e7ddbe0 Mon Sep 17 00:00:00 2001 From: Mohammad Khalilzadeh Date: Thu, 22 Aug 2024 20:23:17 +0330 Subject: [PATCH 05/35] fixes --- Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css index e0311d4e7..4ee17db72 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css @@ -23,7 +23,7 @@ } .duration-config .field-text .MuiTextField-root, -.duration-config .field { +.maintenance-options .duration-config .field { width: 70px; min-width: 70px; max-width: 70px; From f5e589886b1284a3d61870e424a6bb7001f0c01f Mon Sep 17 00:00:00 2001 From: Mohammad Khalilzadeh Date: Sat, 24 Aug 2024 10:11:13 +0330 Subject: [PATCH 06/35] adding the type number --- Client/src/Components/Inputs/Field/index.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx index 0b2fe56fe..f44201e3f 100644 --- a/Client/src/Components/Inputs/Field/index.jsx +++ b/Client/src/Components/Inputs/Field/index.jsx @@ -177,7 +177,14 @@ const Field = forwardRef( ); Field.propTypes = { - type: PropTypes.oneOf(["text", "password", "url", "email", "description"]), + type: PropTypes.oneOf([ + "text", + "password", + "url", + "email", + "description", + "number", + ]), id: PropTypes.string.isRequired, label: PropTypes.string, https: PropTypes.bool, From 4b2973e308b352f5783f12fbdd135661cf2a8afd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 09:39:45 -0700 Subject: [PATCH 07/35] Add team schema --- Server/models/Team.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Server/models/Team.js diff --git a/Server/models/Team.js b/Server/models/Team.js new file mode 100644 index 000000000..33c81f10b --- /dev/null +++ b/Server/models/Team.js @@ -0,0 +1,12 @@ +const mongoose = require("mongoose"); + +const TeamSchema = mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + immutable: true, + }, + timestamps: true, +}); + +module.exports = mongoose.model("Team", TeamSchema); From ef02bd8078c433f7e62c699b32b44ec06c01b0b4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 09:46:23 -0700 Subject: [PATCH 08/35] Check for superadmin as opposed to admin --- Server/controllers/authController.js | 8 ++++---- Server/db/mongo/MongoDB.js | 6 +++--- Server/models/user.js | 2 +- Server/routes/authRoute.js | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 6fbcdb669..7c053ae56 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -293,13 +293,13 @@ const inviteVerifyController = async (req, res, next) => { * @returns {Promise} */ -const checkAdminController = async (req, res) => { +const checkSuperadminController = async (req, res) => { try { - const adminExists = await req.db.checkAdmin(req, res); + const superAdminExists = await req.db.checkSuperadmin(req, res); return res.status(200).json({ success: true, msg: successMessages.AUTH_ADMIN_EXISTS, - data: adminExists, + data: superAdminExists, }); } catch (error) { error.service = SERVICE_NAME; @@ -508,7 +508,7 @@ module.exports = { userEditController, inviteController, inviteVerifyController, - checkAdminController, + checkSuperadminController, recoveryRequestController, validateRecoveryTokenController, resetPasswordController, diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 9c1005c32..8c1d97351 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -15,9 +15,9 @@ const connect = async () => { } }; -const checkAdmin = async (req, res) => { +const checkSuperadmin = async (req, res) => { try { - const admin = await UserModel.findOne({ role: "admin" }); + const admin = await UserModel.findOne({ role: "superadmin" }); if (admin !== null) { return true; } @@ -141,7 +141,7 @@ module.exports = { requestRecoveryToken, validateRecoveryToken, resetPassword, - checkAdmin, + checkSuperadmin, getAllMonitors, getMonitorStatsById, getMonitorById, diff --git a/Server/models/user.js b/Server/models/user.js index 977cabc15..73567798c 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -38,7 +38,7 @@ const UserSchema = mongoose.Schema( role: { type: [String], default: "user", - enum: ["user", "admin"], + enum: ["user", "admin", "superadmin"], }, }, { diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 201807121..589c0f1ac 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -14,7 +14,7 @@ const { recoveryRequestController, validateRecoveryTokenController, resetPasswordController, - checkAdminController, + checkSuperadminController, getAllUsersController, deleteUserController, inviteController, @@ -30,7 +30,7 @@ router.put( verifyJWT, userEditController ); -router.get("/users/admin", checkAdminController); +router.get("/users/superadmin", checkSuperadminController); router.get("/users", verifyJWT, verifyAdmin, getAllUsersController); router.delete( "/user/:userId", From 4c2cc6fb2cbe8dbd63bb1203caf2d2415d295db1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 10:00:34 -0700 Subject: [PATCH 09/35] First user is superadmin instead of admin --- Client/src/HOC/withAdminCheck.jsx | 4 ++-- Client/src/Pages/Auth/Login.jsx | 2 +- Client/src/Pages/Auth/Register/Register.jsx | 23 ++++++++++++++------- Client/src/Utils/NetworkService.js | 4 ++-- Server/validation/joi.js | 11 ++++++---- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Client/src/HOC/withAdminCheck.jsx b/Client/src/HOC/withAdminCheck.jsx index 30a767f36..097f224e9 100644 --- a/Client/src/HOC/withAdminCheck.jsx +++ b/Client/src/HOC/withAdminCheck.jsx @@ -10,7 +10,7 @@ const withAdminCheck = (WrappedComponent) => { useEffect(() => { networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === true) { navigate("/login"); @@ -20,7 +20,7 @@ const withAdminCheck = (WrappedComponent) => { logger.error(error); }); }, [navigate]); - return ; + return ; }; const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || "Component"; diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index b962fbb9a..30b7f8f8a 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -301,7 +301,7 @@ const Login = () => { return; } networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === false) { navigate("/register"); diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 4680c1821..4dd8fad24 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -23,11 +23,11 @@ import { logger } from "../../../Utils/Logger"; * Displays the initial landing page. * * @param {Object} props - * @param {boolean} props.isAdmin - Whether the user is creating and admin account + * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click. * @returns {JSX.Element} */ -const LandingPage = ({ isAdmin, onSignup }) => { +const LandingPage = ({ isSuperAdmin, onSignup }) => { const theme = useTheme(); return ( @@ -40,7 +40,8 @@ const LandingPage = ({ isAdmin, onSignup }) => { Sign Up - Create your {isAdmin ? "admin " : ""}account to get started. + Create your {isSuperAdmin ? "Super admin " : ""}account to get + started. @@ -93,7 +94,7 @@ const LandingPage = ({ isAdmin, onSignup }) => { }; LandingPage.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, onSignup: PropTypes.func, }; @@ -422,7 +423,7 @@ StepThree.propTypes = { onBack: PropTypes.func, }; -const Register = ({ isAdmin }) => { +const Register = ({ isSuperAdmin }) => { const dispatch = useDispatch(); const navigate = useNavigate(); const { token } = useParams(); @@ -525,7 +526,10 @@ const Register = ({ isAdmin }) => { const handleStepThree = async (e) => { e.preventDefault(); - let registerForm = { ...form, role: isAdmin ? ["admin"] : form.role }; + let registerForm = { + ...form, + role: isSuperAdmin ? ["superadmin"] : form.role, + }; let error = validateForm(registerForm, { context: { password: form.password }, }); @@ -616,7 +620,10 @@ const Register = ({ isAdmin }) => { }} > {step === 0 ? ( - setStep(1)} /> + setStep(1)} + /> ) : step === 1 ? ( { ); }; Register.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, }; export default Register; diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index de4508cff..97071a7aa 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -293,8 +293,8 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async doesAdminExist() { - return this.axiosInstance.get("/auth/users/admin"); + async doesSuperAdminExist() { + return this.axiosInstance.get("/auth/users/superadmin"); } /** diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 4f8042e12..3a9b7e1a1 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -5,8 +5,11 @@ const joi = require("joi"); //**************************************** const roleValidatior = (role) => (value, helpers) => { - if (!value.includes(role)) { - throw new joi.ValidationError(`You do not have ${role} authorization`); + const hasRole = roles.some((role) => value.includes(role)); + if (!hasRole) { + throw new Joi.ValidationError( + `You do not have the required authorization. Required roles: ${roles.join(", ")}` + ); } return value; }; @@ -66,7 +69,7 @@ const registerValidation = joi.object({ profileImage: joi.any(), role: joi .array() - .items(joi.string().valid("admin", "user")) + .items(joi.string().valid("superadmin", "admin", "user")) .min(1) .required(), }); @@ -123,7 +126,7 @@ const deleteUserParamValidation = joi.object({ }); const inviteRoleValidation = joi.object({ - roles: joi.custom(roleValidatior("admin")).required(), + roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(), }); const inviteBodyValidation = joi.object({ From a6f5c96f93bdb2d68eb313dac8105d5d101e06c1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 10:23:05 -0700 Subject: [PATCH 10/35] Create team on superadmin create --- Server/db/mongo/modules/userModule.js | 13 +++++++++++++ Server/models/Team.js | 18 +++++++++++------- Server/models/user.js | 5 +++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index 3dacfd943..bc2f83958 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -1,4 +1,5 @@ const UserModel = require("../../../models/user"); +const TeamModel = require("../../../models/Team"); const { errorMessages } = require("../../../utils/messages"); const { GenerateAvatarImage } = require("../../../utils/imageProcessing"); @@ -27,7 +28,19 @@ const insertUser = async (req, res) => { const avatar = await GenerateAvatarImage(req.file); userData.avatarImage = avatar; } + + let teamId; + // Is user superadmin? If so, create team + if (userData.role.includes("superadmin")) { + const team = new TeamModel({ + email: userData.email, + }); + teamId = team._id; + await team.save(); + } + const newUser = new UserModel(userData); + newUser.teamId = teamId; await newUser.save(); return await UserModel.findOne({ _id: newUser._id }) .select("-password") diff --git a/Server/models/Team.js b/Server/models/Team.js index 33c81f10b..25e1f9b22 100644 --- a/Server/models/Team.js +++ b/Server/models/Team.js @@ -1,12 +1,16 @@ const mongoose = require("mongoose"); -const TeamSchema = mongoose.Schema({ - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, +const TeamSchema = mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + }, }, - timestamps: true, -}); + { + timestamps: true, + } +); module.exports = mongoose.model("Team", TeamSchema); diff --git a/Server/models/user.js b/Server/models/user.js index 73567798c..a6045547d 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -40,6 +40,11 @@ const UserSchema = mongoose.Schema( default: "user", enum: ["user", "admin", "superadmin"], }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, }, { timestamps: true, From c39eab49bd0639f98d24651fac3da4eeac8610dd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 13:29:11 -0700 Subject: [PATCH 11/35] Add teams to a user when they are created --- Client/src/Pages/Account/index.jsx | 8 ++++++-- Client/src/Pages/Auth/Register/Register.jsx | 9 +++++---- Client/src/Validation/validation.js | 1 + Server/controllers/authController.js | 3 ++- Server/db/mongo/modules/inviteModule.js | 1 + Server/db/mongo/modules/userModule.js | 2 ++ Server/models/InviteToken.js | 6 ++++++ Server/validation/joi.js | 4 +++- 8 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 2fed926b8..e731d231a 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -25,7 +25,11 @@ const Account = ({ open = "profile" }) => { let tabList = ["Profile", "Password", "Team"]; const { user } = useSelector((state) => state.auth); - if (!user.role.includes("admin")) tabList = ["Profile", "Password"]; + const requiredRoles = ["superadmin", "admin"]; + const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); + if (hideTeams) { + tabList = ["Profile", "Password"]; + } return ( { - {user.role.includes("admin") && } + {!hideTeams && } ); diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 4dd8fad24..39a5fdafd 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -444,24 +444,26 @@ const Register = ({ isSuperAdmin }) => { password: "", confirm: "", role: [], + teamId: "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); useEffect(() => { const fetchInvite = async () => { + console.log("FETCHING"); if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); - const { role, email } = res.data.data; - setForm({ ...form, email, role }); + const { role, email, teamId } = res.data.data; + setForm({ ...form, email, role, teamId }); } catch (error) { logger.error(error); } } }; fetchInvite(); - }, [token, form]); + }, []); /** * Validates the form data against the validation schema. @@ -494,7 +496,6 @@ const Register = ({ isSuperAdmin }) => { const handleStepOne = async (e) => { e.preventDefault(); - let error = validateForm({ firstName: form.firstName, lastName: form.lastName, diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 35d47d6b1..c326e8ee9 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -76,6 +76,7 @@ const credentials = joi.object({ return value; }), role: joi.array(), + teamId: joi.string().allow("").optional(), }); const monitorValidation = joi.object({ diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 7c053ae56..720dcd08c 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -226,7 +226,8 @@ const inviteController = async (req, res, next) => { try { // Only admins can invite const token = getTokenFromHeaders(req.headers); - const { role, firstname } = jwt.decode(token); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; try { await inviteRoleValidation.validateAsync({ roles: role }); await inviteBodyValidation.validateAsync(req.body); diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index b4404c615..73e883930 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -8,6 +8,7 @@ const requestInviteToken = async (req, res) => { let inviteToken = new InviteToken({ email: req.body.email, role: req.body.role, + teamId: req.body.teamId, token: crypto.randomBytes(32).toString("hex"), }); await inviteToken.save(); diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index bc2f83958..2df13ddc7 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -37,6 +37,8 @@ const insertUser = async (req, res) => { }); teamId = team._id; await team.save(); + } else { + teamId = userData.teamId; } const newUser = new UserModel(userData); diff --git a/Server/models/InviteToken.js b/Server/models/InviteToken.js index 10515dadb..5fbd4fc73 100644 --- a/Server/models/InviteToken.js +++ b/Server/models/InviteToken.js @@ -7,6 +7,12 @@ const InviteTokenSchema = mongoose.Schema( required: true, unique: true, }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, role: { type: Array, required: true, diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 3a9b7e1a1..14654e1ae 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -5,7 +5,7 @@ const joi = require("joi"); //**************************************** const roleValidatior = (role) => (value, helpers) => { - const hasRole = roles.some((role) => value.includes(role)); + const hasRole = role.some((role) => value.includes(role)); if (!hasRole) { throw new Joi.ValidationError( `You do not have the required authorization. Required roles: ${roles.join(", ")}` @@ -72,6 +72,7 @@ const registerValidation = joi.object({ .items(joi.string().valid("superadmin", "admin", "user")) .min(1) .required(), + teamId: joi.string().allow("").required(), }); const editUserParamValidation = joi.object({ @@ -135,6 +136,7 @@ const inviteBodyValidation = joi.object({ "string.email": "Must be a valid email address", }), role: joi.array().required(), + teamId: joi.string().required(), }); const inviteVerifciationBodyValidation = joi.object({ From 20b2b089a42877e61d78bf979f2f61d42ec1662c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 15:21:06 -0700 Subject: [PATCH 12/35] Removed awaits for emails, added redirect if invite verify fails --- Client/src/Pages/Auth/Register/Register.jsx | 3 +- Server/controllers/authController.js | 46 ++++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 39a5fdafd..4423189af 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -451,14 +451,13 @@ const Register = ({ isSuperAdmin }) => { useEffect(() => { const fetchInvite = async () => { - console.log("FETCHING"); if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); const { role, email, teamId } = res.data.data; setForm({ ...form, email, role, teamId }); } catch (error) { - logger.error(error); + navigate("/register", { replace: true }); } } }; diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 720dcd08c..955d590e7 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -90,12 +90,20 @@ const registerController = async (req, res, next) => { const token = issueToken(userForToken); - await req.emailService.buildAndSendEmail( - "welcomeEmailTemplate", - { name: newUser.firstName }, - newUser.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "welcomeEmailTemplate", + { name: newUser.firstName }, + newUser.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending welcome email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, @@ -241,15 +249,22 @@ const inviteController = async (req, res, next) => { } const inviteToken = await req.db.requestInviteToken(req, res); - await req.emailService.buildAndSendEmail( - "employeeActivationTemplate", - { - name: firstname, - link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, - }, - req.body.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); return res .status(200) @@ -275,7 +290,6 @@ const inviteVerifyController = async (req, res, next) => { try { const invite = await req.db.getInviteToken(req, res); - res .status(200) .json({ status: "success", msg: "Invite verified", data: invite }); From 32101b4c2f40f43bcf43dfeab8a3f179739305aa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 15:27:05 -0700 Subject: [PATCH 13/35] Initialize User model to creat index --- Server/models/user.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Server/models/user.js b/Server/models/user.js index 977cabc15..e05e74037 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -70,4 +70,10 @@ UserSchema.methods.comparePassword = async function (submittedPassword) { return res; }; +const User = mongoose.model("User", UserSchema); + +User.init().then(() => { + console.log("User model initialized"); +}); + module.exports = mongoose.model("User", UserSchema); From 48bd6ec95a8e1591783996b1b51bf8f947ec55ef Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 15:34:50 -0700 Subject: [PATCH 14/35] Updated verifyAdmin middleware --- Server/middleware/verifyAdmin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/middleware/verifyAdmin.js b/Server/middleware/verifyAdmin.js index 7ef123451..94f529d35 100644 --- a/Server/middleware/verifyAdmin.js +++ b/Server/middleware/verifyAdmin.js @@ -42,7 +42,7 @@ const verifyAdmin = (req, res, next) => { .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); } - if (decoded.role.includes("admin") === false) { + if (decoded.role.includes("superadmin") === false) { logger.error(errorMessages.INVALID_AUTH_TOKEN, { service: SERVICE_NAME, }); From bad0039757e1118b63ce4f79db619851cfa9bd5a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 15:36:53 -0700 Subject: [PATCH 15/35] renamed verifyAdmin to verifySuperAdmin --- Server/middleware/{verifyAdmin.js => verifySuperAdmin.js} | 4 ++-- Server/routes/authRoute.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename Server/middleware/{verifyAdmin.js => verifySuperAdmin.js} (94%) diff --git a/Server/middleware/verifyAdmin.js b/Server/middleware/verifySuperAdmin.js similarity index 94% rename from Server/middleware/verifyAdmin.js rename to Server/middleware/verifySuperAdmin.js index 94f529d35..82401b3b7 100644 --- a/Server/middleware/verifyAdmin.js +++ b/Server/middleware/verifySuperAdmin.js @@ -11,7 +11,7 @@ const { errorMessages } = require("../utils/messages"); * @param {express.NextFunction} next * @returns {express.Response} */ -const verifyAdmin = (req, res, next) => { +const verifySuperAdmin = (req, res, next) => { const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { @@ -54,4 +54,4 @@ const verifyAdmin = (req, res, next) => { }); }; -module.exports = { verifyAdmin }; +module.exports = { verifySuperAdmin }; diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 589c0f1ac..e78dd5e52 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,6 +1,6 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); -const { verifyAdmin } = require("../middleware/verifyAdmin"); +const { verifySuperAdmin } = require("../middleware/verifySuperAdmin"); const { verifyOwnership } = require("../middleware/verifyOwnership"); const multer = require("multer"); const upload = multer(); @@ -31,7 +31,7 @@ router.put( userEditController ); router.get("/users/superadmin", checkSuperadminController); -router.get("/users", verifyJWT, verifyAdmin, getAllUsersController); +router.get("/users", verifyJWT, verifySuperAdmin, getAllUsersController); router.delete( "/user/:userId", verifyJWT, From 3e63a3266954fa4eb77dd47d5754935ced7cd67e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 16:57:30 -0700 Subject: [PATCH 16/35] Renamed variable --- Server/db/mongo/MongoDB.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 8c1d97351..d6440ca12 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -17,8 +17,8 @@ const connect = async () => { const checkSuperadmin = async (req, res) => { try { - const admin = await UserModel.findOne({ role: "superadmin" }); - if (admin !== null) { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { return true; } return false; From 98f56248eb94e86b5290b42b6257e4dd9614352b Mon Sep 17 00:00:00 2001 From: M M Date: Mon, 26 Aug 2024 17:16:29 -0700 Subject: [PATCH 17/35] Update all areas where it says friendly name to display name. --- .../Maintenance/CreateMaintenanceWindow/index.jsx | 14 +++++++------- Client/src/Pages/Monitors/Configure/index.jsx | 2 +- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 +- Client/src/Pages/PageSpeed/Configure/index.jsx | 2 +- .../src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 2 +- Client/src/Validation/validation.js | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx index 9110b7a96..f97578d5a 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -62,7 +62,7 @@ const CreateNewMaintenanceWindow = () => { startTime: dayjs(), duration: "60", unit: "minutes", - friendlyName: "", + displayName: "", AddMonitors: "", }); const [errors, setErrors] = useState({}); @@ -82,7 +82,7 @@ const CreateNewMaintenanceWindow = () => { startTime: values.startTime.format("HH:mm"), duration: values.duration, unit: values.unit, - friendlyName: values.friendlyName, + displayName: values.displayName, addMonitors: values.AddMonitors, }; @@ -178,14 +178,14 @@ const CreateNewMaintenanceWindow = () => { ), }, { - title: "Friendly name", + title: "Display name", component: ( handleChange(e, "friendlyName")} - error={errors.friendlyName} + value={values.displayName} + onChange={(e) => handleChange(e, "displayName")} + error={errors.displayName} /> ), }, diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index bd2a26df0..ebf6e3279 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -314,7 +314,7 @@ const Configure = () => { { { - Monitor friendly name + Monitor display name { Create pagespeed monitor - Monitor friendly name + Monitor display name Date: Mon, 26 Aug 2024 17:43:56 -0700 Subject: [PATCH 18/35] refactor invite to its own route and controller --- .../TabPanels/Account/TeamPanel.jsx | 9 +- Client/src/Utils/NetworkService.js | 4 +- Server/controllers/authController.js | 6 +- Server/controllers/inviteController.js | 96 +++++++++++++++++++ Server/db/mongo/modules/inviteModule.js | 40 ++++++-- Server/db/mongo/modules/userModule.js | 19 ++-- Server/index.js | 2 + Server/routes/authRoute.js | 5 - Server/routes/inviteRoute.js | 11 +++ Server/validation/joi.js | 4 +- 10 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 Server/controllers/inviteController.js create mode 100644 Server/routes/inviteRoute.js diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index a0393977d..aa218e721 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -32,6 +32,12 @@ import Select from "../../Inputs/Select"; */ const TeamPanel = () => { + const roleMap = { + superadmin: "Super admin", + admin: "Admin", + user: "Team member", + }; + const theme = useTheme(); const { authToken, user } = useSelector((state) => state.auth); @@ -78,6 +84,7 @@ const TeamPanel = () => { // { id: 4, name: "ACTION" }, ], rows: team?.map((member, idx) => { + const roles = member.role.map((role) => roleMap[role]).join(","); return { id: member._id, data: [ @@ -100,7 +107,7 @@ const TeamPanel = () => { { // TODO - Add select dropdown id: idx + 2, - data: member.role.includes("admin") ? "Administrator" : "Member", + data: roles, }, // FEATURE STILL TO BE IMPLEMENTED // { diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 97071a7aa..cd46469b4 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -327,7 +327,7 @@ class NetworkService { */ async requestInvitationToken(authToken, email, role) { return this.axiosInstance.post( - `/auth/invite`, + `/invite`, { email, role }, { headers: { Authorization: `Bearer ${authToken}` }, @@ -346,7 +346,7 @@ class NetworkService { * */ async verifyInvitationToken(token) { - return this.axiosInstance.post(`/auth/invite/verify`, { + return this.axiosInstance.post(`/invite/verify`, { token, }); } diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 955d590e7..03f7f1b66 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -1,6 +1,6 @@ const express = require("express"); const { - registerValidation, + registrationBodyValidation, loginValidation, editUserParamValidation, editUserBodyValidation, @@ -50,7 +50,7 @@ const getTokenFromHeaders = (headers) => { const registerController = async (req, res, next) => { // joi validation try { - await registerValidation.validateAsync(req.body); + await registrationBodyValidation.validateAsync(req.body); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -78,7 +78,7 @@ const registerController = async (req, res, next) => { // Create a new user try { - const newUser = await req.db.insertUser(req, res); + const newUser = await req.db.insertUser({ ...req.body }, req.file); logger.info(successMessages.AUTH_CREATE_USER, { service: SERVICE_NAME, userId: newUser._id, diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js new file mode 100644 index 000000000..e685d0eef --- /dev/null +++ b/Server/controllers/inviteController.js @@ -0,0 +1,96 @@ +const { + inviteRoleValidation, + inviteBodyValidation, + inviteVerifciationBodyValidation, +} = require("../validation/joi"); +const logger = require("../utils/logger"); +require("dotenv").config(); +const { errorMessages, successMessages } = require("../utils/messages"); +var jwt = require("jsonwebtoken"); +const SERVICE_NAME = "inviteController"; + +const getTokenFromHeaders = (headers) => { + const authorizationHeader = headers.authorization; + if (!authorizationHeader) throw new Error("No auth headers"); + + const parts = authorizationHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") + throw new Error("Invalid auth headers"); + + return parts[1]; +}; + +const inviteController = async (req, res, next) => { + try { + // Only admins can invite + const token = getTokenFromHeaders(req.headers); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; + try { + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + const inviteToken = await req.db.requestInviteToken({ ...req.body }); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + + return res + .status(200) + .json({ success: true, msg: "Invite sent", data: inviteToken }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +const inviteVerifyController = async (req, res, next) => { + try { + await inviteVerifciationBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + try { + const invite = await req.db.getInviteToken(req.body.token); + res + .status(200) + .json({ status: "success", msg: "Invite verified", data: invite }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +module.exports = { + inviteController, + inviteVerifyController, +}; diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index 73e883930..975f6f1e8 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -2,15 +2,25 @@ const InviteToken = require("../../../models/InviteToken"); const crypto = require("crypto"); const { errorMessages } = require("../../../utils/messages"); -const requestInviteToken = async (req, res) => { +/** + * Request an invite token for a user. + * + * This function deletes any existing invite tokens for the user's email, + * generates a new token, saves it, and then returns the new token. + * + * @param {Object} userData - The user data. + * @param {string} userData.email - The user's email. + * @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team. + * @param {Array} userData.role - The user's role(s). + * @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time. + * @returns {Promise} The invite token. + * @throws {Error} If there is an error. + */ +const requestInviteToken = async (userData) => { try { - await InviteToken.deleteMany({ email: req.body.email }); - let inviteToken = new InviteToken({ - email: req.body.email, - role: req.body.role, - teamId: req.body.teamId, - token: crypto.randomBytes(32).toString("hex"), - }); + await InviteToken.deleteMany({ email: userData.email }); + userData.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = new InviteToken(userData); await inviteToken.save(); return inviteToken; } catch (error) { @@ -18,10 +28,20 @@ const requestInviteToken = async (req, res) => { } }; -const getInviteToken = async (req, res) => { +/** + * Retrieves and deletes an invite token. + * + * This function searches for an invite token in the database and deletes it. + * If the invite token is not found, it throws an error. + * + * @param {string} token - The invite token to search for. + * @returns {Promise} The invite token data. + * @throws {Error} If the invite token is not found or there is another error. + */ +const getInviteToken = async (token) => { try { const invite = await InviteToken.findOneAndDelete({ - token: req.body.token, + token, }); if (invite === null) { throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND); diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index 2df13ddc7..d29c1889b 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -14,35 +14,30 @@ const { ParseBoolean } = require("../../../utils/utils"); * @returns {Promise} * @throws {Error} */ -const insertUser = async (req, res) => { +const insertUser = async (userData, imageFile) => { try { - const userData = { ...req.body }; - if (req.file) { + if (imageFile) { // 1. Save the full size image userData.profileImage = { - data: req.file.buffer, - contentType: req.file.mimetype, + data: imageFile.buffer, + contentType: imageFile.mimetype, }; // 2. Get the avatar sized image - const avatar = await GenerateAvatarImage(req.file); + const avatar = await GenerateAvatarImage(imageFile); userData.avatarImage = avatar; } - let teamId; - // Is user superadmin? If so, create team + // Handle creating team if superadmin if (userData.role.includes("superadmin")) { const team = new TeamModel({ email: userData.email, }); - teamId = team._id; + userData.teamId = team._id; await team.save(); - } else { - teamId = userData.teamId; } const newUser = new UserModel(userData); - newUser.teamId = teamId; await newUser.save(); return await UserModel.findOne({ _id: newUser._id }) .select("-password") diff --git a/Server/index.js b/Server/index.js index 9a5f1797f..846b11e3d 100644 --- a/Server/index.js +++ b/Server/index.js @@ -7,6 +7,7 @@ const { verifyJWT } = require("./middleware/verifyJWT"); const { handleErrors } = require("./middleware/handleErrors"); const { errorMessages } = require("./utils/messages"); const authRouter = require("./routes/authRoute"); +const inviteRouter = require("./routes/inviteRoute"); const monitorRouter = require("./routes/monitorRoute"); const checkRouter = require("./routes/checkRoute"); const alertRouter = require("./routes/alertRoute"); @@ -71,6 +72,7 @@ const startApp = async () => { //routes app.use("/api/v1/auth", authRouter); + app.use("/api/v1/invite", inviteRouter); app.use("/api/v1/monitors", verifyJWT, monitorRouter); app.use("/api/v1/checks", verifyJWT, checkRouter); app.use("/api/v1/alerts", verifyJWT, alertRouter); diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index e78dd5e52..ddecd8d39 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -17,8 +17,6 @@ const { checkSuperadminController, getAllUsersController, deleteUserController, - inviteController, - inviteVerifyController, } = require("../controllers/authController"); //Auth routes @@ -39,9 +37,6 @@ router.delete( deleteUserController ); -router.post("/invite", verifyJWT, inviteController); -router.post("/invite/verify", inviteVerifyController); - //Recovery routes router.post("/recovery/request", recoveryRequestController); router.post("/recovery/validate", validateRecoveryTokenController); diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js new file mode 100644 index 000000000..05df2f51f --- /dev/null +++ b/Server/routes/inviteRoute.js @@ -0,0 +1,11 @@ +const router = require("express").Router(); +const { verifyJWT } = require("../middleware/verifyJWT"); +const { + inviteController, + inviteVerifyController, +} = require("../controllers/inviteController"); + +router.post("/", verifyJWT, inviteController); +router.post("/verify", inviteVerifyController); + +module.exports = router; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 14654e1ae..a9bcb9683 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -39,7 +39,7 @@ const loginValidation = joi.object({ ), }); -const registerValidation = joi.object({ +const registrationBodyValidation = joi.object({ firstName: joi .string() .required() @@ -336,7 +336,7 @@ const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({ module.exports = { roleValidatior, loginValidation, - registerValidation, + registrationBodyValidation, recoveryValidation, recoveryTokenValidation, newPasswordValidation, From ce542879e708244596c5b7c9c2998901d0bffe43 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 17:46:56 -0700 Subject: [PATCH 19/35] Add invite object for clarity --- Client/src/Pages/Auth/Register/Register.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 4423189af..8163201ae 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -454,7 +454,8 @@ const Register = ({ isSuperAdmin }) => { if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); - const { role, email, teamId } = res.data.data; + const invite = res.data.data; + const { role, email, teamId } = invite; setForm({ ...form, email, role, teamId }); } catch (error) { navigate("/register", { replace: true }); From a02f547b2cf422195f8857de2df4c257e40bca0a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 19:15:01 -0700 Subject: [PATCH 20/35] Add teamID to monitor model --- Server/models/Monitor.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 7d4bd17d2..525f4e4eb 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -6,6 +6,13 @@ const MonitorSchema = mongoose.Schema( type: mongoose.Schema.Types.ObjectId, ref: "User", immutable: true, + required: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, }, name: { type: String, From e658b4759a9ed99d312dfe4ecab76c46480b4427 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 19:25:19 -0700 Subject: [PATCH 21/35] Add teamID to monitor on creation --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 ++ Server/db/mongo/modules/monitorModule.js | 2 +- Server/validation/joi.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 28493080a..21233a264 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -118,9 +118,11 @@ const CreateMonitor = () => { form = { ...form, description: form.name, + teamId: user.teamId, userId: user._id, notifications: monitor.notifications, }; + console.log(form); const action = await dispatch( createUptimeMonitor({ authToken, monitor: form }) ); diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 9778b6224..612b71a89 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -364,10 +364,10 @@ const getMonitorsByUserId = async (req, res) => { */ const createMonitor = async (req, res) => { try { + console.log(req.body); const monitor = new Monitor({ ...req.body }); // Remove notifications fom monitor as they aren't needed here monitor.notifications = undefined; - monitor.userId = req.user._id; await monitor.save(); return monitor; } catch (error) { diff --git a/Server/validation/joi.js b/Server/validation/joi.js index a9bcb9683..24e7f7b76 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -180,6 +180,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({ const createMonitorBodyValidation = joi.object({ _id: joi.string(), userId: joi.string().required(), + teamId: joi.string().required(), name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), From 465c1f9f9eef3a401a7980b827d8fc0e7514d480 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 19:38:24 -0700 Subject: [PATCH 22/35] Refactor getMonitorByUserID to getMonitorByTeamID --- .../UptimeMonitors/uptimeMonitorsSlice.js | 4 ++-- Client/src/Utils/NetworkService.js | 6 +++--- Server/controllers/monitorController.js | 18 +++++++++--------- Server/db/mongo/MongoDB.js | 4 ++-- Server/db/mongo/modules/monitorModule.js | 8 ++++---- Server/routes/monitorRoute.js | 2 +- Server/validation/joi.js | 10 +++++----- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 9ab7d385b..dc9fa3c20 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -33,9 +33,9 @@ export const getUptimeMonitorsByUserId = createAsyncThunk( async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, - user._id, + user.teamId, 25, ["http", "ping"], null, diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index cd46469b4..1901f6654 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -52,9 +52,9 @@ class NetworkService { * @param {boolean} [normalize] - Whether to normalize the retrieved monitors. * @returns {Promise} The response from the axios GET request. */ - async getMonitorsByUserId( + async getMonitorsByTeamId( authToken, - userId, + teamId, limit, types, status, @@ -74,7 +74,7 @@ class NetworkService { if (normalize) params.append("normalize", normalize); return this.axiosInstance.get( - `/monitors/user/${userId}?${params.toString()}`, + `/monitors/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 37aff6c96..c3fe37955 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -1,10 +1,10 @@ const { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, + getMonitorsByTeamIdValidation, createMonitorBodyValidation, editMonitorBodyValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdQueryValidation, } = require("../validation/joi"); const sslChecker = require("ssl-checker"); @@ -149,10 +149,10 @@ const getMonitorById = async (req, res, next) => { * @returns {Promise} * @throws {Error} */ -const getMonitorsByUserId = async (req, res, next) => { +const getMonitorsByTeamId = async (req, res, next) => { try { - await getMonitorsByUserIdValidation.validateAsync(req.params); - await getMonitorsByUserIdQueryValidation.validateAsync(req.query); + await getMonitorsByTeamIdValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -163,12 +163,12 @@ const getMonitorsByUserId = async (req, res, next) => { } try { - const userId = req.params.userId; - const monitors = await req.db.getMonitorsByUserId(req, res); + const teamId = req.params.teamId; + const monitors = await req.db.getMonitorsByTeamId(req, res); return res.json({ success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(userId), + msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), data: monitors, }); } catch (error) { @@ -342,7 +342,7 @@ module.exports = { getMonitorStatsById, getMonitorCertificate, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index d6440ca12..05dfa35c5 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -66,7 +66,7 @@ const { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, @@ -145,7 +145,7 @@ module.exports = { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 612b71a89..6e3bba66c 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -287,17 +287,17 @@ const getMonitorById = async (monitorId) => { }; /** - * Get monitors by UserID + * Get monitors by TeamID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise>} * @throws {Error} */ -const getMonitorsByUserId = async (req, res) => { +const getMonitorsByTeamId = async (req, res) => { try { let { limit, type, status, sortOrder, normalize } = req.query || {}; - const monitorQuery = { userId: req.params.userId }; + const monitorQuery = { teamId: req.params.teamId }; if (type !== undefined) { const types = Array.isArray(type) ? type : [type]; @@ -453,7 +453,7 @@ module.exports = { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 847b2b25d..facbf47ed 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -7,7 +7,7 @@ router.get("/", monitorController.getAllMonitors); router.get("/stats/:monitorId", monitorController.getMonitorStatsById); router.get("/certificate/:monitorId", monitorController.getMonitorCertificate); router.get("/:monitorId", monitorController.getMonitorById); -router.get("/user/:userId", monitorController.getMonitorsByUserId); +router.get("/team/:teamId", monitorController.getMonitorsByTeamId); router.post("/", monitorController.createMonitor); router.delete( diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 24e7f7b76..a97574548 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -160,11 +160,11 @@ const getMonitorByIdQueryValidation = joi.object({ normalize: joi.boolean(), }); -const getMonitorsByUserIdValidation = joi.object({ - userId: joi.string().required(), +const getMonitorsByTeamIdValidation = joi.object({ + teamId: joi.string().required(), }); -const getMonitorsByUserIdQueryValidation = joi.object({ +const getMonitorsByTeamIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), @@ -347,8 +347,8 @@ module.exports = { createMonitorBodyValidation, getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdValidation, + getMonitorsByTeamIdQueryValidation, editMonitorBodyValidation, editUserParamValidation, editUserBodyValidation, From 3fbd986bbe060753880da4567710576858bdd17c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Aug 2024 09:13:48 -0700 Subject: [PATCH 23/35] Update admin to superadmin in sidebar --- Client/src/Components/Sidebar/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index ae6507b69..de35e400e 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -289,7 +289,7 @@ function Sidebar() { if ( child.name === "Team" && authState.user?.role && - !authState.user.role.includes("admin") + !authState.user.role.includes("superadmin") ) { return null; } @@ -349,7 +349,7 @@ function Sidebar() { if ( child.name === "Team" && authState.user?.role && - !authState.user.role.includes("admin") + !authState.user.role.includes("superadmin") ) { return null; } From 5bd13a572e9e45afcb3a20132967110ed1060237 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Aug 2024 10:39:55 -0700 Subject: [PATCH 24/35] Add middleware for verifying roles, use middleware on appropriate routs --- Server/middleware/isAllowed.js | 53 ++++++++++++++++++++++++++++++++++ Server/routes/authRoute.js | 9 ++++-- Server/routes/inviteRoute.js | 15 ++++++++-- Server/routes/monitorRoute.js | 21 ++++++++++---- Server/utils/messages.js | 3 ++ 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 Server/middleware/isAllowed.js diff --git a/Server/middleware/isAllowed.js b/Server/middleware/isAllowed.js new file mode 100644 index 000000000..fefb26099 --- /dev/null +++ b/Server/middleware/isAllowed.js @@ -0,0 +1,53 @@ +const jwt = require("jsonwebtoken"); +const TOKEN_PREFIX = "Bearer "; +const SERVICE_NAME = "allowedRoles"; +const { errorMessages } = require("../utils/messages"); + +const isAllowed = (allowedRoles) => { + return (req, res, next) => { + const token = req.headers["authorization"]; + + // If no token is pressent, return an error + if (!token) { + const error = new Error(errorMessages.NO_AUTH_TOKEN); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + + // If the token is improperly formatted, return an error + if (!token.startsWith(TOKEN_PREFIX)) { + const error = new Error(errorMessages.INVALID_AUTH_TOKEN); + error.status = 400; + error.service = SERVICE_NAME; + next(error); + return; + } + // Parse the token + try { + const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); + var decoded = jwt.verify(parsedToken, process.env.JWT_SECRET); + const userRoles = decoded.role; + + // Check if the user has the required role + if (userRoles.some((role) => allowedRoles.includes(role))) { + next(); + return; + } else { + const error = new Error(errorMessages.INSUFFICIENT_PERMISSIONS); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + } catch (error) { + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + }; +}; + +module.exports = { isAllowed }; diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index ddecd8d39..743a55c19 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,7 +1,7 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); -const { verifySuperAdmin } = require("../middleware/verifySuperAdmin"); const { verifyOwnership } = require("../middleware/verifyOwnership"); +const { isAllowed } = require("../middleware/isAllowed"); const multer = require("multer"); const upload = multer(); const User = require("../models/user"); @@ -29,7 +29,12 @@ router.put( userEditController ); router.get("/users/superadmin", checkSuperadminController); -router.get("/users", verifyJWT, verifySuperAdmin, getAllUsersController); +router.get( + "/users", + verifyJWT, + isAllowed(["admin", "superadmin"]), + getAllUsersController +); router.delete( "/user/:userId", verifyJWT, diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js index 05df2f51f..54e74b0fa 100644 --- a/Server/routes/inviteRoute.js +++ b/Server/routes/inviteRoute.js @@ -1,11 +1,22 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); +const { isAllowed } = require("../middleware/isAllowed"); + const { inviteController, inviteVerifyController, } = require("../controllers/inviteController"); -router.post("/", verifyJWT, inviteController); -router.post("/verify", inviteVerifyController); +router.post( + "/", + isAllowed(["admin", "superadmin"]), + verifyJWT, + inviteController +); +router.post( + "/verify", + isAllowed(["admin", "superadmin"]), + inviteVerifyController +); module.exports = router; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index facbf47ed..e08c43b7d 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,7 +1,6 @@ const router = require("express").Router(); const monitorController = require("../controllers/monitorController"); -const { verifyOwnership } = require("../middleware/verifyOwnership"); -const Monitor = require("../models/Monitor"); +const { isAllowed } = require("../middleware/isAllowed"); router.get("/", monitorController.getAllMonitors); router.get("/stats/:monitorId", monitorController.getMonitorStatsById); @@ -9,17 +8,27 @@ router.get("/certificate/:monitorId", monitorController.getMonitorCertificate); router.get("/:monitorId", monitorController.getMonitorById); router.get("/team/:teamId", monitorController.getMonitorsByTeamId); -router.post("/", monitorController.createMonitor); +router.post( + "/", + isAllowed(["admin", "superadmin"]), + monitorController.createMonitor +); + router.delete( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.deleteMonitor ); + router.put( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.editMonitor ); -router.delete("/all", monitorController.deleteAllMonitors); +router.delete( + "/all", + isAllowed(["superadmin"]), + monitorController.deleteAllMonitors +); module.exports = router; diff --git a/Server/utils/messages.js b/Server/utils/messages.js index 55f0fa7e9..7375fe448 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -17,6 +17,9 @@ const errorMessages = { 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", From efe508f93614ff0f3def503fe257ea3ad70c5c91 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Tue, 27 Aug 2024 10:48:30 -0700 Subject: [PATCH 25/35] Update index.jsx --- Client/src/Pages/Monitors/Configure/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index ebf6e3279..6df2f1be2 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -314,7 +314,7 @@ const Configure = () => { Date: Tue, 27 Aug 2024 12:07:30 -0700 Subject: [PATCH 26/35] Rename thunks to reflect teamId vs userId --- .../PageSpeedMonitor/pageSpeedMonitorSlice.js | 12 ++++++------ .../Features/UptimeMonitors/uptimeMonitorsSlice.js | 10 +++++----- Client/src/Pages/Incidents/index.jsx | 2 +- Client/src/Pages/Monitors/Configure/index.jsx | 4 ++-- Client/src/Pages/Monitors/index.jsx | 6 +++--- Client/src/Pages/PageSpeed/Configure/index.jsx | 4 ++-- Client/src/Pages/PageSpeed/index.jsx | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 22e2b3324..5679de108 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -28,12 +28,12 @@ export const createPageSpeed = createAsyncThunk( } ); -export const getPageSpeedByUserId = createAsyncThunk( - "pageSpeedMonitors/getPageSpeedByUserId", +export const getPageSpeedByTeamId = createAsyncThunk( + "pageSpeedMonitors/getPageSpeedByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, user._id, 25, @@ -127,15 +127,15 @@ const pageSpeedMonitorSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getPageSpeedByUserId.pending, (state) => { + .addCase(getPageSpeedByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getPageSpeedByUserId.fulfilled, (state, action) => { + .addCase(getPageSpeedByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getPageSpeedByUserId.rejected, (state, action) => { + .addCase(getPageSpeedByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index dc9fa3c20..237d9d3dc 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -28,8 +28,8 @@ export const createUptimeMonitor = createAsyncThunk( } ); -export const getUptimeMonitorsByUserId = createAsyncThunk( - "montiors/getMonitorsByUserId", +export const getUptimeMonitorsByTeamId = createAsyncThunk( + "montiors/getMonitorsByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { @@ -126,15 +126,15 @@ const uptimeMonitorsSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getUptimeMonitorsByUserId.pending, (state) => { + .addCase(getUptimeMonitorsByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getUptimeMonitorsByUserId.fulfilled, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getUptimeMonitorsByUserId.rejected, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 34768f7f9..d660be270 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -48,7 +48,7 @@ const Incidents = () => { useEffect(() => { const fetchMonitors = async () => { setLoading(true); - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( authState.authToken, authState.user._id, 1, diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index bd2a26df0..bfb93a1de 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -8,7 +8,7 @@ import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { updateUptimeMonitor, - getUptimeMonitorsByUserId, + getUptimeMonitorsByTeamId, deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Button from "../../../Components/Button"; @@ -177,7 +177,7 @@ const Configure = () => { ); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getUptimeMonitorsByUserId(authToken)); + dispatch(getUptimeMonitorsByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx index 9eef7ec3a..8f2b93531 100644 --- a/Client/src/Pages/Monitors/index.jsx +++ b/Client/src/Pages/Monitors/index.jsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { deleteUptimeMonitor, - getUptimeMonitorsByUserId, + getUptimeMonitorsByTeamId, } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { useNavigate } from "react-router-dom"; import Button from "../../Components/Button"; @@ -44,7 +44,7 @@ const ActionsMenu = ({ monitor }) => { ); if (action.meta.requestStatus === "fulfilled") { setIsOpen(false); // close modal - dispatch(getUptimeMonitorsByUserId(authState.authToken)); + dispatch(getUptimeMonitorsByTeamId(authState.authToken)); createToast({ body: "Monitor deleted successfully." }); } else { createToast({ body: "Failed to delete monitor." }); @@ -269,7 +269,7 @@ const Monitors = () => { const dispatch = useDispatch({}); useEffect(() => { - dispatch(getUptimeMonitorsByUserId(authState.authToken)); + dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); const up = monitorState.monitors.reduce((acc, cur) => { diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index 83484de7e..bb51c0703 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -5,7 +5,7 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router"; import { deletePageSpeed, - getPageSpeedByUserId, + getPageSpeedByTeamId, updatePageSpeed, } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { monitorValidation } from "../../../Validation/validation"; @@ -127,7 +127,7 @@ const PageSpeedConfigure = () => { ); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getPageSpeedByUserId(authToken)); + dispatch(getPageSpeedByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 368ae2062..399597b13 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react"; import { formatDate, formatDurationRounded } from "../../Utils/timeUtils"; import { StatusLabel } from "../../Components/Label"; import { useDispatch, useSelector } from "react-redux"; -import { getPageSpeedByUserId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import PageSpeedIcon from "../../assets/icons/page-speed.svg?react"; import Fallback from "../../Components/Fallback"; import "./index.css"; @@ -133,7 +133,7 @@ const PageSpeed = () => { (state) => state.pageSpeedMonitors ); useEffect(() => { - dispatch(getPageSpeedByUserId(authToken)); + dispatch(getPageSpeedByTeamId(authToken)); }, [authToken, dispatch]); // will show skeletons only on initial load From eedc6bbed608fbf4988050f223476a31c76ba5fb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Aug 2024 12:22:32 -0700 Subject: [PATCH 27/35] Refactor getChecksByUserId to getChecksByTeamId --- Client/src/Pages/Incidents/IncidentTable/index.jsx | 4 ++-- Client/src/Pages/Incidents/index.jsx | 4 ++-- Client/src/Utils/NetworkService.js | 6 +++--- Server/controllers/checkController.js | 14 +++++++------- Server/db/mongo/MongoDB.js | 4 ++-- Server/db/mongo/modules/checkModule.js | 8 ++++---- Server/routes/checkRoute.js | 8 ++------ Server/validation/joi.js | 10 +++++----- 8 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 7a77208f8..ae633c4dd 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -46,9 +46,9 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { try { let res; if (selectedMonitor === "0") { - res = await networkService.getChecksByUser( + res = await networkService.getChecksByTeam( authToken, - user._id, + user.teamId, "desc", null, null, diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index d660be270..46c7d51cf 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -50,14 +50,14 @@ const Incidents = () => { setLoading(true); const res = await networkService.getMonitorsByTeamId( authState.authToken, - authState.user._id, + authState.user.teamId, 1, null, null, null, null ); - + console.log(res); // Reduce to a lookup object for 0(1) lookup if (res.data && res.data.data.length > 0) { const monitorLookup = res.data.data.reduce((acc, monitor) => { diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 1901f6654..c375fade4 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -409,9 +409,9 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async getChecksByUser( + async getChecksByTeam( authToken, - userId, + teamId, sortOrder, limit, dateRange, @@ -427,7 +427,7 @@ class NetworkService { if (page) params.append("page", page); if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); return this.axiosInstance.get( - `/checks/user/${userId}?${params.toString()}`, + `/checks/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}` }, } diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 957eeed4e..f553dabcb 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -3,8 +3,8 @@ const { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, } = require("../validation/joi"); const { successMessages } = require("../utils/messages"); @@ -62,10 +62,10 @@ const getChecks = async (req, res, next) => { } }; -const getUserChecks = async (req, res, next) => { +const getTeamChecks = async (req, res, next) => { try { - await getUserChecksParamValidation.validateAsync(req.params); - await getUserChecksQueryValidation.validateAsync(req.query); + await getTeamChecksParamValidation.validateAsync(req.params); + await getTeamChecksQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -75,7 +75,7 @@ const getUserChecks = async (req, res, next) => { return; } try { - const checkData = await req.db.getUserChecks(req); + const checkData = await req.db.getTeamChecks(req); return res.status(200).json({ success: true, msg: successMessages.CHECK_GET, @@ -115,6 +115,6 @@ const deleteChecks = async (req, res, next) => { module.exports = { createCheck, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, }; diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 05dfa35c5..1b675e7e5 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -92,7 +92,7 @@ const { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, } = require("./modules/checkModule"); @@ -153,7 +153,7 @@ module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, createAlert, getAlertsByUserId, diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 782e5f64d..56a40d2ad 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -116,12 +116,12 @@ const getChecks = async (req) => { } }; -const getUserChecks = async (req) => { - const { userId } = req.params; +const getTeamChecks = async (req) => { + const { teamId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; // Get monitorIDs - const userMonitors = await Monitor.find({ userId: userId }); + const userMonitors = await Monitor.find({ teamId: teamId }); const monitorIds = userMonitors.map((monitor) => monitor._id); //Build check query @@ -189,6 +189,6 @@ module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, }; diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index 4bba24e79..562b91263 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -9,13 +9,9 @@ router.post( checkController.createCheck ); -router.get( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - checkController.getChecks -); +router.get("/:monitorId", checkController.getChecks); -router.get("/user/:userId", checkController.getUserChecks); +router.get("/team/:teamId", checkController.getTeamChecks); router.delete( "/:monitorId", diff --git a/Server/validation/joi.js b/Server/validation/joi.js index a97574548..714eff5cf 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -271,11 +271,11 @@ const getChecksQueryValidation = joi.object({ rowsPerPage: joi.number(), }); -const getUserChecksParamValidation = joi.object({ - userId: joi.string().required(), +const getTeamChecksParamValidation = joi.object({ + teamId: joi.string().required(), }); -const getUserChecksQueryValidation = joi.object({ +const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), dateRange: joi.string().valid("day", "week", "month"), @@ -364,8 +364,8 @@ module.exports = { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, deleteUserParamValidation, getPageSpeedCheckParamValidation, From cd6f55a9c99a05eed4edbf76857a8b2ce218af19 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Aug 2024 12:54:44 -0700 Subject: [PATCH 28/35] Conditionally render some components based on admin status --- Client/src/App.jsx | 10 +++- Client/src/Components/Fallback/index.jsx | 19 +++--- Client/src/HOC/withAdminProp.jsx | 20 +++++++ Client/src/Pages/Monitors/Details/index.jsx | 53 +++++++++-------- Client/src/Pages/Monitors/index.jsx | 65 ++++++++++++--------- Client/src/Pages/PageSpeed/index.jsx | 6 +- 6 files changed, 110 insertions(+), 63 deletions(-) create mode 100644 Client/src/HOC/withAdminProp.jsx diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 12db9dab9..f8a2f72f5 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -21,6 +21,7 @@ import ProtectedRoute from "./Components/ProtectedRoute"; import Details from "./Pages/Monitors/Details"; import Maintenance from "./Pages/Maintenance"; import withAdminCheck from "./HOC/withAdminCheck"; +import withAdminProp from "./HOC/withAdminProp"; import Configure from "./Pages/Monitors/Configure"; import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; @@ -30,6 +31,9 @@ import PageSpeedConfigure from "./Pages/PageSpeed/Configure"; function App() { const AdminCheckedRegister = withAdminCheck(Register); + const MonitorsWithAdminProp = withAdminProp(Monitors); + const DetailsWithAdminProp = withAdminProp(Details); + return ( <> @@ -37,11 +41,11 @@ function App() { } + element={} /> } + element={} /> } + element={} /> { +const Fallback = ({ title, checks, link = "/", isAdmin }) => { const theme = useTheme(); const navigate = useNavigate(); @@ -36,7 +36,7 @@ const Fallback = ({ title, checks, link = "/" }) => { /> - A {title} is used to: + A {title} monitor is used to: {checks.map((check, index) => ( { /> ))} -