diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js index f7f33c5d3..06e38a2e2 100755 --- a/server/controllers/inviteController.js +++ b/server/controllers/inviteController.js @@ -1,41 +1,46 @@ -import { inviteRoleValidation, inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; -import jwt from "jsonwebtoken"; -import { getTokenFromHeaders } from "../utils/utils.js"; -import { asyncHandler, createServerError } from "../utils/errorUtils.js"; +import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "inviteController"; +/** + * Controller for handling user invitation operations + * Manages invite token generation, email sending, and token verification + */ class InviteController { - constructor(db, settingsService, emailService, stringService) { + /** + * Creates a new InviteController instance + * @param {Object} dependencies - Dependencies injected into the controller + * @param {Object} dependencies.db - Database connection instance + * @param {Object} dependencies.settingsService - Service for managing application settings + * @param {Object} dependencies.emailService - Service for sending emails + * @param {Object} dependencies.stringService - Service for internationalized strings + * @param {Object} dependencies.inviteService - Service for invite-related operations + */ + constructor({ db, settingsService, emailService, stringService, inviteService }) { this.db = db; this.settingsService = settingsService; this.emailService = emailService; this.stringService = stringService; + this.inviteService = inviteService; } /** - * Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.headers - The headers of the request. - * @property {string} req.headers.authorization - The authorization header containing the JWT token. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.email - The email of the user to be invited. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). + * Generates an invite token for a user invitation + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing invite details + * @param {Object} req.user - Authenticated user object + * @param {string} req.user.teamId - Team ID of the authenticated user + * @param {Object} res - Express response object + * @returns {Promise} Response with invite token data */ getInviteToken = asyncHandler( - async (req, res, next) => { - // Only admins can invite - const token = getTokenFromHeaders(req.headers); - const { role, teamId } = jwt.decode(token); - req.body.teamId = teamId; - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); - - const inviteToken = await this.db.requestInviteToken({ ...req.body }); + async (req, res) => { + const invite = req.body; + const teamId = req?.user?.teamId; + invite.teamId = teamId; + await inviteBodyValidation.validateAsync(invite); + const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, @@ -45,27 +50,26 @@ class InviteController { "getInviteToken" ); + /** + * Sends an invitation email to a user + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing invite details + * @param {Object} req.user - Authenticated user object + * @param {string} req.user.teamId - Team ID of the authenticated user + * @param {string} req.user.firstName - First name of the authenticated user + * @param {Object} res - Express response object + * @returns {Promise} Response with invite token data + */ sendInviteEmail = asyncHandler( - async (req, res, next) => { - // Only admins can invite - const token = getTokenFromHeaders(req.headers); - const { role, firstname, teamId } = jwt.decode(token); - req.body.teamId = teamId; - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); + async (req, res) => { + const inviteRequest = req.body; + inviteRequest.teamId = req?.user?.teamId; + await inviteBodyValidation.validateAsync(inviteRequest); - const inviteToken = await this.db.requestInviteToken({ ...req.body }); - const { clientHost } = this.settingsService.getSettings(); - - const html = await this.emailService.buildEmail("employeeActivationTemplate", { - name: firstname, - link: `${clientHost}/register/${inviteToken.token}`, + const inviteToken = await this.inviteService.sendInviteEmail({ + inviteRequest, + firstName: req?.user?.firstName, }); - const result = await this.emailService.sendEmail(req.body.email, "Welcome to Uptime Monitor", html); - if (!result) { - throw createServerError("Failed to send invite e-mail... Please verify your settings."); - } - return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, @@ -75,17 +79,25 @@ class InviteController { "sendInviteEmail" ); - inviteVerifyController = asyncHandler( - async (req, res, next) => { + /** + * Verifies an invite token and returns invite details + * @param {Object} req - Express request object + * @param {Object} req.body - Request body containing the invite token + * @param {string} req.body.token - The invite token to verify + * @param {Object} res - Express response object + * @returns {Promise} Response with verified invite data + */ + verifyInviteToken = asyncHandler( + async (req, res) => { await inviteVerificationBodyValidation.validateAsync(req.body); - const invite = await this.db.getInviteToken(req.body.token); + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); return res.success({ msg: this.stringService.inviteVerified, data: invite, }); }, SERVICE_NAME, - "inviteVerifyController" + "verifyInviteToken" ); } diff --git a/server/index.js b/server/index.js index c3c23995e..2a8b5259b 100755 --- a/server/index.js +++ b/server/index.js @@ -61,6 +61,7 @@ import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/Su import UserService from "./service/business/userService.js"; import CheckService from "./service/business/checkService.js"; import DiagnosticService from "./service/business/diagnosticService.js"; +import InviteService from "./service/business/inviteService.js"; //Network service and dependencies import NetworkService from "./service/infrastructure/networkService.js"; @@ -198,7 +199,12 @@ const startApp = async () => { stringService, }); const diagnosticService = new DiagnosticService(); - + const inviteService = new InviteService({ + db, + settingsService, + emailService, + stringService, + }); // const jobQueueHelper = new JobQueueHelper({ // redisService, // Queue, @@ -304,12 +310,13 @@ const startApp = async () => { checkService: ServiceRegistry.get(CheckService.SERVICE_NAME), }); - const inviteController = new InviteController( - ServiceRegistry.get(MongoDB.SERVICE_NAME), - ServiceRegistry.get(SettingsService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME) - ); + const inviteController = new InviteController({ + db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), + emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), + stringService: ServiceRegistry.get(StringService.SERVICE_NAME), + inviteService, + }); const maintenanceWindowController = new MaintenanceWindowController( ServiceRegistry.get(MongoDB.SERVICE_NAME), diff --git a/server/routes/inviteRoute.js b/server/routes/inviteRoute.js index c50eae9e3..d495c5476 100755 --- a/server/routes/inviteRoute.js +++ b/server/routes/inviteRoute.js @@ -10,8 +10,8 @@ class InviteRoutes { } initRoutes() { - this.router.post("/send", this.inviteController.sendInviteEmail); - this.router.post("/verify", this.inviteController.inviteVerifyController); + this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); + this.router.post("/verify", this.inviteController.verifyInviteToken); this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); } diff --git a/server/service/business/inviteService.js b/server/service/business/inviteService.js new file mode 100644 index 000000000..b70c151d4 --- /dev/null +++ b/server/service/business/inviteService.js @@ -0,0 +1,40 @@ +const SERVICE_NAME = "inviteService"; +import { createServerError } from "../../utils/errorUtils.js"; + +class InviteService { + static SERVICE_NAME = SERVICE_NAME; + + constructor({ db, settingsService, emailService, stringService }) { + this.db = db; + this.settingsService = settingsService; + this.emailService = emailService; + this.stringService = stringService; + } + + getInviteToken = async ({ invite, teamId }) => { + invite.teamId = teamId; + const inviteToken = await this.db.requestInviteToken(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ inviteRequest, firstName }) => { + const inviteToken = await this.db.requestInviteToken({ ...inviteRequest }); + const { clientHost } = this.settingsService.getSettings(); + + const html = await this.emailService.buildEmail("employeeActivationTemplate", { + name: firstName, + link: `${clientHost}/register/${inviteToken.token}`, + }); + const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html); + if (!result) { + throw createServerError("Failed to send invite e-mail... Please verify your settings."); + } + }; + + verifyInviteToken = async ({ inviteToken }) => { + const invite = await this.db.getInviteToken(inviteToken); + return invite; + }; +} + +export default InviteService; diff --git a/server/validation/joi.js b/server/validation/joi.js index 3436195f4..744b0ec74 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -87,10 +87,6 @@ const deleteUserParamValidation = joi.object({ email: joi.string().email().required(), }); -const inviteRoleValidation = joi.object({ - roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(), -}); - const inviteBodyValidation = joi.object({ email: joi.string().trim().email().required().messages({ "string.empty": "Email is required", @@ -652,7 +648,6 @@ export { recoveryValidation, recoveryTokenBodyValidation, newPasswordValidation, - inviteRoleValidation, inviteBodyValidation, inviteVerificationBodyValidation, createMonitorBodyValidation,