Skip to content

Commit

Permalink
Merge pull request #74 from CS3219-AY2324S1/reset-password
Browse files Browse the repository at this point in the history
[backend] feat: add reset password
  • Loading branch information
sltsheryl authored Sep 30, 2023
2 parents bedd4dc + e5037e3 commit cf51e8f
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 0 deletions.
19 changes: 19 additions & 0 deletions backend/user-service/package-lock.json

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

2 changes: 2 additions & 0 deletions backend/user-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@types/express": "^4.17.17",
"@types/express-validator": "^3.0.0",
"@types/morgan": "^1.9.5",
"@types/nodemailer": "^6.4.11",
"nodemon": "^3.0.1",
"prisma": "^5.3.1",
"ts-node": "^10.9.1",
Expand All @@ -36,6 +37,7 @@
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"nodemailer": "^6.9.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
Expand Down
94 changes: 94 additions & 0 deletions backend/user-service/src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
generateRefreshToken,
authenticateAccessToken,
authenticateRefreshToken,
generateTemporaryToken,
verifyTemporaryToken,
} from "../utils/jwt";
import transporter from "../utils/nodemailer";

interface LogInData {
username: string;
Expand Down Expand Up @@ -231,6 +234,97 @@ export async function updateBothTokens(req: Request, res: Response) {
}
}

export const sendResetEmail: RequestHandler[] = [
body("email").notEmpty().isEmail(),
async (req, res) => {
if (!validationResult(req).isEmpty()) {
res.status(400).json({ errors: validationResult(req).array() });
return;
}

const { email } = matchedData(req);

try {
const user = await prisma.user.findUnique({ where: { email } });

if (!user) {
res.status(404).json({ errors: [{ msg: "User not found." }] });
return;
}

const resetToken = generateTemporaryToken(email);
const mailOptions = {
from: "[email protected]",
to: email,
subject: "Password Reset",
text: `Click the link below to reset your password: http://localhost:8000/reset-password?token=${resetToken}`,
};

transporter.sendMail(mailOptions, (error: Error | null, info: any) => {
if (error) {
console.error(error);
res.status(500).json({ errors: [{ msg: "Internal Server Error" }] });
} else {
console.log("Email sent: " + info.response);
res
.status(200)
.json({ message: "Password reset link sent successfully." });
}
});
} catch (error) {
console.error(error);
res.status(500).json({ errors: [{ msg: "Internal Server Error" }] });
}
},
];

export const resetPassword: RequestHandler[] = [
body("password")
.notEmpty()
.isLength({ min: 8 })
.withMessage("Password should have length of at least 8."),
async (req, res) => {
if (!validationResult(req).isEmpty()) {
res.status(400).json({ errors: validationResult(req).array() });
return;
}

const { password } = matchedData(req);
const { token } = req.query;

try {
const isValidToken = await verifyTemporaryToken(token as string);

if (!isValidToken) {
const errorMessage = "Invalid token: Unable to verify token.";
console.error(errorMessage);
res.status(400).json({ errors: [{ msg: errorMessage }] });
return;
}

const user = await prisma.user.findFirst({
where: { email: isValidToken.email },
});

if (!user) {
res.status(400).json({ errors: [{ msg: "User not found." }] });
return;
}

const hashedPassword = hashPassword(password);
await prisma.user.update({
where: { id: user.id },
data: { password: hashedPassword },
});

res.status(200).json({ message: "Password reset successful." });
} catch (error) {
console.error(error);
res.status(500).json({ errors: [{ msg: "Internal Server Error" }] });
}
},
];

// This verify refresh token function also generates new access token after verification
export async function updateAccessToken(req: Request, res: Response) {
const refreshToken = req.cookies["refreshToken"]; // accessToken is stored in a cookie
Expand Down
2 changes: 2 additions & 0 deletions backend/user-service/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ router.get(
AuthMiddleWare.verifyAccessToken,
AuthController.getCurrentUser,
);
router.post("/send-reset-email", AuthController.sendResetEmail);
router.post("/reset-password", AuthController.resetPassword);

export default router;
17 changes: 17 additions & 0 deletions backend/user-service/src/utils/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";

const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET as string;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET as string;
const TEMPORARY_TOKEN_SECRET = process.env.TEMPORARY_TOKEN_SECRET as string;

export async function generateAccessToken(userWithoutPassword: object) {
return jwt.sign({ user: userWithoutPassword }, ACCESS_TOKEN_SECRET, {
Expand Down Expand Up @@ -48,3 +49,19 @@ export async function authenticateRefreshToken(
);
});
}

// is using email or the entire object better
export function generateTemporaryToken(email: string): string {
return jwt.sign({ email }, TEMPORARY_TOKEN_SECRET, { expiresIn: "1h" });
}

export function verifyTemporaryToken(token: string): { email: string } | null {
try {
const decoded = jwt.verify(token, TEMPORARY_TOKEN_SECRET) as {
email: string;
};
return decoded;
} catch (error) {
return null;
}
}
12 changes: 12 additions & 0 deletions backend/user-service/src/utils/nodemailer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
service: "gmail",
host: "smtp.gmail.com",
secure: false,
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_PASSWORD,
},
});
export default transporter;

0 comments on commit cf51e8f

Please sign in to comment.