diff --git a/backend/package-lock.json b/backend/package-lock.json index b4ad97d..d1b6a2c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,7 @@ "mongoose": "^8.6.3", "multer": "^1.4.5-lts.1", "multer-storage-cloudinary": "^4.0.0", + "nodemailer": "^6.9.16", "winston": "^3.14.2" }, "devDependencies": { @@ -34,6 +35,7 @@ "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.7.2", + "@types/nodemailer": "^6.4.17", "@types/supertest": "^6.0.2", "jest": "^29.7.0", "nodemon": "^3.1.7", @@ -1763,6 +1765,16 @@ "undici-types": "~6.19.8" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", @@ -5515,6 +5527,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", diff --git a/backend/package.json b/backend/package.json index e6f3c4f..ffae96e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,6 +27,7 @@ "mongoose": "^8.6.3", "multer": "^1.4.5-lts.1", "multer-storage-cloudinary": "^4.0.0", + "nodemailer": "^6.9.16", "winston": "^3.14.2" }, "devDependencies": { @@ -40,6 +41,7 @@ "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.7.2", + "@types/nodemailer": "^6.4.17", "@types/supertest": "^6.0.2", "jest": "^29.7.0", "nodemon": "^3.1.7", diff --git a/backend/src/controllers/users/user.controller.ts b/backend/src/controllers/users/user.controller.ts index fd82a06..54c3784 100644 --- a/backend/src/controllers/users/user.controller.ts +++ b/backend/src/controllers/users/user.controller.ts @@ -6,7 +6,9 @@ import bookingRepo from '../../database/repositories/booking.repo'; import activityRepo from '../../database/repositories/activity.repo'; import itineraryRepo from '../../database/repositories/itinerary.repo'; import bcrypt from 'bcrypt'; +import emailService from '../../services/email/email.service'; import { accountType } from '../../types/User.types'; +import authService from '../../services/auth.service'; const getUsers = async (req: Request, res: Response) => { try { @@ -206,6 +208,51 @@ const rejectUser = async (req: Request, res: Response) => { } }; +const forgotPassword = async (req: Request, res: Response) => { + try { + const email = req.body.email; + const user = await userRepo.findUserByEmail(email); + if (!user) { + res.status(ResponseStatusCodes.NOT_FOUND).json({ message: 'User not found' }); + return; + } + + user.OTP = Math.floor(100000 + Math.random() * 900000).toString(); + await userRepo.updateUser(user._id, user); + + console.log(user); + + await emailService.sendForgotPasswordEmail(email, user.OTP); + + res.status(ResponseStatusCodes.OK).json({ message: 'OTP sent to email' }); + } catch (error: any) { + logger.error(`Error sending OTP: ${error.message}`); + res.status(ResponseStatusCodes.INTERNAL_SERVER_ERROR).json({ message: error.message }); + } +}; + +const verifyOTP = async (req: Request, res: Response) => { + try { + const email = req.body.email; + const OTP = req.body.OTP; + const user = await userRepo.findUserByEmail(email); + if (!user) { + res.status(ResponseStatusCodes.NOT_FOUND).json({ message: 'User not found' }); + return; + } + + if (user.OTP === OTP) { + const token = authService.generateAccessToken({ userId: user._id, accountType: user.account_type }); + res.status(ResponseStatusCodes.OK).json({ message: 'OTP verified', data: { token: token } }); + } else { + res.status(ResponseStatusCodes.UNAUTHORIZED).json({ message: 'Invalid OTP' }); + } + } catch (error: any) { + logger.error(`Error verifying OTP: ${error.message}`); + res.status(ResponseStatusCodes.INTERNAL_SERVER_ERROR).json({ message: error.message }); + } +}; + const getItinerary = async (req: Request, res: Response) => { try { const itineraries = await userRepo.getItinerary(req.user.userId); @@ -363,6 +410,8 @@ export { ChangeUserPassword, acceptTerms, rejectUser, + forgotPassword, + verifyOTP, getItinerary, getActivity, cancelActivityBooking, diff --git a/backend/src/database/models/user.model.ts b/backend/src/database/models/user.model.ts index fe17bea..b1ea5c3 100644 --- a/backend/src/database/models/user.model.ts +++ b/backend/src/database/models/user.model.ts @@ -54,6 +54,29 @@ const companyProfileSchema = new mongoose.Schema({ }, }); +const notificationSchema = new mongoose.Schema({ + title: { + type: String, + }, + message: { + type: String, + }, + notificationType: { + type: String, + enum: ['error', 'warning', 'information', 'success'], + default: 'information', + required: true, + }, + read: { + type: Boolean, + default: false, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + const userSchema = new mongoose.Schema( { account_type: { @@ -154,6 +177,12 @@ const userSchema = new mongoose.Schema( purchased_products: { type: [{ type: Schema.Types.ObjectId, ref: 'product' }], }, + notifications: { + type: [notificationSchema], + }, + OTP: { + type: String, + }, cart: { type: [cartItemSchema], default: [], diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 57e2b22..8de8713 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -48,9 +48,11 @@ const openPaths = [ { path: '/api/museums/getall', methods: ['GET'] }, { path: '/api/activities', methods: ['GET'] }, { path: '/api/attachments', methods: ['POST'] }, - { path: '/api/termsAndConditions', methods: ['GET'] }, // Add /terms to the openPaths list + { path: '/api/termsAndConditions', methods: ['GET'] }, { path: '/api/tags', methods: ['GET'] }, { path: '/api/categories', methods: ['GET'] }, + { path: '/api/users/forgotPassword', methods: ['POST'] }, + { path: '/api/users/verifyOTP', methods: ['POST'] }, ]; const authenticateUnlessOpen = (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/routes/user.route.ts b/backend/src/routes/user.route.ts index 1cc4d54..9a7bd3b 100644 --- a/backend/src/routes/user.route.ts +++ b/backend/src/routes/user.route.ts @@ -23,11 +23,17 @@ import { getPurchasedProducts, getHowManyUsers, getHowManyUsersByMonth, + forgotPassword, + verifyOTP, } from '../controllers/users/user.controller'; import cartController from '../controllers/cart.controller'; const router = Router(); +// Open routes +router.post('/forgotPassword', forgotPassword); +router.post('/verifyOTP', verifyOTP); + // User-specific routes router.use('/advertisers', advertiserRouter); router.use('/tourGuides', tourGuideRouter); diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index c6944ad..c18bb96 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; class AuthService { - public generateAccessToken(user: any) { + public generateAccessToken(user: { userId: string; accountType: string }) { return jwt.sign(user, process.env.TOKEN_SECRET as string, { expiresIn: '30d' }); } } diff --git a/backend/src/services/email/email-templates/forgotPassword.html b/backend/src/services/email/email-templates/forgotPassword.html new file mode 100644 index 0000000..aa07f22 --- /dev/null +++ b/backend/src/services/email/email-templates/forgotPassword.html @@ -0,0 +1,59 @@ + + +
+ + +{{message}}
+ +