Skip to content

Commit

Permalink
adding reset password feature
Browse files Browse the repository at this point in the history
  • Loading branch information
elijahladdie committed May 5, 2024
1 parent ac06e84 commit 8f69fbb
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 18 deletions.
105 changes: 100 additions & 5 deletions src/__test__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,118 @@ describe('POST /user/verify/:id', () => {
const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: newUser.email } });

if(user){
if (user) {
const verifyRes = await request(app).get(`/user/verify/${user.id}`);

// Assert
expect(verifyRes.status).toBe(200);
expect(verifyRes.text).toEqual('<p>User verified successfully</p>');

// Check that the user's verified field is now true
const verifiedUser = await userRepository.findOne({ where: { email: newUser.email } });
if (verifiedUser){
if (verifiedUser) {
expect(verifiedUser.verified).toBe(true);
}

}

if (user) {
await userRepository.remove(user);
}
});
});
});

describe('Send password reset link', () => {

it('Attempt to send email with rate limiting', async () => {
const email = "[email protected]";

const requests = Array.from({ length: 5 }, async () => {
return await request(app).post(`/user/password/reset/link?email=${email}`);
});

const responses = await Promise.all(requests);
const lastResponse = responses[responses.length - 1];
expect(lastResponse.status).toBe(500);
expect(lastResponse.body.message).toEqual('User not found');
});

it('Attempt to send email with invalid email template', async () => {
const email = "[email protected]";

const res = await request(app).post(`/user/password/reset/link?email=${email}`);

expect(res.status).toBe(500);
expect(res.body.message).toEqual('User not found');
});

it('Send email to a user with special characters in email address', async () => {
const email = "[email protected]";

const res = await request(app).post(`/user/password/reset/link?email=${encodeURIComponent(email)}`);

expect(res.status).toBe(500);
expect(res.body.message).toEqual('User not found');
});

});
describe('Password Reset Service', () => {
it('Should reset password successfully', async () => {
const data = {
"newPassword": "user",
"confirmPassword": "user",
};
const email = "[email protected]";
const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: email } });
if (user) {
const res: any = await request(app).post(`/user/password/reset?userid=${user.id}&email=${email}`).send(data);
// Assert
expect(res.status).toBe(200);
expect(res.data.message).toEqual('Password updated successful');
}
});

it('Should return 404 if user not found', async () => {
const data = {
"newPassword": "user",
"confirmPassword": "user",
};
const email = "[email protected]";
const userId = "nonexistentuserid";
const res: any = await request(app).post(`/user/password/reset?userid=${userId}&email=${email}`).send(data);
// Assert
expect(res.status).toBe(404);
});

it('Should return 204 if required fields are missing', async () => {
const data = {
//
};
const email = "[email protected]";

const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: email } });
if (user) {
const res: any = await request(app).post(`/user/password/reset?userid=${user.id}&email=${email}`).send(data);
expect(res.status).toBe(204);
expect(res.data.error).toEqual('Please provide all required fields');
}
});

it('Should return 204 if newPassword and confirmPassword do not match', async () => {
const data = {
"newPassword": "user123",
"confirmPassword": "user456",
};
const email = "[email protected]";

const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: email } });
if (user) {
const res: any = await request(app).post(`/user/password/reset?userid=${user.id}&email=${email}`).send(data);
expect(res.status).toBe(204);
expect(res.data.error).toEqual('New password must match confirm password');
}
});
});
13 changes: 7 additions & 6 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Request, Response } from 'express';
import { User } from '../entities/User';
import bcrypt from 'bcrypt';
import { getRepository } from 'typeorm';
import { responseError, responseServerError, responseSuccess } from '../utils/response.utils';
import { validate } from 'class-validator';
import { userVerificationService, userRegistrationService } from '../services';
import { userVerificationService, userRegistrationService ,sendPasswordResetLinkService, userPasswordResetService} from '../services';

export const userRegistration = async (req: Request, res: Response) => {
await userRegistrationService(req, res);
Expand All @@ -13,3 +8,9 @@ export const userVerification = async (req: Request, res: Response) => {
await userVerificationService(req, res);
}

export const userPasswordReset = async (req: Request, res: Response) => {
await userPasswordResetService(req, res);
}
export const sendPasswordResetLink = async (req: Request, res: Response) => {
await sendPasswordResetLinkService(req, res);
}
3 changes: 1 addition & 2 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import { userRegistration,userVerification } from './authController';

export { userRegistration,userVerification };
export * from './authController';
2 changes: 1 addition & 1 deletion src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
// export all middlewares
export * from "./errorHandler"
7 changes: 6 additions & 1 deletion src/routes/UserRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Router } from 'express';
import { userRegistration, userVerification} from '../controllers/index';
import { sendPasswordResetLink, userPasswordReset , userRegistration, userVerification} from '../controllers';




const router = Router();

router.post('/register', userRegistration);
router.get('/verify/:id', userVerification);
router.post("/password/reset", userPasswordReset);
router.post("/password/reset/link", sendPasswordResetLink);

export default router;
8 changes: 5 additions & 3 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// export all Services
export * from './userServices/userRegistrationService';
export * from './userServices/userValidationService';

export * from "./userServices/sendResetPasswordLinkService";
export * from "./userServices/userPasswordResetService";
export * from "./userServices/userRegistrationService";
export * from "./userServices/userValidationService";
120 changes: 120 additions & 0 deletions src/services/userServices/sendResetPasswordLinkService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Request, Response } from "express";
import { responseError, responseServerError, responseSuccess } from "../../utils/response.utils";
import nodemailer from 'nodemailer';
import { getRepository } from "typeorm";
import { User } from "../../entities/User";

export const sendPasswordResetLinkService = async (req: Request, res: Response) => {
try {
const transporter = nodemailer.createTransport({
host: process.env.HOST,
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.AUTH_EMAIL,
pass: process.env.AUTH_PASSWORD
},
});
const email = req.query.email as string;

if (!email) {
return responseError(res, 500, 'Missing required field');
}
const userRepository = getRepository(User);
const existingUser = await userRepository.findOneBy({ email });
if (!existingUser) {
return responseError(res, 500, 'User not found', existingUser);
}
const mailOptions: nodemailer.SendMailOptions = {
to: email,
subject: `Password reset link `,
html: `
<!doctype html>
<html lang="en-US">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>Reset Password Email Template</title>
<meta name="description" content="Reset Password Email Template.">
<style type="text/css">
a:hover {text-decoration: underline !important;}
</style>
</head>
<body marginheight="0" topmargin="0" marginwidth="0" style="margin: 0px; background-color: #f2f3f8;" leftmargin="0">
<!--100% body table-->
<table cellspacing="0" border="0" cellpadding="0" width="100%" bgcolor="#f2f3f8"
style="@import url(https://fonts.googleapis.com/css?family=Rubik:300,400,500,700|Open+Sans:300,400,600,700); font-family: 'Open Sans', sans-serif;">
<tr>
<td>
<table style="background-color: #f2f3f8; max-width:670px; margin:0 auto;" width="100%" border="0"
align="center" cellpadding="0" cellspacing="0">
<tr>
<td style="height:80px;">&nbsp;</td>
</tr>
<tr>
<td style="height:20px;">&nbsp;</td>
</tr>
<tr>
<td>
<table width="95%" border="0" align="center" cellpadding="0" cellspacing="0"
style="max-width:670px;background:#fff; border-radius:3px; text-align:center;-webkit-box-shadow:0 6px 18px 0 rgba(0,0,0,.06);-moz-box-shadow:0 6px 18px 0 rgba(0,0,0,.06);box-shadow:0 6px 18px 0 rgba(0,0,0,.06);">
<tr>
<td style="height:40px;">&nbsp;</td>
</tr>
<tr>
<td style="padding:0 35px;">
<h1 style="color:#1e1e2d; font-weight:500; margin:0;font-size:32px;font-family:'Rubik',sans-serif;text-decoration:none; ">You have
requested to reset your password</h1>
<span
style="display:inline-block; vertical-align:middle; margin:29px 0 26px; border-bottom:1px solid #cecece; width:100px;"></span>
<p style="color:#455056; font-size:15px;line-height:24px; margin:0;">
We cannot simply send you your old password. A unique link to reset your
password has been generated for you. To reset your password, click the
following link and follow the instructions.
</p>
<a href="${process.env.FRONTEND_URL}/${process.env.PASSWORD_ROUTE}?userid=${existingUser.id}&email=${existingUser.email}" target="_blank"
style="background:#20e277;text-decoration:none !important; font-weight:500; margin-top:35px; color:#fff;text-transform:uppercase; font-size:14px;padding:10px 24px;display:inline-block;border-radius:50px;">Reset
Password</a>
</td>
</tr>
<tr>
<td style="height:40px;">&nbsp;</td>
</tr>
</table>
</td>
<tr>
<td style="height:20px;">&nbsp;</td>
</tr>
<tr>
<td style="text-align:center;">
<p style="font-size:14px; color:rgba(69, 80, 86, 0.7411764705882353); line-height:18px; margin:0 0 0;">&copy; <strong>Knights Ecommerce</strong></p>
</td>
</tr>
<tr>
<td style="height:80px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--/100% body table-->
</body>
</html>`
};

try {
const sendMail = await transporter.sendMail(mailOptions);
return responseSuccess(res, 200, "Code sent on your email", sendMail);
} catch (error) {;
return responseError(res, 500, 'Error occurred while sending email');
}


} catch (error) {
return responseServerError(res, `Internal server error: `);
}
}
;
35 changes: 35 additions & 0 deletions src/services/userServices/userPasswordResetService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import bcrypt from 'bcrypt';
import { Request, Response } from "express";
import { responseError, responseServerError, responseSuccess } from "../../utils/response.utils";
import { getRepository } from "typeorm";
import { User } from "../../entities/User";

export const userPasswordResetService = async (req: Request, res: Response) => {
try {
const { email, userid } = req.params;
const { newPassword, confirmPassword } = req.body;

const userRepository = getRepository(User);

const existingUser = await userRepository.findOneBy({ email, id: userid });
if (!existingUser) {
return responseError(res, 404, 'Something went wrong in finding your data');
}

if (!newPassword || !confirmPassword) {
return responseError(res, 204, 'Please provide all required fields');
}
if (newPassword !== confirmPassword) {
return responseError(res, 204, 'new password must match confirm password');
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);

existingUser.password = hashedPassword;
const updadeUser = await userRepository.save(existingUser);
return responseSuccess(res, 200, "Password updated successful", updadeUser);
} catch (error) {
return responseServerError(res, "Internal server error");
}
}

0 comments on commit 8f69fbb

Please sign in to comment.