Skip to content

Commit

Permalink
feat(Reset-password):User who forgot password can reset it via email
Browse files Browse the repository at this point in the history
- User who forgot password can request resetting it
- Sending reset-password email containing link along with token to reset password
- Reset password using the provided token
- Token is used only once

[Delivers #187419058]
  • Loading branch information
YvetteNyibuka committed Apr 23, 2024
1 parent abbc596 commit 3bba3d8
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 55 deletions.
5 changes: 4 additions & 1 deletion .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ GOOGLE_SECRET_ID=
GOOGLE_CALLBACK_URL=
SESSION_SECRET=


JWT_SECRET=
SENDER_NAME=
SENDER_EMAIL=
SENDER_PASSWORD=
ACCESS_TOKEN_SECRET=
19 changes: 18 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 24 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
"author": "",
"license": "ISC",
"dependencies": {
"@types/jsonwebtoken": "^9.0.6",
"@types/nodemailer": "^6.4.14",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"joi": "^17.12.3",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.13",
"passport": "^0.5.0",
"joi": "^17.12.3",
"passport-local": "^1.0.0",
"passport-stub": "^1.1.1",
"pg": "^8.11.5",
Expand All @@ -55,37 +56,36 @@
"/src/database/config/db.config.ts"
]
},

"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@eslint/js": "^9.0.0",
"@types/eslint": "^8.56.9",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.7",
"@types/passport": "^1.0.16",
"@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5",
"passport-stub": "^1.1.1",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"devDependencies": {
"@eslint/js": "^9.0.0",
"@types/bcrypt": "^5.0.2",
"@types/eslint": "^8.56.9",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.7",
"@types/passport": "^1.0.16",
"@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"husky": "^9.0.11",
"jest": "^29.7.0",
"lint-staged": "^15.2.2",
"passport-stub": "^1.1.1",
"prettier": "3.2.5",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"@types/supertest": "^6.0.2",
"dotenv": "^16.4.5",
"jest": "^29.7.0",
"sequelize-cli": "^6.6.2",
"supertest": "^6.3.4",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
Expand Down
99 changes: 99 additions & 0 deletions src/controllers/resetPasswort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Request, Response, NextFunction } from 'express';
import { User } from '../database/models/User';
import { generatePasswordResetToken } from '../helpers/security.helpers';
import { ACCESS_TOKEN_SECRET, SENDER_EMAIL, SENDER_NAME } from '../utils/keys';
import Jwt from 'jsonwebtoken';
import { senderEmail } from '../helpers/nodemailer';
import { hashPassword } from '../utils/password';

const usedTokens: { [token: string]: boolean } = {};

export const forgotPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email } = req.body;

const isUserExist:any = await User.findOne({ where: { email: email } });

if (!isUserExist) {
return res.status(404).json({
message: 'User not found',
});
}

const resetToken = generatePasswordResetToken({ id: isUserExist.id as any, email: isUserExist.email });
usedTokens[resetToken] = false;

// console.log("reset token====================", resetToken);

res.setHeader('Authorization', resetToken);
const port = process.env.PORT || 3000;

const confirmlink: any = `http://localhost:${port}/passwordReset?token=${resetToken}`;
const mailOptions = {
from: `${SENDER_EMAIL}`,
to: email,
subject: 'Reset Password',
html: `
<p>Click <a href="${confirmlink}">here</a> to reset your password</p>
`,
};
res.status(200).json({message: "email sent successfully", resetToken})
senderEmail(mailOptions);


} catch (error) {
console.error(error);
res.status(500).json({ message: 'An error occurred while processing your request.' });
}
};


export const resetPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
const { password } = req.body;
const { token } = req.params;

if (!token) {
return res.status(400).json({
message: 'Token is required',
});
}

if (!ACCESS_TOKEN_SECRET) {
throw new Error("ACCESS_TOKEN_SECRET is not defined");
}

const decoded: any = Jwt.verify(token, ACCESS_TOKEN_SECRET);

if (!decoded || !decoded.id) {
return res.status(400).json({
message: 'Invalid token',
});
}

const resetingUser = await User.findOne({ where: { id: decoded.id } });

if (!resetingUser) {
return res.status(404).json({
message: 'User not found',
});
}

// Check if token has been used
if (usedTokens[token]) {
return res.status(400).json({
message: 'Token has already been used',
});
}
const hashedPassword: string = await hashPassword(password) as string;
usedTokens[token] = true;
await resetingUser.update({ password: hashedPassword });

res.status(200).json({ message: 'Password reset successfully' });
} catch (error) {
console.error(error);
res.status(500).json({ message: 'An error occurred while processing your request.' });
}
};

54 changes: 26 additions & 28 deletions src/database/config/db.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,50 @@ import { Sequelize } from "sequelize";
config();

let db_uri: string = "";
const APP_MODE: string = (process.env.DEV_MODE as string) || "development";
const DB_HOST_MODE: string = process.env.DB_HOSTED_MODE as string;
const APP_MODE: string = process.env.DEV_MODE || "development";
const DB_HOST_MODE: string = process.env.DB_HOSTED_MODE || "local";

let dialect_option: any;

switch (APP_MODE) {
case "test":
db_uri = process.env.DB_TEST_URL as string;
break;

case "production":
db_uri = process.env.DB_PROD_URL as string;
break;
default:
db_uri = process.env.DB_DEV_URL as string;
break;
case "test":
db_uri = process.env.DB_TEST_URL || "";
break;
case "production":
db_uri = process.env.DB_PROD_URL || "";
break;
default:
db_uri = process.env.DB_DEV_URL || "";
break;
}

DB_HOST_MODE === "local"
? (dialect_option = {})
: (dialect_option = {
ssl: {
require: process.env.SSL,
rejectUnauthorized: true,
},
});
const isLocal = DB_HOST_MODE === "local";
dialect_option = isLocal ? {} : {
ssl: {
require: true, // Adjust based on your needs
rejectUnauthorized: true,
},
};

export const sequelizeConnection: Sequelize = new Sequelize(db_uri, {
dialect: "postgres",
dialectOptions: dialect_option,
logging: false,
pool: {
dialect: "postgres",
dialectOptions: dialect_option,
logging: process.env.NODE_ENV === "development" ? console.log : false,
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000,
},
},
});

export const connectionToDatabase = async () => {
try {
try {
await sequelizeConnection.authenticate();
await sequelizeConnection.sync();
console.log("Database connected successfully.", db_uri);
} catch (error) {
} catch (error) {
console.log("Unable to connect to the database:", error);
process.exit(1);
}
}
}
37 changes: 37 additions & 0 deletions src/helpers/nodemailer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import nodemailer from 'nodemailer';
import { SENDER_EMAIL, SENDER_NAME, SENDER_PASSWORD } from '../utils/keys';

interface MailOptions {
to: string;
subject: string;
html: any;
}

const sender = nodemailer.createTransport({
service: "gmail",
secure: true,
port:5432,
auth: {
user: `${SENDER_EMAIL}`,
pass: `${SENDER_PASSWORD}`,
},
tls: {
rejectUnauthorized: false,
},
});

// SEND EMAIL FUNCTION
export function senderEmail({ to, subject, html }: MailOptions) {
const mailOptions = {
from: `"${SENDER_NAME}" <${SENDER_EMAIL}>`,
to,
subject,
html,
};

sender.sendMail(mailOptions, (error) => {
if (error) {
console.log("EMAILING USER FAILED:", error);
}
});
}
10 changes: 10 additions & 0 deletions src/helpers/security.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ interface TokenData {
id: string | number;
role: string;
}
interface resetTokenData {
id: string | number;
email: string;
}

export const generateAccessToken = (userData: TokenData) => {
const token = jwt.sign(userData, ACCESS_TOKEN_SECRET as string, {
expiresIn: "1d",
});
return token;
};
export const generatePasswordResetToken = (userData1: resetTokenData) => {
const token = jwt.sign(userData1, ACCESS_TOKEN_SECRET as string, {
expiresIn: "1d",
});
return token;
};

export const verifyAccessToken = (token: string, res: Response) => {
const tokenValidation = validateToken(token, ACCESS_TOKEN_SECRET as string);
Expand Down
5 changes: 4 additions & 1 deletion src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express from "express";
import userController from "../controllers/userController";
import userMiddleware from "../middlewares/user.middleware";
import { forgotPassword} from "../controllers/resetPasswort";
import { resetPassword } from "../controllers/resetPasswort";

const userRoutes = express.Router();
userRoutes.post(
Expand All @@ -10,6 +12,7 @@ userRoutes.post(
);

userRoutes.post("/login", userMiddleware.logInValidated, userController.login);

userRoutes.post("/forgot-password", forgotPassword);
userRoutes.post("/reset-password/:token", resetPassword);

export default userRoutes;
Loading

0 comments on commit 3bba3d8

Please sign in to comment.