2025-04-20 11:29:53 -07:00
import {
registrationBodyValidation ,
loginValidation ,
editUserParamValidation ,
editUserBodyValidation ,
recoveryValidation ,
recoveryTokenValidation ,
newPasswordValidation ,
} from "../validation/joi.js" ;
import logger from "../utils/logger.js" ;
import jwt from "jsonwebtoken" ;
import { getTokenFromHeaders } from "../utils/utils.js" ;
import crypto from "crypto" ;
import { handleValidationError , handleError } from "./controllerUtils.js" ;
const SERVICE_NAME = "authController" ;
class AuthController {
constructor ( db , settingsService , emailService , jobQueue , stringService ) {
this . db = db ;
this . settingsService = settingsService ;
this . emailService = emailService ;
this . jobQueue = jobQueue ;
this . stringService = stringService ;
}
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
issueToken = ( payload , appSettings ) => {
try {
const tokenTTL = appSettings ? . jwtTTL ?? "2h" ;
const tokenSecret = appSettings ? . jwtSecret ;
const payloadData = payload ;
return jwt . sign ( payloadData , tokenSecret , { expiresIn : tokenTTL });
} catch ( error ) {
throw handleError ( error , SERVICE_NAME , "issueToken" );
}
};
/**
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.inviteToken - The invite token for registration.
* @property {Object} req.file - The file object for the user's profile image.
* @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 creation of the user, the created user data, and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
registerUser = async ( req , res , next ) => {
try {
2025-04-28 10:34:32 -07:00
if ( req . body ? . email ) {
2025-04-20 11:29:53 -07:00
req . body . email = req . body . email ? . toLowerCase ();
2025-04-28 10:34:32 -07:00
}
2025-04-20 11:29:53 -07:00
await registrationBodyValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
// Create a new user
try {
2025-04-28 10:34:32 -07:00
const user = req . body ;
2025-04-20 11:29:53 -07:00
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this . db . checkSuperadmin ( req , res );
if ( superAdminExists ) {
2025-04-28 10:34:32 -07:00
const invitedUser = await this . db . getInviteTokenAndDelete ( user . inviteToken );
user . role = invitedUser . role ;
user . teamId = invitedUser . teamId ;
2025-04-20 11:29:53 -07:00
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto . randomBytes ( 64 ). toString ( "hex" );
await this . db . updateAppSettings ({ jwtSecret });
}
const newUser = await this . db . insertUser ({ ... req . body }, req . file );
logger . info ({
message : this . stringService . authCreateUser ,
service : SERVICE_NAME ,
details : newUser . _id ,
});
const userForToken = { ... newUser . _doc };
delete userForToken . profileImage ;
delete userForToken . avatarImage ;
const appSettings = await this . settingsService . getSettings ();
const token = this . issueToken ( userForToken , appSettings );
this . emailService
. buildAndSendEmail (
"welcomeEmailTemplate" ,
{ name : newUser . firstName },
newUser . email ,
"Welcome to Uptime Monitor"
)
. catch (( error ) => {
logger . error ({
message : error . message ,
service : SERVICE_NAME ,
method : "registerUser" ,
stack : error . stack ,
});
});
res . success ({
msg : this . stringService . authCreateUser ,
data : { user : newUser , token : token },
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "registerController" ));
}
};
/**
* Logs in a user by validating the user's credentials and issuing a JWT token.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user.
* @property {string} req.body.password - The password of the user.
* @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 login of the user, the user data (without password and avatar image), and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
*/
loginUser = async ( req , res , next ) => {
try {
2025-04-28 10:34:32 -07:00
if ( req . body ? . email ) {
req . body . email = req . body . email ? . toLowerCase ();
2025-04-20 11:29:53 -07:00
}
await loginValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
try {
const { email , password } = req . body ;
// Check if user exists
const user = await this . db . getUserByEmail ( email );
// Compare password
const match = await user . comparePassword ( password );
if ( match !== true ) {
const error = new Error ( this . stringService . authIncorrectPassword );
error . status = 401 ;
next ( error );
return ;
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ... user . _doc };
delete userWithoutPassword . password ;
delete userWithoutPassword . avatarImage ;
// Happy path, return token
const appSettings = await this . settingsService . getSettings ();
const token = this . issueToken ( userWithoutPassword , appSettings );
// reset avatar image
userWithoutPassword . avatarImage = user . avatarImage ;
return res . success ({
msg : this . stringService . authLoginUser ,
data : {
user : userWithoutPassword ,
token : token ,
},
});
} catch ( error ) {
error . status = 401 ;
next ( handleError ( error , SERVICE_NAME , "loginUser" ));
}
};
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.userId - The ID of the user to be edited.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.password - The current password of the user.
* @property {string} req.body.newPassword - The new password of the user.
* @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 update of the user, and the updated user data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
*/
editUser = async ( req , res , next ) => {
try {
await editUserParamValidation . validateAsync ( req . params );
await editUserBodyValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
// TODO is this neccessary any longer? Verify ownership middleware should handle this
if ( req . params . userId !== req . user . _id . toString ()) {
const error = new Error ( this . stringService . unauthorized );
error . status = 401 ;
error . service = SERVICE_NAME ;
next ( error );
return ;
}
try {
// Change Password check
if ( req . body . password && req . body . newPassword ) {
// Get token from headers
const token = getTokenFromHeaders ( req . headers );
// Get email from token
const { jwtSecret } = this . settingsService . getSettings ();
const { email } = jwt . verify ( token , jwtSecret );
// Add user email to body for DB operation
req . body . email = email ;
// Get user
const user = await this . db . getUserByEmail ( email );
// Compare passwords
const match = await user . comparePassword ( req . body . password );
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if ( ! match ) {
const error = new Error ( this . stringService . authIncorrectPassword );
error . status = 403 ;
next ( error );
return ;
}
// If a match, update the password
req . body . password = req . body . newPassword ;
}
const updatedUser = await this . db . updateUser ( req , res );
res . success ({
msg : this . stringService . authUpdateUser ,
data : updatedUser ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "userEditController" ));
}
};
/**
* Checks if a superadmin account exists in the database.
* @async
* @param {Object} req - The Express request object.
* @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 existence of a superadmin, and a boolean indicating the existence of a superadmin.
* @throws {Error} If there is an error during the process.
*/
checkSuperadminExists = async ( req , res , next ) => {
try {
const superAdminExists = await this . db . checkSuperadmin ( req , res );
return res . success ({
msg : this . stringService . authAdminExists ,
data : superAdminExists ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "checkSuperadminController" ));
}
};
/**
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user requesting recovery.
* @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 creation of the recovery token, and the message ID of the sent email.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
requestRecovery = async ( req , res , next ) => {
try {
await recoveryValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
try {
const { email } = req . body ;
const user = await this . db . getUserByEmail ( email );
const recoveryToken = await this . db . requestRecoveryToken ( req , res );
const name = user . firstName ;
const { clientHost } = this . settingsService . getSettings ();
const url = ` ${ clientHost } /set-new-password/ ${ recoveryToken . token } ` ;
const msgId = await this . emailService . buildAndSendEmail (
"passwordResetTemplate" ,
{
name ,
email ,
url ,
},
email ,
"Checkmate Password Reset"
);
return res . success ({
msg : this . stringService . authCreateRecoveryToken ,
data : msgId ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "recoveryRequestController" ));
}
};
/**
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token to be validated.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
validateRecovery = async ( req , res , next ) => {
try {
await recoveryTokenValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
try {
await this . db . validateRecoveryToken ( req , res );
return res . success ({
msg : this . stringService . authVerifyRecoveryToken ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "validateRecoveryTokenController" ));
}
};
/**
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token.
* @property {string} req.body.password - The new password of the user.
* @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 reset of the password, the updated user data (without password and avatar image), and a new JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
resetPassword = async ( req , res , next ) => {
try {
await newPasswordValidation . validateAsync ( req . body );
} catch ( error ) {
const validationError = handleValidationError ( error , SERVICE_NAME );
next ( validationError );
return ;
}
try {
const user = await this . db . resetPassword ( req , res );
const appSettings = await this . settingsService . getSettings ();
const token = this . issueToken ( user . _doc , appSettings );
return res . success ({
msg : this . stringService . authResetPassword ,
data : { user , token },
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "resetPasswordController" ));
}
};
/**
* Deletes a user and all associated monitors, checks, and alerts.
*
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Function} next - The next middleware function.
* @returns {Object} The response object with success status and message.
* @throws {Error} If user validation fails or user is not found in the database.
*/
deleteUser = async ( req , res , next ) => {
try {
const token = getTokenFromHeaders ( req . headers );
const decodedToken = jwt . decode ( token );
const { email } = decodedToken ;
// Check if the user exists
const user = await this . db . getUserByEmail ( email );
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this . db . getMonitorsByTeamId ({
2025-06-10 09:16:38 +08:00
query : {},
params : { teamId : user . teamId . toString () },
2025-04-20 11:29:53 -07:00
});
if ( user . role . includes ( "superadmin" )) {
//2. Remove all jobs, delete checks and alerts
result ? . monitors . length > 0 &&
( await Promise . all (
result . monitors . map ( async ( monitor ) => {
await this . jobQueue . deleteJob ( monitor );
})
));
// 3. Delete team
await this . db . deleteTeam ( user . teamId );
// 4. Delete all other team members
await this . db . deleteAllOtherUsers ();
// 5. Delete each monitor
await this . db . deleteMonitorsByUserId ( user . _id );
}
// 6. Delete the user by id
await this . db . deleteUser ( user . _id );
return res . success ({
msg : this . stringService . authDeleteUser ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "deleteUserController" ));
}
};
getAllUsers = async ( req , res , next ) => {
try {
const allUsers = await this . db . getAllUsers ( req , res );
return res . success ({
msg : this . stringService . authGetAllUsers ,
data : allUsers ,
});
} catch ( error ) {
next ( handleError ( error , SERVICE_NAME , "getAllUsersController" ));
}
};
}
export default AuthController ;