diff --git a/__test__/cart.test.ts b/__test__/cart.test.ts index e5b12f7..c33b0c2 100644 --- a/__test__/cart.test.ts +++ b/__test__/cart.test.ts @@ -11,6 +11,11 @@ import { dummy } from "./prod"; import * as userServices from "../src/services/user.service" import { number } from "joi"; +jest.mock("../src/services/mail.service", () => ({ + sendEmailService: jest.fn(), + sendNotification: jest.fn(), +})); + const queryInterface = sequelize.getQueryInterface(); let sellerToken: any; @@ -28,6 +33,7 @@ describe("testing cart", () => { name: "admin123", username: "admin123", email: "admin1@example.com", + isVerified:true, password: await bcrypt.hash("password", 10), roleId: 3, }; @@ -35,12 +41,14 @@ describe("testing cart", () => { const testBuyer = { name: "buyer123", username: "buyer123", + isVerified:true, email: "buyer1@example.com", password: await bcrypt.hash("password", 10), }; const testSeller = { name: "seller123", username: "seller123", + isVerified:true, email: "seller123@example.com", password: await bcrypt.hash("password", 10), }; diff --git a/__test__/payment.test.ts b/__test__/payment.test.ts index 0070c7e..d936c0f 100644 --- a/__test__/payment.test.ts +++ b/__test__/payment.test.ts @@ -15,6 +15,12 @@ let adminToken: any; let sellerToken: any; +jest.mock("../src/services/mail.service", () => ({ + sendEmailService: jest.fn(), + sendNotification: jest.fn(), +})); + + describe("test stripe api payment", () => { beforeAll(async () => { try { @@ -23,6 +29,7 @@ describe("test stripe api payment", () => { const testAdmin = { name: "admin123", username: "admin123", + isVerified:true, email: "admin1@example.com", password: await bcrypt.hash("password", 10), roleId: 3, @@ -31,6 +38,7 @@ describe("test stripe api payment", () => { const testBuyer = { name: "buyer123", username: "buyer123", + isVerified:true, email: "buyer1@example.com", password: await bcrypt.hash("password", 10), }; @@ -38,6 +46,7 @@ describe("test stripe api payment", () => { const testSeller = { name: "seller123", username: "seller123", + isVerified:true, email: "seller123@example.com", password: await bcrypt.hash("password", 10), }; diff --git a/__test__/product.test.ts b/__test__/product.test.ts index 5b95c9e..027c060 100644 --- a/__test__/product.test.ts +++ b/__test__/product.test.ts @@ -10,16 +10,21 @@ import User from "../src/sequelize/models/users"; import bcrypt from "bcryptjs"; import { Role } from "../src/sequelize/models/roles"; import redisClient from "../src/config/redis"; -import { response } from "express"; import { placeOrder } from "../src/services/payment.service"; import Cart from "../src/sequelize/models/Cart"; -import CartItem from "../src/sequelize/models/CartItem"; import OrderItem from "../src/sequelize/models/orderItems"; import * as userService from "../src/services/user.service" +import { generateVerificationToken } from "../src/utils/generateResetToken"; + +jest.mock("../src/services/mail.service", () => ({ + sendEmailService: jest.fn(), + sendNotification: jest.fn(), +})); const userData: any = { name: "yvanna", username: "testuser", + isVerified:true, email: "test1@gmail.com", role:"seller", password: "test1234", @@ -36,6 +41,7 @@ const product:any = { name: "pens", images: ["image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg"], stockQuantity: 8, + price: 5000, discount: 3.5, categoryID: 1, @@ -44,6 +50,7 @@ const product:any = { const dummyBuyer = { name: "test user", username: "testUser", + isVerified:true, email: "soleil@soleil0w.com", password: "soleil00", } @@ -77,8 +84,7 @@ describe("Testing product Routes", () => { ]) await User.create(testAdmin); - - const dummy = await request(app).post("/api/v1/users/register").send(dummySeller); + // await User.create(dummySeller); await Product.destroy({}); await Category.destroy({truncate:true}); } catch (error) { @@ -92,6 +98,7 @@ describe("Testing product Routes", () => { await sequelize.close(); await redisClient.quit() }); + test("should return 201 and create a new user when registering successfully", async () => { const response = await request(app) .post("/api/v1/users/register") @@ -105,7 +112,20 @@ describe("Testing product Routes", () => { .send(dummyBuyer); expect(response.status).toBe(201); }) + test('should return 201 and register a dummy buyer user', async () => { + const response = await request(app) + .post("/api/v1/users/register") + .send(dummySeller); + expect(response.status).toBe(201); + }) let buyerToken: any; + it("It should verify user account.",async()=>{ + const token = generateVerificationToken('soleil@soleil0w.com', 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) test("should login an buyer", async () =>{ const response = await request(app).post("/api/v1/users/login").send({ @@ -122,21 +142,37 @@ describe("Testing product Routes", () => { .send(product) expect(response.status).toBe(401); },2000); - + + it("It should verify user account.",async()=>{ + const token = generateVerificationToken('admin1@example.com', 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) test("should login an Admin", async () =>{ const response = await request(app).post("/api/v1/users/login").send({ email: "admin1@example.com", - password: "password" + password: "password" }) adminToken = response.body.token; + expect(response.status).toBe(200) }); + it("It should verify user account.",async()=>{ + const token = generateVerificationToken(dummySeller.email, 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) test("should update dummyseller's role to seller", async () => { const logDummySeller = await request(app).post("/api/v1/users/login").send({ email: dummySeller.email, password: dummySeller.password, }); - expect(logDummySeller.status).toBe(200); + // expect(logDummySeller.status).toBe(200); + expect(logDummySeller.body.message).toBe('Logged in') token = logDummySeller.body.token; const seller = await userService.getUserByEmail(dummySeller.email) @@ -149,6 +185,7 @@ describe("Testing product Routes", () => { }) .set("Authorization", "Bearer " + adminToken); expect(response.status).toBe(200); + expect(response.body.message).toBe('User role updated successfully'); }); @@ -477,7 +514,7 @@ test('It should return status 200 for removed category',async() =>{ }) it("changing product availability of product which does not exist", async ()=>{ const response = await request(app) - .patch(`/api/v1/products/${91}/status`) + .patch(`/api/v1/products/${4444444}/status`) .set("Authorization", "Bearer " + token); expect(response.body.message).toBe('Product not found') }) diff --git a/__test__/user.test.ts b/__test__/user.test.ts index 08f7bdd..a315e20 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -16,9 +16,14 @@ import { QueryTypes } from "sequelize"; // import redisClient from "../src/config/redis"; import Redis from "ioredis"; import { env } from "../src/utils/env"; -import { generateResetToken } from "../src/utils/generateResetToken"; +import { generateResetToken, generateVerificationToken } from "../src/utils/generateResetToken"; -let redisClient:any; +let redisClient: any; + +jest.mock("../src/services/mail.service", () => ({ + sendEmailService: jest.fn(), + sendNotification: jest.fn(), +})); const userData: any = { @@ -32,12 +37,13 @@ const userData: any = { const dummySeller = { name: "dummy1234", username: "username1234", - email: "soleilcyber00@gmail.com", + email: "srukundo01@gmail.com", password: "1234567890", lastPasswordUpdateTime: "3000, 11, 18" }; const userTestData = { newPassword: "Test@123", + isVerified:true, confirmPassword: "Test@123", wrongPassword: "Test456", }; @@ -109,6 +115,14 @@ describe("Testing user Routes", () => { expect(response.status).toBe(201); }, 20000); + it("It should verify user account.",async()=>{ + const token = generateVerificationToken(userData.email, 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) + test("should return 409 when registering with an existing email", async () => { User.create(userData); @@ -138,6 +152,7 @@ describe("Testing user Routes", () => { password: userData.password, }); expect(response.status).toBe(200); + expect(response.body.message).toBe("Logged in"); token = response.body.token; }); @@ -204,14 +219,28 @@ describe("Testing user Routes", () => { expect(response.body.status).toBe(401); spyonOne.mockRestore(); }, 20000); + it("It should verify user account.",async()=>{ + const token = generateVerificationToken('admin1@example.com', 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) test("should login an Admin", async () =>{ const response = await request(app).post("/api/v1/users/login").send({ email: "admin1@example.com", - password: "password" + password: "password" }) adminToken = response.body.token; }); + it("It should verify user account.",async()=>{ + const token = generateVerificationToken(dummySeller.email, 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) test("should update dummyseller's role to seller", async () => { const logDummySeller = await request(app).post("/api/v1/users/login").send({ @@ -219,6 +248,7 @@ describe("Testing user Routes", () => { password: dummySeller.password, }); expect(logDummySeller.status).toBe(200); + expect(logDummySeller.body.message).toBe("Logged in"); const seller = await userServices.getUserByEmail(dummySeller.email); const dummySellerId = seller?.id; @@ -230,18 +260,25 @@ describe("Testing user Routes", () => { .set("Authorization", "Bearer " + adminToken); expect(response.status).toBe(200); + // expect(response.body.message).toBe('User role updated successfully'); }); - test("Should send otp verification code", async () => { + test("Should send otp verification code", async () => { + jest.unmock("../src/services/mail.service"); + const originalMailService = jest.requireActual("../src/services/mail.service"); const spy = jest.spyOn(mailServices, "sendEmailService"); const response = await request(app).post("/api/v1/users/login").send({ email: dummySeller.email, password: dummySeller.password, }); - expect(response.body.message).toBe("OTP verification code has been sent ,please use it to verify that it was you"); - // expect(spy).toHaveBeenCalled(); + expect(response.body.message).toBe("OTP verification code has been sent ,please use it to verify that it was you"); + expect(spy).toHaveBeenCalled() + jest.mock("../src/services/mail.service", () => ({ + sendEmailService: jest.fn(), + sendNotification: jest.fn(), + })); }, 70000); test("should log a user in to retrieve a token", async () => { @@ -552,6 +589,28 @@ describe('Patch /api/v1/users/reset-password', () => { },60000); }); +describe("Verifying user account",()=>{ + it("It should verify user account.",async()=>{ + await User.create(userData) + const token = generateVerificationToken(userData.email, 60); + const response = await request(app) + .get(`/api/v1/users/verify-user?token=${token}`) + expect(response.status).toBe(200) + expect(response.body.message).toBe('User verified successfully.') + },60000) + + it("It should send a verification link.",async()=>{ + const response = await request(app) + .post('/api/v1/users/verify-user-email') + .send({ + email:userData.email + }) + expect(response.status).toBe(409) + expect(response.body.message).toBe("User is already verified.") + },60000) + +}) + afterAll(async () => { try { await sequelize.query('TRUNCATE TABLE profiles, users CASCADE'); diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index d81cb4d..5597377 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -106,7 +106,7 @@ export const createUserController = async (req: Request, res: Response) => { } return res.status(201).json({ status: 201, - message: "User successfully created." + message: "User successfully created. check your inbox to verify your email" }); } catch (err: any) { if (err.name === "UnauthorizedError" && err.message === "User already exists") { @@ -194,6 +194,7 @@ export const handleSuccess = async (req: Request, res: Response) => { name: user.displayName, email: user.emails[0].value, username: user.name.familyName, + isVerified:true, //@ts-ignore password: null }); @@ -405,3 +406,30 @@ export const resetPasswordController = async (req: Request, res: Response): Prom return res.status(500).json({ message: 'Internal server error.' }); } }; + +export const verifyUserEmailController = async (req: Request, res: Response) => { + try { + // Extract email from the request body + const {email} = req.body; + + const { error } = Emailschema.validate(req.body); + if (error) { + const cleanErrorMessage = error.details.map(detail => detail.message.replace(/['"]/g, '').trim()).join(', ') + return res.status(400).json({ message: cleanErrorMessage }); + } + const result = await userService.verifyUserEmail(email); + res.status(result.status).json({ message: result.message }); + } catch (error) { + res.status(500).json({ error: 'Failed to send verification email.' }); + } +}; +export const verifyUserController = async (req: Request, res: Response): Promise => { + try { + // Extract token from the request query + const token = req.query.token as string; + const result = await userService.verifyNewUser(token); + res.status(result.status).json({ message: result.message }); + } catch (error) { + res.status(500).json({ error: 'Failed to verify new user.' }); + } +}; \ No newline at end of file diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index b4ba080..81d734b 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -19,6 +19,7 @@ import { sendResetLink, updateForgotPassword, verifyUserAccessToken, + verifyUserEmail, } from "./users"; import { getProducts, @@ -39,7 +40,6 @@ import { getCategories, addCategories, getSingleCategory, updateCategories, dele deleteRole } from "./roledoc"; import { AddToWishes, deleteWish, getWishes, getWishesByProduct, wishSchema } from "./wishes"; -import { joinChats } from "./chats"; import { addItemToCartDoc, clearAllProductFromCartDoc, removeProductFromCartDoc, updateProductQuantityDoc, viewCartDoc } from "./cart"; import { getAllNotifications, readNotification } from "./notifications"; import { homepage } from "./home"; @@ -115,6 +115,9 @@ const options = { "/api/v1/users/reset-password": { patch: updateForgotPassword, }, + "/api/v1/users/verify-user-email": { + post: verifyUserEmail, + }, "/api/v1/users/me": { post: verifyUserAccessToken, diff --git a/src/docs/users.ts b/src/docs/users.ts index 199fc96..6db6fdf 100644 --- a/src/docs/users.ts +++ b/src/docs/users.ts @@ -481,3 +481,39 @@ export const updateForgotPassword = { } } }; +export const verifyUserEmail = { + tags: ["Users"], + summary: "Verify user email", + description: "Verify user email using the provided email address", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + email: { + type: "string", + description: "The user's email address" + } + }, + required: ["email"] + } + } + } + }, + responses: { + 200: { + description: "Verification email sent successfully" + }, + 400: { + description: "Invalid email address" + }, + 404: { + description: "User not found" + }, + 500: { + description: "Internal server error" + } + } +}; \ No newline at end of file diff --git a/src/email-templates/verifyUser.ts b/src/email-templates/verifyUser.ts new file mode 100644 index 0000000..40cc678 --- /dev/null +++ b/src/email-templates/verifyUser.ts @@ -0,0 +1,95 @@ +// emailTemplates/verifyUser.ts +const verifyUserEmailTemplate = (username:string,verificationLink:string) => { + return ` + + + + + + Verify Your Email + + + +
+
+ Company Logo +
+
+

Welcome, ${username}!

+

Thank you for registering with us. Please verify your email address to complete your registration.

+

Click the button below to verify your email address:

+ Verify Email +

If the button above does not work, copy and paste the following link into your browser:

+

${verificationLink}

+
+ +
+ + + `; + + +}; + +export default verifyUserEmailTemplate; + + +export const generateEmailVerificationEmail = (Username:string)=> { + return ` +Dear ${Username}, +
+ +Thank you for verifying your email address. You are now able to start using our system. +
+ +If you have any questions, feel free to reach out to our support team. +
+ +Best regards, +
+ATLP-eccommerce + `; +} \ No newline at end of file diff --git a/src/middlewares/isVerified.ts b/src/middlewares/isVerified.ts new file mode 100644 index 0000000..cab5ce5 --- /dev/null +++ b/src/middlewares/isVerified.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; +import User from '../sequelize/models/users'; + +export const isVerified = async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + const { email} = req.body; + const user = await User.findOne({ + where:{ + email:email + } + }); + if (user?.isVerified === false) { + return res.status(403).json({ message: 'Account is not verified' }); + } + next(); + }; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index c3e9f89..d78da1d 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController, otpVerification, changeUserAccountStatus, logout, sendResetLinkEmail, resetPasswordController } from "../controllers/userControllers"; +import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController, otpVerification,updateUserRole, changeUserAccountStatus, logout, sendResetLinkEmail, resetPasswordController, verifyUserEmailController, verifyUserController } from "../controllers/userControllers"; import { emailValidation, validateSchema } from "../middlewares/validator"; import { isLoggedIn } from "../middlewares/isLoggedIn"; import { passwordUpdateSchema } from "../schemas/passwordUpdate"; @@ -9,21 +9,21 @@ import logInSchema from "../schemas/loginSchema"; import { profileSchemas, signUpSchema } from "../schemas/signUpSchema"; import upload from "../middlewares/multer"; import isUploadedFileImage from "../middlewares/isImage"; -import bodyParser from "body-parser"; import { isAdmin } from "../middlewares/isAdmin"; import { roleUpdateSchema } from "../schemas/userRoleUpdateSchema"; -import { updateUserRole } from "../controllers/userControllers"; import { roleExist } from "../middlewares/roleExist"; import { userExist } from "../middlewares/userExist"; import { isDisabled } from "../middlewares/isDisabled"; import { verifyToken } from "../middlewares/verifyToken"; import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate"; +import { isVerified } from "../middlewares/isVerified"; const userRoutes = Router(); userRoutes.get("/", fetchAllUsers); -userRoutes.put("/passwordupdate", isLoggedIn,validateSchema(passwordUpdateSchema), updatePassword) -userRoutes.post("/login", emailValidation,validateSchema(logInSchema),isDisabled,userLogin); -userRoutes.post("/register", emailValidation,validateSchema(signUpSchema), createUserController); +userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword) +userRoutes.post("/login", emailValidation,validateSchema(logInSchema),isDisabled,isVerified,userLogin); +userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController); +userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword); userRoutes.get("/2fa-verify/:token",tokenVerification); userRoutes.post("/2fa-verify",otpVerification); userRoutes.get('/profile', @@ -47,6 +47,9 @@ userRoutes.get("/auth/google/success", handleSuccess); userRoutes.get("/auth/google/failure", handleFailure); userRoutes.post('/password-reset-link', sendResetLinkEmail); userRoutes.patch('/reset-password', resetPasswordController); +userRoutes.post('/verify-user-email', verifyUserEmailController); +userRoutes.get('/verify-user', verifyUserController); + userRoutes.get("/me", verifyToken); diff --git a/src/schemas/signUpSchema.ts b/src/schemas/signUpSchema.ts index 2a435b2..46a55e7 100644 --- a/src/schemas/signUpSchema.ts +++ b/src/schemas/signUpSchema.ts @@ -6,6 +6,7 @@ export const signUpSchema = Joi.object({ email: Joi.string().min(6).required().email(), password: Joi.string().min(6).max(20).required(), lastPasswordUpdateTime:Joi.date(), + isVerified:Joi.boolean().optional(), role: Joi.string().optional(), }).options({ allowUnknown: false }); diff --git a/src/sequelize/migrations/h20240519174339-add-is-verified.js b/src/sequelize/migrations/h20240519174339-add-is-verified.js new file mode 100644 index 0000000..34f3b32 --- /dev/null +++ b/src/sequelize/migrations/h20240519174339-add-is-verified.js @@ -0,0 +1,23 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + const isVerifiedColumnExists = await queryInterface.describeTable("users").then((columns) => { + return "isAvailable" in columns; + }); + + if (!isVerifiedColumnExists) { + await queryInterface.addColumn("users", "isVerified", { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }); + } + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn("users", "isVerified"); + }, +}; + diff --git a/src/sequelize/models/users.ts b/src/sequelize/models/users.ts index c467133..04278ec 100644 --- a/src/sequelize/models/users.ts +++ b/src/sequelize/models/users.ts @@ -15,6 +15,7 @@ export interface UserAttributes { lastPasswordUpdateTime?: Date; roleId: number | undefined; isActive?:boolean; + isVerified?:boolean; createdAt?: Date; updatedAt?: Date; } @@ -27,6 +28,7 @@ class User extends Model implements UserAttributes { password!: string; lastPasswordUpdateTime!:Date | undefined; isActive: boolean | undefined; + isVerified?: boolean | undefined; roleId!: number | undefined; createdAt!: Date | undefined; updatedAt!: Date | undefined; @@ -73,6 +75,11 @@ User.init( allowNull:false, defaultValue:true }, + isVerified:{ + type: DataTypes.BOOLEAN, + allowNull:false, + defaultValue:false + }, createdAt: { allowNull: false, type: DataTypes.DATE, diff --git a/src/sequelize/seeders/b20240412144111-demo-user.js b/src/sequelize/seeders/b20240412144111-demo-user.js index a15c350..5951f9f 100644 --- a/src/sequelize/seeders/b20240412144111-demo-user.js +++ b/src/sequelize/seeders/b20240412144111-demo-user.js @@ -31,6 +31,7 @@ module.exports = { password: await bcrypt.hash("soleil00", 10), lastPasswordUpdateTime:new Date(), roleId: 3, + isVerified:true, createdAt: new Date(), updatedAt: new Date(), }, @@ -41,6 +42,7 @@ module.exports = { password: await bcrypt.hash("soleil00", 10), lastPasswordUpdateTime:new Date(), roleId: 1, + isVerified:true, createdAt: new Date(), updatedAt: new Date(), }, @@ -51,6 +53,7 @@ module.exports = { password: await bcrypt.hash("Test@123", 10), lastPasswordUpdateTime:new Date(), roleId: 2, + isVerified:true, createdAt: new Date(), updatedAt: new Date(), }, @@ -61,6 +64,7 @@ module.exports = { password: await bcrypt.hash("Test@123", 10), lastPasswordUpdateTime:new Date(), roleId: 2, + isVerified:true, createdAt: new Date(), updatedAt: new Date(), }, diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 4ee6984..9ffb089 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -10,10 +10,13 @@ import userRoutes from "../routes/userRoutes"; import sequelize from "../config/dbConnection"; import { activationTemplate } from "../email-templates/activation"; import redisClient from "../config/redis"; -import { generateResetToken, verifyResetToken } from "../utils/generateResetToken"; +import { generateResetToken, generateVerificationToken, verifyResetToken } from "../utils/generateResetToken"; import { env } from "../utils/env"; import { generatePasswordResetEmail } from "../email-templates/generatePasswordResetEmail"; import { generatePasswordUpdateEmailContent } from "../email-templates/generatePasswordUpdateEmailContent"; +import verifyUserEmailTemplate, { generateEmailVerificationEmail } from "../email-templates/verifyUser"; +import { generateToken } from "../utils/jsonwebtoken"; +import { IUser } from "../types"; @@ -88,6 +91,13 @@ export const createUserService = async (name: string, email: string, username: s postalCode:"", country: "", }); + + const subject = 'Please verify your email address'; + const token = generateVerificationToken(user.email, 60); + const verificationLink = `${process.env.REMOTE_URL || `${process.env.LOCAL_URL}:${process.env.PORT}`}/api/v1/users/verify-user?token=${token}`; + + await sendEmailService(user,subject,verifyUserEmailTemplate(user.username,verificationLink)) + return user; } @@ -260,3 +270,48 @@ export const resetPassword = async (token: string, newPassword: string): Promise } }; +export const verifyNewUser = async (token: string) => { + + try { + const decodedToken = verifyResetToken(token); + const isBlacklisted = await isTokenBlacklisted(token); + if (!decodedToken || isBlacklisted) { + return { status: 400, message: 'Invalid token.' }; + } + const user:any = { + email:decodedToken.email + } + const subject = `Email Verification Confirmation`; + const [updatedRows] = await User.update({ isVerified: true }, { where: { email: decodedToken.email } }); + await addToBlacklist(token); + if (updatedRows > 0) { + await sendEmailService(user,subject,generateEmailVerificationEmail(decodedToken.email)) + return { status: 200, message: 'User verified successfully.' }; + } else { + return { status: 404, message: 'User not found or already verified.' }; + } + } catch (error) { + throw new Error('Failed to verify user.'); + } +}; +export const verifyUserEmail = async (email: string) => { + try { + const user = await User.findOne({ where: { email} }); + if(user?.isVerified === true){ + return { status: 409, message: 'User is already verified.' }; + + } + if (!user) { + return { status: 404, message: 'User not found.' }; + } + const subject = 'Please verify your email address'; + const token = generateVerificationToken(email, 60); + const verificationLink = `${process.env.REMOTE_URL || `${process.env.LOCAL_URL}:${process.env.PORT}`}/api/v1/users/verify-user?token=${token}`; + + await sendEmailService(user,subject,verifyUserEmailTemplate(user.username,verificationLink)) + + return { status: 201, message: 'Verification email sent successfully.' }; + } catch (error) { + throw new Error('Failed to verify user.'); + } +}; \ No newline at end of file diff --git a/src/utils/generateResetToken.ts b/src/utils/generateResetToken.ts index a207042..3f9df07 100644 --- a/src/utils/generateResetToken.ts +++ b/src/utils/generateResetToken.ts @@ -11,6 +11,16 @@ export const generateResetToken = (email: string, expiresInMinutes: number): str }, `${env.jwt_secret}`); return token; }; +export const generateVerificationToken = (email: string, expiresInMinutes: number): string => { + const createdAt = Math.floor(Date.now() / 1000); + const expiresAt = createdAt + expiresInMinutes * 60; + const token = sign({ + email, + createdAt, + expiresAt + }, `${env.jwt_secret}`); + return token; +}; export const verifyResetToken = (token: string): { email: string } | null => { try {