Skip to content

Commit 66f8ceb

Browse files
authored
Merge pull request #84 from njxue/forgot-password
Forgot password with email and reset token
2 parents 333c98c + 90ae765 commit 66f8ceb

25 files changed

+581
-28
lines changed

backend/user-service/.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ REDIS_PORT=6379
3535
PEERPREP_QUESTION_INITDB_NAME=peerprepQuestionServiceDB # must match question service .env file and init-mongo.js
3636
PEERPREP_USER_INITDB_NAME=peerprepUserServiceDB # must match user service .env file and init-mongo.js
3737
PEERPREP_MATCHING_INITDB_NAME=peerprepMatchingServiceDB # must match user service .env file and init-mongo.js
38+
39+
# SMTP configs for email
40+
SMTP_HOST=smtp.gmail.com
41+
SMTP_PORT=587
42+
43+
SMTP_PASSWORD=scdqcveqpurzzajj
44+
45+
# App URL
46+
APP_URL=http://localhost:3000

backend/user-service/config/authConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ export const jwtConfig = {
99
accessTokenOptions: {
1010
expiresIn: process.env.ENV === "production" ? "15m" : "30s", // Shorter duration in dev for testing
1111
},
12+
resetTokenOptions: {
13+
expiresIn: "15m",
14+
},
1215
accessTokenSecret: process.env.JWT_ACCESS_TOKEN_SECRET,
1316
refreshTokenSecret: process.env.JWT_REFRESH_TOKEN_SECRET,
17+
resetTokenSecret: process.env.JWT_RESET_TOKEN_SECRET,
1418
};
1519

1620
export const REFRESH_TOKEN_COOKIE_KEY = "refreshToken";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import dotenv from "dotenv";
2+
3+
dotenv.config();
4+
5+
export const emailConfig = {
6+
user: process.env.SMTP_USER,
7+
password: process.env.SMTP_PASSWORD,
8+
smtp_host: process.env.SMTP_HOST,
9+
smtp_port: Number(process.env.SMTP_PORT),
10+
};

backend/user-service/controller/auth-controller.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { formatUserResponse } from "./user-controller.js";
44
import { jwtConfig, REFRESH_TOKEN_COOKIE_KEY, refreshTokenCookieOptions } from "../config/authConfig.js";
55
import TokenService from "../services/tokenService.js";
66
import { BadRequestError, NotFoundError, UnauthorisedError } from "../utils/httpErrors.js";
7-
import { decode } from "jsonwebtoken";
87

98
export async function handleLogin(req, res, next) {
109
const { email, password } = req.body;
@@ -44,7 +43,7 @@ export async function handleLogout(req, res, next) {
4443
}
4544
const refreshToken = req.cookies[REFRESH_TOKEN_COOKIE_KEY];
4645
const decodedToken = await TokenService.verifyToken(refreshToken, jwtConfig.refreshTokenSecret);
47-
await TokenService.blacklistToken(decodedToken);
46+
await TokenService.blacklistRefreshToken(decodedToken);
4847
return res.sendStatus(204);
4948
} catch (err) {
5049
next(err);
@@ -83,7 +82,7 @@ export async function refresh(req, res, next) {
8382
const accessToken = TokenService.generateAccessToken(dbUser);
8483
const newRefreshToken = TokenService.generateRefreshToken(dbUser);
8584
res.cookie(REFRESH_TOKEN_COOKIE_KEY, newRefreshToken, refreshTokenCookieOptions);
86-
await TokenService.blacklistToken(decodedRefreshToken);
85+
await TokenService.blacklistRefreshToken(decodedRefreshToken);
8786

8887
return res.status(200).json({
8988
message: "Access token refreshed",

backend/user-service/controller/user-controller.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import bcrypt from "bcrypt";
2-
import jwt from "jsonwebtoken";
32
import { jwtConfig, REFRESH_TOKEN_COOKIE_KEY, refreshTokenCookieOptions } from "../config/authConfig.js";
43
import { isValidObjectId } from "mongoose";
54
import {
@@ -12,10 +11,15 @@ import {
1211
findUserByUsernameOrEmail as _findUserByUsernameOrEmail,
1312
updateUserById as _updateUserById,
1413
updateUserPrivilegeById as _updateUserPrivilegeById,
14+
findUserByEmail,
1515
} from "../model/repository.js";
16-
import { BadRequestError, ConflictError, NotFoundError } from "../utils/httpErrors.js";
16+
import { BadRequestError, ConflictError, NotFoundError, UnauthorisedError } from "../utils/httpErrors.js";
1717
import TokenService from "../services/tokenService.js";
18+
import { sendEmail } from "../services/emailService.js";
19+
import dotenv from "dotenv";
1820

21+
dotenv.config();
22+
const PASSWORD_SALT = 10;
1923
export async function createUser(req, res, next) {
2024
try {
2125
const { username, email, password } = req.body;
@@ -28,7 +32,7 @@ export async function createUser(req, res, next) {
2832
throw new ConflictError("Username or email already exists");
2933
}
3034

31-
const salt = bcrypt.genSaltSync(10);
35+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
3236
const hashedPassword = bcrypt.hashSync(password, salt);
3337
const createdUser = await _createUser(username, email, hashedPassword);
3438

@@ -42,12 +46,11 @@ export async function createUser(req, res, next) {
4246
data: { accessToken, user: { ...formatUserResponse(createdUser) } },
4347
});
4448
} catch (err) {
45-
console.error('Error creating user:', err);
49+
console.error("Error creating user:", err);
4650
next(err);
4751
}
4852
}
4953

50-
5154
export async function getUser(req, res, next) {
5255
try {
5356
const userId = req.params.id;
@@ -101,7 +104,7 @@ export async function updateUser(req, res, next) {
101104

102105
let hashedPassword;
103106
if (password) {
104-
const salt = bcrypt.genSaltSync(10);
107+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
105108
hashedPassword = bcrypt.hashSync(password, salt);
106109
}
107110
const updatedUser = await _updateUserById(userId, username, email, hashedPassword);
@@ -162,6 +165,56 @@ export async function deleteUser(req, res, next) {
162165
}
163166
}
164167

168+
export async function forgetPassword(req, res, next) {
169+
try {
170+
const { email } = req.body;
171+
const resetToken = await TokenService.generateResetToken(email);
172+
const passwordResetLink = `${process.env.APP_URL}/reset-password?token=${resetToken}`;
173+
174+
await sendEmail({
175+
to: email,
176+
subject: "Reset password",
177+
htmlTemplateData: { passwordResetLink },
178+
});
179+
180+
res.sendStatus(204);
181+
} catch (err) {
182+
console.error(err);
183+
next(err);
184+
}
185+
}
186+
187+
export async function resetPassword(req, res, next) {
188+
try {
189+
const { password, token } = req.body;
190+
if (password) {
191+
const decoded = await TokenService.verifyResetToken(token, jwtConfig.resetTokenSecret);
192+
193+
if (await TokenService.isResetTokenBlacklisted(decoded)) {
194+
throw new UnauthorisedError("Reset token is invalid");
195+
}
196+
197+
const email = decoded.email;
198+
const user = await findUserByEmail(email);
199+
if (!user) {
200+
throw new NotFoundError(`No user with the email ${email} is found`);
201+
}
202+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
203+
const hashedPassword = bcrypt.hashSync(password, salt);
204+
const updatedUser = await _updateUserById(user.id, user.username, user.email, hashedPassword);
205+
await TokenService.blacklistResetToken(decoded);
206+
207+
return res.status(200).json({
208+
message: "Password has been resetted",
209+
data: formatUserResponse(updatedUser),
210+
});
211+
}
212+
} catch (err) {
213+
console.error(err);
214+
next(err);
215+
}
216+
}
217+
165218
export function formatUserResponse(user) {
166219
return {
167220
_id: user.id,

backend/user-service/package-lock.json

Lines changed: 137 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/user-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
"cookie-parser": "^1.4.6",
2121
"cors": "^2.8.5",
2222
"dotenv": "^16.4.5",
23+
"ejs": "^3.1.10",
2324
"express": "^4.19.2",
2425
"jsonwebtoken": "^9.0.2",
2526
"mongoose": "^8.5.4",
27+
"nodemailer": "^6.9.16",
2628
"redis": "^4.7.0",
2729
"swagger-jsdoc": "^6.2.8",
2830
"swagger-ui-express": "^5.0.1",

0 commit comments

Comments
 (0)