add invite service

This commit is contained in:
Alex Holliday
2025-07-23 14:58:44 -07:00
parent 3239f555a1
commit aea79fec2e
5 changed files with 115 additions and 61 deletions

View File

@@ -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<Object>} 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<Object>} 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<Object>} 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"
);
}

View File

@@ -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),

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,