From 4b2973e308b352f5783f12fbdd135661cf2a8afd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 09:39:45 -0700 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 48bd6ec95a8e1591783996b1b51bf8f947ec55ef Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 15:34:50 -0700 Subject: [PATCH 07/12] 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 08/12] 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 09/12] 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 ac37211e32d9748cf9995242e754feefb40d5f99 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Aug 2024 17:43:56 -0700 Subject: [PATCH 10/12] 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 11/12] 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 3fbd986bbe060753880da4567710576858bdd17c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Aug 2024 09:13:48 -0700 Subject: [PATCH 12/12] 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; }