diff --git a/src/auth-service/controllers/user.controller.js b/src/auth-service/controllers/user.controller.js index fba8d083e9..626a14d7de 100644 --- a/src/auth-service/controllers/user.controller.js +++ b/src/auth-service/controllers/user.controller.js @@ -1664,6 +1664,169 @@ const createUser = { return; } }, + resetPasswordRequest: async (req, res, next) => { + try { + const errors = extractErrorsFromRequest(req); + if (errors) { + next( + new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors) + ); + return; + } + const { email } = req.body; + const tenant = req.query.tenant; + const token = userUtil.generateNumericToken(5); + const result = await userUtil.initiatePasswordReset( + { + email, + token, + tenant, + }, + next + ); + + res + .status(httpStatus.OK) + .json({ success: true, message: result.message }); + } catch (error) { + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + return; + } + }, + resetPassword: async (req, res, next) => { + try { + const errors = extractErrorsFromRequest(req); + if (errors) { + next( + new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors) + ); + return; + } + const { token } = req.params; + const { password } = req.body; + + const defaultTenant = constants.DEFAULT_TENANT || "airqo"; + const tenant = isEmpty(req.query.tenant) + ? defaultTenant + : req.query.tenant; + + const result = await userUtil.resetPassword( + { + token, + password, + tenant, + }, + next + ); + + res + .status(httpStatus.OK) + .json({ success: true, message: result.message }); + } catch (error) { + logObject("error in controller", error); + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + return; + } + }, + + registerMobileUser: async (req, res, next) => { + try { + const errors = extractErrorsFromRequest(req); + if (errors) { + next( + new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors) + ); + return; + } + + const request = req; + const defaultTenant = constants.DEFAULT_TENANT || "airqo"; + request.query.tenant = isEmpty(req.query.tenant) + ? defaultTenant + : req.query.tenant; + + request.body.analyticsVersion = 4; + + const result = await userUtil.registerMobileUser(request, next); + + if (isEmpty(result) || res.headersSent) { + return; + } + + if (result.success === true) { + res + .status(httpStatus.CREATED) + .json({ success: true, message: result.message, data: result.user }); + } else { + next(new HttpError(result.message, httpStatus.BAD_REQUEST)); + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + return; + } + }, + + verifyMobileEmail: async (req, res, next) => { + try { + const errors = extractErrorsFromRequest(req); + if (errors) { + next( + new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors) + ); + return; + } + const request = req; + const defaultTenant = constants.DEFAULT_TENANT || "airqo"; + request.query.tenant = isEmpty(req.query.tenant) + ? defaultTenant + : req.query.tenant; + + const result = await userUtil.verifyMobileEmail(request, next); + + if (isEmpty(result) || res.headersSent) { + return; + } + if (result.success) { + res + .status(httpStatus.OK) + .json({ success: true, message: result.message, data: result.user }); + } else { + next(new HttpError(result.message, httpStatus.BAD_REQUEST)); + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + return; + } + }, + subscribeToNewsLetter: async (req, res, next) => { try { const errors = extractErrorsFromRequest(req); diff --git a/src/auth-service/middleware/passport.js b/src/auth-service/middleware/passport.js index de407dea05..302922824c 100644 --- a/src/auth-service/middleware/passport.js +++ b/src/auth-service/middleware/passport.js @@ -172,6 +172,44 @@ const useEmailWithLocalStrategy = (tenant, req, res, next) => ) ); return; + } else if (user.analyticsVersion === 4 && !user.verified) { + await createUserUtil + .mobileVerificationReminder({ tenant, email: user.email }, next) + .then((verificationResponse) => { + if ( + !verificationResponse || + verificationResponse.success === false + ) { + logger.error( + `Verification reminder failed: ${ + verificationResponse + ? verificationResponse.message + : "No response" + }` + ); + } + }) + .catch((err) => { + logger.error( + `Error sending verification reminder: ${err.message}` + ); + }); + + req.auth.success = false; + req.auth.message = + "account not verified, verification email has been sent to your email"; + req.auth.status = httpStatus.FORBIDDEN; + next( + new HttpError( + "account not verified, verification email has been sent to your email", + httpStatus.FORBIDDEN, + { + message: + "account not verified, verification email has been sent to your email", + } + ) + ); + return; } req.auth.success = true; req.auth.message = "successful login"; @@ -294,6 +332,44 @@ const useUsernameWithLocalStrategy = (tenant, req, res, next) => ) ); return; + } else if (user.analyticsVersion === 4 && !user.verified) { + createUserUtil + .mobileVerificationReminder({ tenant, email: user.email }, next) + .then((verificationResponse) => { + if ( + !verificationResponse || + verificationResponse.success === false + ) { + logger.error( + `Verification reminder failed: ${ + verificationResponse + ? verificationResponse.message + : "No response" + }` + ); + } + }) + .catch((err) => { + logger.error( + `Error sending verification reminder: ${err.message}` + ); + }); + + req.auth.success = false; + req.auth.message = + "account not verified, verification email has been sent to your email"; + req.auth.status = httpStatus.FORBIDDEN; + next( + new HttpError( + "account not verified, verification email has been sent to your email", + httpStatus.FORBIDDEN, + { + message: + "account not verified, verification email has been sent to your email", + } + ) + ); + return; } req.auth.success = true; req.auth.message = "successful login"; diff --git a/src/auth-service/models/User.js b/src/auth-service/models/User.js index ba938c75d7..11a8f25ea6 100644 --- a/src/auth-service/models/User.js +++ b/src/auth-service/models/User.js @@ -93,7 +93,6 @@ const UserSchema = new Schema( }, userName: { type: String, - required: [true, "UserName is required!"], trim: true, unique: true, }, @@ -404,6 +403,14 @@ UserSchema.pre( return next(new Error("Phone number or email is required!")); } + if (!this.userName && this.email) { + this.userName = this.email; + } + + if (!this.userName) { + return next(new Error("userName is required!")); + } + // Profile picture validation - only for new documents if ( this.profilePicture && @@ -470,10 +477,6 @@ UserSchema.pre( ]; } - // Ensure default values for new documents - this.verified = this.verified ?? false; - this.analyticsVersion = this.analyticsVersion ?? 2; - // Permissions handling for new documents if (this.permissions && this.permissions.length > 0) { this.permissions = [...new Set(this.permissions)]; @@ -581,15 +584,6 @@ UserSchema.pre( updates.permissions = uniquePermissions; } } - - // Conditional default values for updates - if (updates && updates.$set) { - updates.$set.verified = updates.$set.verified ?? false; - updates.$set.analyticsVersion = updates.$set.analyticsVersion ?? 2; - } else { - updates.verified = updates.verified ?? false; - updates.analyticsVersion = updates.analyticsVersion ?? 2; - } } // Additional checks for new documents diff --git a/src/auth-service/routes/v2/users.routes.js b/src/auth-service/routes/v2/users.routes.js index bf362448bc..dcd42645b5 100644 --- a/src/auth-service/routes/v2/users.routes.js +++ b/src/auth-service/routes/v2/users.routes.js @@ -1,7 +1,7 @@ // users.routes.js const express = require("express"); const router = express.Router(); -const createUserController = require("@controllers/user.controller"); +const userController = require("@controllers/user.controller"); const userValidations = require("@validators/users.validators"); const { setJWTAuth, @@ -31,7 +31,7 @@ router.use(userValidations.pagination); router.get( "/deleteMobileUserData/:userId/:token", userValidations.deleteMobileUserData, - createUserController.deleteMobileUserData + userController.deleteMobileUserData ); router.post( @@ -39,7 +39,7 @@ router.post( userValidations.login, setLocalAuth, authLocal, - createUserController.login + userController.login ); router.post( @@ -47,7 +47,7 @@ router.post( userValidations.login, setLocalAuth, authLocal, - createUserController.login + userController.login ); router.post( @@ -55,7 +55,7 @@ router.post( userValidations.login, setLocalAuth, authLocal, - createUserController.loginWithDetails + userController.loginWithDetails ); router.get( @@ -63,7 +63,7 @@ router.get( userValidations.tenant, setJWTAuth, authJWT, - createUserController.logout + userController.logout ); router.post( @@ -71,55 +71,51 @@ router.post( userValidations.tenant, setGuestToken, authGuest, - createUserController.guest + userController.guest ); router.post( "/emailLogin", userValidations.emailLogin, - createUserController.loginInViaEmail + userController.loginInViaEmail ); router.post( "/emailAuth/:purpose?", userValidations.emailAuth, - createUserController.emailAuth + userController.emailAuth ); -router.post( - "/feedback", - userValidations.feedback, - createUserController.sendFeedback -); +router.post("/feedback", userValidations.feedback, userController.sendFeedback); router.post( "/firebase/lookup", userValidations.firebaseLookup, - createUserController.lookUpFirebaseUser + userController.lookUpFirebaseUser ); router.post( "/firebase/create", userValidations.firebaseCreate, - createUserController.createFirebaseUser + userController.createFirebaseUser ); router.post( "/firebase/login", userValidations.firebaseLogin, - createUserController.loginWithFirebase + userController.loginWithFirebase ); router.post( "/firebase/signup", userValidations.firebaseSignup, - createUserController.signUpWithFirebase + userController.signUpWithFirebase ); router.post( "/syncAnalyticsAndMobile", userValidations.syncAnalyticsAndMobile, - createUserController.syncAnalyticsAndMobile + userController.syncAnalyticsAndMobile ); router.post( @@ -127,66 +123,61 @@ router.post( userValidations.emailReport, setJWTAuth, authJWT, - createUserController.emailReport + userController.emailReport ); router.post( "/firebase/verify", userValidations.firebaseVerify, - createUserController.verifyFirebaseCustomToken + userController.verifyFirebaseCustomToken ); -router.post("/verify", setJWTAuth, authJWT, createUserController.verify); +router.post("/verify", setJWTAuth, authJWT, userController.verify); router.get( "/combined", userValidations.tenant, setJWTAuth, authJWT, - createUserController.listUsersAndAccessRequests + userController.listUsersAndAccessRequests ); router.get( "/verify/:user_id/:token", userValidations.verifyEmail, - createUserController.verifyEmail + userController.verifyEmail ); router.get( "/auth/google/callback", setGoogleAuth, authGoogleCallback, - createUserController.googleCallback + userController.googleCallback ); -router.get( - "/auth/google", - setGoogleAuth, - authGoogle, - createUserController.login -); +router.get("/auth/google", setGoogleAuth, authGoogle, userController.login); router.get( "/", userValidations.tenant, setJWTAuth, authJWT, - createUserController.list + userController.list ); router.post( "/registerUser", userValidations.registerUser, - createUserController.register + userController.register ); -router.post("/", userValidations.createUser, createUserController.create); +router.post("/", userValidations.createUser, userController.create); router.put( "/updatePasswordViaEmail", userValidations.updatePasswordViaEmail, setJWTAuth, - createUserController.updateForgottenPassword + userController.updateForgottenPassword ); router.put( @@ -194,29 +185,49 @@ router.put( userValidations.updatePassword, setJWTAuth, authJWT, - createUserController.updateKnownPassword + userController.updateKnownPassword ); router.post( "/forgotPassword", userValidations.forgotPassword, - createUserController.forgot + userController.forgot ); -router.put("/", userValidations.updateUser, createUserController.update); +router.post( + "/reset-password-request", + userValidations.resetPasswordRequest, + userController.resetPasswordRequest +); -router.put( - "/:user_id", - userValidations.updateUserById, - createUserController.update +router.post( + "/reset-password/:token", + userValidations.resetPassword, + userController.resetPassword ); +router.post( + "/register", + userValidations.createUser, + userController.registerMobileUser +); + +router.post( + "/verify-email/:token", + userValidations.verifyMobileEmail, + userController.verifyMobileEmail +); + +router.put("/", userValidations.updateUser, userController.update); + +router.put("/:user_id", userValidations.updateUserById, userController.update); + router.delete( "/", userValidations.deleteUser, setJWTAuth, authJWT, - createUserController.delete + userController.delete ); router.delete( @@ -224,25 +235,25 @@ router.delete( userValidations.deleteUserById, setJWTAuth, authJWT, - createUserController.delete + userController.delete ); router.post( "/newsletter/subscribe", userValidations.newsletterSubscribe, - createUserController.subscribeToNewsLetter + userController.subscribeToNewsLetter ); router.post( "/newsletter/resubscribe", userValidations.newsletterResubscribe, - createUserController.reSubscribeToNewsLetter + userController.reSubscribeToNewsLetter ); router.post( "/newsletter/unsubscribe", userValidations.newsletterUnsubscribe, - createUserController.unSubscribeFromNewsLetter + userController.unSubscribeFromNewsLetter ); router.get( @@ -250,7 +261,7 @@ router.get( userValidations.tenant, setJWTAuth, authJWT, - createUserController.listStatistics + userController.listStatistics ); router.get( @@ -258,7 +269,7 @@ router.get( userValidations.cache, setJWTAuth, authJWT, - createUserController.listCache + userController.listCache ); router.get( @@ -266,7 +277,7 @@ router.get( userValidations.tenant, setJWTAuth, authJWT, - createUserController.listLogs + userController.listLogs ); router.get( @@ -274,25 +285,25 @@ router.get( userValidations.tenant, setJWTAuth, authJWT, - createUserController.getUserStats + userController.getUserStats ); router.post( "/subscribe/:type", userValidations.subscribeToNotifications, - createUserController.subscribeToNotifications + userController.subscribeToNotifications ); router.post( "/unsubscribe/:type", userValidations.unSubscribeFromNotifications, - createUserController.unSubscribeFromNotifications + userController.unSubscribeFromNotifications ); router.post( "/notification-status/:type", userValidations.notificationStatus, - createUserController.checkNotificationStatus + userController.checkNotificationStatus ); router.get( @@ -300,7 +311,7 @@ router.get( userValidations.getUser, setJWTAuth, authJWT, - createUserController.list + userController.list ); module.exports = router; diff --git a/src/auth-service/utils/common/email.msgs.js b/src/auth-service/utils/common/email.msgs.js index b54c5af8fd..ad5822c295 100644 --- a/src/auth-service/utils/common/email.msgs.js +++ b/src/auth-service/utils/common/email.msgs.js @@ -41,6 +41,33 @@ module.exports = { `; return constants.EMAIL_BODY({ email, content }); }, + mobilePasswordReset: ({ token, email }) => { + const content = ` + + +

You requested a password reset for your AirQo account associated with ${email}.

+

Use this code to finish setting up your new password:

+

${token}

+

This code will expire in 24 hours.

+ + + `; + return constants.EMAIL_BODY({ email, content }); + }, + mobileEmailVerification: ({ token, email }) => { + const content = ` + + +

Welcome to AirQo! Thank you for registering.

+

Please use the code below to verify your email address (${email}):

+

${token}

+

This code will expire in 24 hours.

+

If you did not register for an AirQo account, you can safely ignore this email.

+ + + `; + return constants.EMAIL_BODY({ email, content }); + }, joinRequest: (firstName, lastName, email) => { const name = firstName + " " + lastName; const content = ` @@ -529,7 +556,8 @@ module.exports = {

You already exist as an AirQo User.

-

Please use the FORGOT PASSWORD feature by clicking HERE.

+

For AirQo Web, please use the FORGOT PASSWORD feature by clicking HERE.

+

For AirQo Mobile, please use the FORGOT PASSWORD feature in the app.

`; diff --git a/src/auth-service/utils/common/email.templates.js b/src/auth-service/utils/common/email.templates.js index 77458d8c74..9c622410cd 100644 --- a/src/auth-service/utils/common/email.templates.js +++ b/src/auth-service/utils/common/email.templates.js @@ -1,4 +1,6 @@ const constants = require("@config/constants"); +const { log } = require("async"); +const { logObject } = require("@utils/shared"); const processString = (inputString) => { const stringWithSpaces = inputString.replace(/[^a-zA-Z0-9]+/g, " "); const uppercasedString = stringWithSpaces.toUpperCase(); @@ -144,33 +146,54 @@ module.exports = { `; return constants.EMAIL_BODY({ email, content }); }, - afterEmailVerification: (firstName, username, email) => { + afterEmailVerification: ( + { firstName, username, email, analyticsVersion = 3 } = {}, + next + ) => { const name = firstName; - const content = ` - - Congratulations! Your account has been successfully verified. -
- We are pleased to inform you that you can now fully access all of the features and services offered by AirQo Analytics. -
- -
- If you have any questions or need assistance with anything, please don't hesitate to reach out to our customer support - team. We are here to help. -
- Thank you for choosing AirQo Analytics, and we look forward to helping you achieve your goals -
-
- Sincerely, -
- The AirQo Data Team - - `; + let content = ""; + + if (analyticsVersion === 4) { + content = ` + + +

Congratulations! Your AirQo account has been successfully verified.

+

You can now fully access all the features and services offered by the AirQo mobile application.

+ +

If you have any questions or need assistance, please don't hesitate to contact our customer support team at support@airqo.net. We are here to help.

+
+

Thank you for choosing AirQo.

+
+

Sincerely,

+

The AirQo Team

+ + `; + } else { + // other user types (web app) + content = ` + +

Congratulations! Your account has been successfully verified.

+

We are pleased to inform you that you can now fully access all of the features and services offered by AirQo Analytics.

+ +
+

If you have any questions or need assistance, please don't hesitate to contact our customer support team at support@airqo.net. We are here to help.

+
+

Thank you for choosing AirQo Analytics, and we look forward to helping you achieve your goals

+
+
+

Sincerely,

+
+

The AirQo Data Team

+ + `; + } + return constants.EMAIL_BODY({ email, content, name }); }, + afterAcceptingInvitation: ({ firstName, username, email, entity_title }) => { const name = firstName; const content = ` diff --git a/src/auth-service/utils/common/mailer.js b/src/auth-service/utils/common/mailer.js index a2f0a30998..daee70eba2 100644 --- a/src/auth-service/utils/common/mailer.js +++ b/src/auth-service/utils/common/mailer.js @@ -880,6 +880,120 @@ const mailer = { ); } }, + sendVerificationEmail: async ({ email, token, tenant }, next) => { + try { + const checkResult = await SubscriptionModel( + tenant + ).checkNotificationStatus({ email, type: "email" }); + if (!checkResult.success) { + return checkResult; + } + let bccEmails = []; + + if (constants.REQUEST_ACCESS_EMAILS) { + bccEmails = constants.REQUEST_ACCESS_EMAILS.split(","); + } + + let subscribedEmails = []; + + for (let i = 0; i < bccEmails.length; i++) { + const bccEmail = bccEmails[i].trim(); + const checkResult = await SubscriptionModel( + tenant + ).checkNotificationStatus({ email: bccEmail, type: "email" }); + + if (checkResult.success) { + subscribedEmails.push(bccEmail); + } + } + + const subscribedBccEmails = subscribedEmails.join(","); + // bcc: subscribedBccEmails, + + const mailOptions = { + from: { + name: constants.EMAIL_NAME, + address: constants.EMAIL, + }, + to: `${email}`, + subject: `Email Verification Code: ${token}`, + html: msgs.mobileEmailVerification({ token, email }), + attachments: [ + { + filename: "airqoLogo.png", + path: imagePath + "/airqoLogo.png", + cid: "AirQoEmailLogo", + contentDisposition: "inline", + }, + { + filename: "faceBookLogo.png", + path: imagePath + "/facebookLogo.png", + cid: "FacebookLogo", + contentDisposition: "inline", + }, + { + filename: "youtubeLogo.png", + path: imagePath + "/youtubeLogo.png", + cid: "YoutubeLogo", + contentDisposition: "inline", + }, + { + filename: "twitterLogo.png", + path: imagePath + "/Twitter.png", + cid: "Twitter", + contentDisposition: "inline", + }, + { + filename: "linkedInLogo.png", + path: imagePath + "/linkedInLogo.png", + cid: "LinkedInLogo", + contentDisposition: "inline", + }, + ], + }; + + if (email === "automated-tests@airqo.net") { + return { + success: true, + message: "email successfully sent", + data: [], + status: httpStatus.OK, + }; + } + + let response = transporter.sendMail(mailOptions); + let data = await response; + if (isEmpty(data.rejected) && !isEmpty(data.accepted)) { + return { + success: true, + message: "email successfully sent", + data, + status: httpStatus.OK, + }; + } else { + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { + message: "email not sent", + emailResults: data, + } + ) + ); + } + } catch (error) { + logObject("the error in the mailer", error); + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, verifyMobileEmail: async ( { firebase_uid = "", token = "", email = "", tenant = "airqo" } = {}, next @@ -1003,7 +1117,14 @@ const mailer = { } }, afterEmailVerification: async ( - { firstName = "", username = "", email = "", tenant = "airqo" } = {}, + { + firstName = "", + lastName = "", + username = "", + email = "", + tenant = "airqo", + analyticsVersion, + } = {}, next ) => { try { @@ -1044,7 +1165,16 @@ const mailer = { }, to: `${email}`, subject: "Welcome to AirQo!", - html: msgTemplates.afterEmailVerification(firstName, username, email), + html: msgTemplates.afterEmailVerification( + { + firstName, + lastName, + username, + email, + analyticsVersion, + }, + next + ), attachments: attachments, }; @@ -1311,6 +1441,55 @@ const mailer = { ); } }, + + sendPasswordResetEmail: async ({ email, token, tenant = "airqo" }, next) => { + try { + const checkResult = await SubscriptionModel( + tenant + ).checkNotificationStatus({ email, type: "email" }); + if (!checkResult.success) { + return checkResult; + } + + const mailOptions = { + from: { + name: constants.EMAIL_NAME, + address: constants.EMAIL, + }, + to: email, + subject: `Password Reset Code: ${token}`, + html: msgs.mobilePasswordReset({ token, email }), + attachments: attachments, + }; + + let response = transporter.sendMail(mailOptions); + let data = await response; + + if (isEmpty(data.rejected) && !isEmpty(data.accepted)) { + return { + success: true, + message: "Email sent successfully", + data, + status: httpStatus.OK, + }; + } else { + next( + new HttpError("Email not sent", httpStatus.INTERNAL_SERVER_ERROR, { + emailResults: data, + }) + ); + } + } catch (error) { + logger.error(`Error sending password reset email: ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, signInWithEmailLink: async ( { email, token, tenant = "airqo" } = {}, next diff --git a/src/auth-service/utils/user.util.js b/src/auth-service/utils/user.util.js index eaea86a85b..31751cc743 100644 --- a/src/auth-service/utils/user.util.js +++ b/src/auth-service/utils/user.util.js @@ -1543,6 +1543,63 @@ const createUserModule = { ); } }, + registerMobileUser: async (request, next) => { + try { + const { tenant } = { + ...request.body, + ...request.query, + ...request.params, + }; + + const userData = request.body; + const verificationToken = generateNumericToken(5); + + const newUserResponse = await UserModel(tenant).register(userData, next); + + if (newUserResponse.success === true) { + const newUser = newUserResponse.data; + + // Add this block to store the token in the VerifyToken collection: + const tokenExpiry = 86400; //24hrs in seconds. Feel free to use any value + + const tokenCreationBody = { + token: verificationToken, + name: newUser.firstName, + expires: new Date(Date.now() + tokenExpiry * 1000), // Set token expiry + }; + + const verifyTokenResponse = await VerifyTokenModel( + tenant.toLowerCase() + ).register(tokenCreationBody, next); + + if (verifyTokenResponse.success === false) { + // Consider rolling back user creation + logger.error( + `Failed to create verification token for user ${newUser.email}: ${verifyTokenResponse.message}` + ); + + return verifyTokenResponse; + } + + await mailer.sendVerificationEmail({ + email: userData.email, + token: verificationToken, + }); + + return { + success: true, + message: "User registered successfully. Please verify your email.", + user: newUser, + }; + } else { + return newUserResponse; + } + } catch (error) { + logObject("error in reg", error); + return { success: false, message: error.message }; + } + }, + verificationReminder: async (request, next) => { try { const { tenant, email } = request; @@ -1618,6 +1675,347 @@ const createUserModule = { ); } }, + mobileVerificationReminder: async (request, next) => { + try { + const { tenant, email } = request; + + const user = await UserModel(tenant) + .findOne({ email }) + .select("_id email firstName lastName verified") + .lean(); + + if (isEmpty(user)) { + next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "User not provided or does not exist", + }) + ); + } + + const token = generateNumericToken(5); + + const tokenCreationBody = { + token, + name: user.firstName, + }; + const responseFromCreateToken = await VerifyTokenModel( + tenant.toLowerCase() + ).register(tokenCreationBody, next); + + if (responseFromCreateToken.success === false) { + return responseFromCreateToken; + } else { + const emailResponse = await mailer.sendVerificationEmail( + { email, token, tenant }, + next + ); + logObject("emailResponse", emailResponse); + if (emailResponse.success === false) { + logger.error( + `Failed to send mobile verification email to user (${email}) with id ${user._id}` + ); + return emailResponse; + } + + const userDetails = { + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + verified: user.verified, + }; + return { + success: true, + message: "Verification code sent to your email.", + data: userDetails, + }; + } + } catch (error) { + logObject("error in mobileVerificationReminder", error); + + logger.error(`Error sending verification reminder: ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, + verifyMobileEmail: async (request, next) => { + try { + const { email, token, tenant, skip, limit } = { + ...request.body, + ...request.query, + ...request.params, + }; + const timeZone = moment.tz.guess(); + let filter = { + token, + expires: { + $gt: moment().tz(timeZone).toDate(), + }, + }; + + const userDetails = await UserModel("airqo") + .find({ + email, + }) + .select("_id firstName lastName userName email verified") + .lean(); + + const user = userDetails[0]; + + if (isEmpty(user)) { + return { + success: false, + message: "Invalid Verification Token or the User does not exist", + errors: { + message: "Invalid Verification Token or the User does not exist", + }, + }; + } + + const responseFromListAccessToken = await VerifyTokenModel(tenant).list( + { + skip, + limit, + filter, + }, + next + ); + + logObject("responseFromListAccessToken", responseFromListAccessToken); + if (responseFromListAccessToken.success === true) { + if (responseFromListAccessToken.status === httpStatus.NOT_FOUND) { + next( + new HttpError("Invalid link", httpStatus.BAD_REQUEST, { + message: "incorrect user or token details provided", + }) + ); + } else if (responseFromListAccessToken.status === httpStatus.OK) { + let update = { + verified: true, + }; + filter = { email }; + + const responseFromUpdateUser = await UserModel(tenant).modify( + { + filter, + update, + }, + next + ); + + if (responseFromUpdateUser.success === true) { + /** + * we shall also need to handle case where there was no update + * later...cases where the user never existed in the first place + * this will not be necessary if user deletion is cascaded. + */ + if (responseFromUpdateUser.status === httpStatus.BAD_REQUEST) { + return responseFromUpdateUser; + } + + filter = { token }; + logObject("the deletion of the token filter", filter); + const responseFromDeleteToken = await VerifyTokenModel( + tenant + ).remove({ filter }, next); + + logObject("responseFromDeleteToken", responseFromDeleteToken); + + if (responseFromDeleteToken.success === true) { + logObject("user", user); + const responseFromSendEmail = await mailer.afterEmailVerification( + { + firstName: user.firstName, + username: user.userName, + email: user.email, + analyticsVersion: 4, + }, + next + ); + + if (responseFromSendEmail.success === true) { + return { + success: true, + message: "email verified sucessfully", + status: httpStatus.OK, + }; + } else if (responseFromSendEmail.success === false) { + return responseFromSendEmail; + } + } else if (responseFromDeleteToken.success === false) { + next( + new HttpError( + "unable to verify user", + responseFromDeleteToken.status + ? responseFromDeleteToken.status + : httpStatus.INTERNAL_SERVER_ERROR, + responseFromDeleteToken.errors + ? responseFromDeleteToken.errors + : { message: "internal server errors" } + ) + ); + } + } else if (responseFromUpdateUser.success === false) { + next( + new HttpError( + "unable to verify user", + responseFromUpdateUser.status + ? responseFromUpdateUser.status + : httpStatus.INTERNAL_SERVER_ERROR, + responseFromUpdateUser.errors + ? responseFromUpdateUser.errors + : { message: "internal server errors" } + ) + ); + } + } + } else if (responseFromListAccessToken.success === false) { + return responseFromListAccessToken; + } + } catch (error) { + return { success: false, message: error.message }; + } + }, + verifyEmail: async (request, next) => { + try { + const { tenant, limit, skip, user_id, token } = { + ...request.query, + ...request.params, + }; + const timeZone = moment.tz.guess(); + let filter = { + token, + expires: { + $gt: moment().tz(timeZone).toDate(), + }, + }; + + const userDetails = await UserModel(tenant) + .find({ + _id: ObjectId(user_id), + }) + .lean(); + + if (isEmpty(userDetails)) { + next( + new HttpError("Bad Reqest Error", httpStatus.BAD_REQUEST, { + message: "User does not exist", + }) + ); + } + + const responseFromListAccessToken = await VerifyTokenModel(tenant).list( + { + skip, + limit, + filter, + }, + next + ); + + if (responseFromListAccessToken.success === true) { + if (responseFromListAccessToken.status === httpStatus.NOT_FOUND) { + next( + new HttpError("Invalid link", httpStatus.BAD_REQUEST, { + message: "incorrect user or token details provided", + }) + ); + } else if (responseFromListAccessToken.status === httpStatus.OK) { + let update = { + verified: true, + }; + filter = { _id: user_id }; + + const responseFromUpdateUser = await UserModel(tenant).modify( + { + filter, + update, + }, + next + ); + + if (responseFromUpdateUser.success === true) { + /** + * we shall also need to handle case where there was no update + * later...cases where the user never existed in the first place + * this will not be necessary if user deletion is cascaded. + */ + if (responseFromUpdateUser.status === httpStatus.BAD_REQUEST) { + return responseFromUpdateUser; + } + + filter = { token }; + logObject("the deletion of the token filter", filter); + const responseFromDeleteToken = await VerifyTokenModel( + tenant + ).remove({ filter }, next); + + logObject("responseFromDeleteToken", responseFromDeleteToken); + + if (responseFromDeleteToken.success === true) { + const responseFromSendEmail = await mailer.afterEmailVerification( + { + firstName: userDetails[0].firstName, + username: userDetails[0].userName, + email: userDetails[0].email, + }, + next + ); + + if (responseFromSendEmail.success === true) { + return { + success: true, + message: "email verified sucessfully", + status: httpStatus.OK, + }; + } else if (responseFromSendEmail.success === false) { + return responseFromSendEmail; + } + } else if (responseFromDeleteToken.success === false) { + next( + new HttpError( + "unable to verify user", + responseFromDeleteToken.status + ? responseFromDeleteToken.status + : httpStatus.INTERNAL_SERVER_ERROR, + responseFromDeleteToken.errors + ? responseFromDeleteToken.errors + : { message: "internal server errors" } + ) + ); + } + } else if (responseFromUpdateUser.success === false) { + next( + new HttpError( + "unable to verify user", + responseFromUpdateUser.status + ? responseFromUpdateUser.status + : httpStatus.INTERNAL_SERVER_ERROR, + responseFromUpdateUser.errors + ? responseFromUpdateUser.errors + : { message: "internal server errors" } + ) + ); + } + } + } else if (responseFromListAccessToken.success === false) { + return responseFromListAccessToken; + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, create: async (request, next) => { try { const { tenant, firstName, email, password, category } = { @@ -2041,6 +2439,102 @@ const createUserModule = { ); } }, + initiatePasswordReset: async ({ email, token, tenant }, next) => { + try { + const update = { + resetPasswordToken: token, + resetPasswordExpires: Date.now() + 3600000, + }; + const responseFromModifyUser = await UserModel(tenant) + .findOneAndUpdate({ email }, update, { new: true }) + .select("firstName lastName email"); + + if (isEmpty(responseFromModifyUser)) { + next( + new HttpError("Bad Request Error", httpStatus.INTERNAL_SERVER_ERROR, { + message: "user does not exist, please crosscheck", + }) + ); + } + + await mailer.sendPasswordResetEmail({ email, token, tenant }); + + return { + success: true, + message: "Password reset email sent successfully", + }; + } catch (error) { + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Unable to initiate password reset", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, + resetPassword: async ({ token, password, tenant }, next) => { + try { + const resetPasswordToken = token; + const timeZone = moment.tz.guess(); + const filter = { + resetPasswordToken, + resetPasswordExpires: { + $gt: moment().tz(timeZone).toDate(), + }, + }; + + const user = await UserModel(tenant).findOne(filter); + if (!user) { + throw new HttpError( + "Password reset token is invalid or has expired.", + httpStatus.BAD_REQUEST + ); + } + const update = { + resetPasswordToken: null, + resetPasswordExpires: null, + password, + }; + + const responseFromModifyUser = await UserModel(tenant) + .findOneAndUpdate({ _id: ObjectId(user._id) }, update, { new: true }) + .select("firstName lastName email"); + + const { email, firstName, lastName } = responseFromModifyUser._doc; + + const responseFromSendEmail = await mailer.updateForgottenPassword( + { + email, + firstName, + lastName, + }, + next + ); + + logObject("responseFromSendEmail", responseFromSendEmail); + + if (responseFromSendEmail.success === true) { + return { + success: true, + message: "Password reset successful", + }; + } else if (responseFromSendEmail.success === false) { + return responseFromSendEmail; + } + } catch (error) { + logObject("error", error); + logger.error(`🐛🐛 Internal Server Error ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, generateResetToken: (next) => { try { const token = crypto.randomBytes(20).toString("hex"); @@ -2647,4 +3141,4 @@ const createUserModule = { }, }; -module.exports = createUserModule; +module.exports = { ...createUserModule, generateNumericToken }; diff --git a/src/auth-service/validators/users.validators.js b/src/auth-service/validators/users.validators.js index 96b6291346..522bc5d3d1 100644 --- a/src/auth-service/validators/users.validators.js +++ b/src/auth-service/validators/users.validators.js @@ -329,6 +329,35 @@ const verifyEmail = [ ], ]; +const verifyMobileEmail = [ + validateTenant, + [ + param("token") + .exists() + .withMessage("Token is required") + .bail() + .notEmpty() + .withMessage("the token should not be empty") + .bail() + .isNumeric() + .withMessage("Token must be numeric") + .bail() + .isLength({ min: 5, max: 5 }) + .withMessage("Token must be 5 digits") + .trim(), + body("email") + .exists() + .withMessage("email is missing in your request") + .bail() + .notEmpty() + .withMessage("the email should not be empty") + .bail() + .isEmail() + .withMessage("this is not a valid email address") + .trim(), + ], +]; + const registerUser = [ validateTenant, [ @@ -842,6 +871,46 @@ const getUser = [ ], ]; +const resetPasswordRequest = [ + body("email") + .exists() + .withMessage("Email is required") + .isEmail() + .withMessage("Invalid email format") + .trim(), + //Potentially add tenant validation here as well, using the oneOf approach if necessary +]; + +const resetPassword = [ + param("token") + .exists() + .withMessage("Token is required") + .bail() + .isNumeric() + .withMessage("Token must be numeric") + .isLength({ min: 5, max: 5 }) + .withMessage("Token must be 5 digits") + .trim(), + body("password") + .exists() + .withMessage("Password is required") + .bail() + .isLength({ min: 6 }) + .withMessage("Password must be at least 6 characters long") + .trim(), + body("confirmPassword") + .exists() + .withMessage("Confirm Password is required") + .bail() + .custom((value, { req }) => { + if (value !== req.body.password) { + throw new Error("Passwords do not match"); + } + return true; + }) + .trim(), +]; + module.exports = { tenant: validateTenant, AirqoTenantOnly: validateAirqoTenantOnly, @@ -876,4 +945,7 @@ module.exports = { unSubscribeFromNotifications, notificationStatus, getUser, + resetPasswordRequest, + resetPassword, + verifyMobileEmail, };