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.
-
-
- - YOUR USERAME: ${username}
- - ACCESS LINK: ${constants.ANALYTICS_BASE_URL}/account/login
-
-
- 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.
+
+ - YOUR USERNAME: ${username}
+ - ACCESS LINK: ${constants.ANALYTICS_BASE_URL}/account/login
+
+
+ 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,
};