Skip to content

Commit 1b11523

Browse files
committed
Add reset password
1 parent 642e1d5 commit 1b11523

File tree

16 files changed

+274
-49
lines changed

16 files changed

+274
-49
lines changed

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";

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function handleLogout(req, res, next) {
4343
}
4444
const refreshToken = req.cookies[REFRESH_TOKEN_COOKIE_KEY];
4545
const decodedToken = await TokenService.verifyToken(refreshToken, jwtConfig.refreshTokenSecret);
46-
await TokenService.blacklistToken(decodedToken);
46+
await TokenService.blacklistRefreshToken(decodedToken);
4747
return res.sendStatus(204);
4848
} catch (err) {
4949
next(err);
@@ -82,7 +82,7 @@ export async function refresh(req, res, next) {
8282
const accessToken = TokenService.generateAccessToken(dbUser);
8383
const newRefreshToken = TokenService.generateRefreshToken(dbUser);
8484
res.cookie(REFRESH_TOKEN_COOKIE_KEY, newRefreshToken, refreshTokenCookieOptions);
85-
await TokenService.blacklistToken(decodedRefreshToken);
85+
await TokenService.blacklistRefreshToken(decodedRefreshToken);
8686

8787
return res.status(200).json({
8888
message: "Access token refreshed",
@@ -92,11 +92,3 @@ export async function refresh(req, res, next) {
9292
next(err);
9393
}
9494
}
95-
96-
export async function sendResetPasswordLinkToEmail(req, res, next) {
97-
try {
98-
99-
} catch (err) {
100-
next(err);
101-
}
102-
}

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

Lines changed: 39 additions & 9 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,12 +11,13 @@ 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";
1818
import { sendEmail } from "../services/emailService.js";
19-
import redisService from "../services/redisService.js";
2019

20+
const PASSWORD_SALT = 10;
2121
export async function createUser(req, res, next) {
2222
try {
2323
const { username, email, password } = req.body;
@@ -30,7 +30,7 @@ export async function createUser(req, res, next) {
3030
throw new ConflictError("Username or email already exists");
3131
}
3232

33-
const salt = bcrypt.genSaltSync(10);
33+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
3434
const hashedPassword = bcrypt.hashSync(password, salt);
3535
const createdUser = await _createUser(username, email, hashedPassword);
3636

@@ -102,7 +102,7 @@ export async function updateUser(req, res, next) {
102102

103103
let hashedPassword;
104104
if (password) {
105-
const salt = bcrypt.genSaltSync(10);
105+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
106106
hashedPassword = bcrypt.hashSync(password, salt);
107107
}
108108
const updatedUser = await _updateUserById(userId, username, email, hashedPassword);
@@ -166,10 +166,8 @@ export async function deleteUser(req, res, next) {
166166
export async function forgetPassword(req, res, next) {
167167
try {
168168
const { email } = req.body;
169-
const emailToken = TokenService.generateEmailToken(email);
170-
redisService.setKeyWithExpiration(email, emailToken, 300);
171-
172-
const resetPasswordLink = `http://localhost:3000/reset-password/${emailToken}`;
169+
const resetToken = await TokenService.generateResetToken(email);
170+
const resetPasswordLink = `http://localhost:3000/reset-password?token=${resetToken}`;
173171
await sendEmail({
174172
to: email,
175173
subject: "Reset password",
@@ -182,6 +180,38 @@ export async function forgetPassword(req, res, next) {
182180
}
183181
}
184182

183+
export async function resetPassword(req, res, next) {
184+
try {
185+
const { password, token } = req.body;
186+
if (password) {
187+
const decoded = await TokenService.verifyResetToken(token, jwtConfig.resetTokenSecret);
188+
189+
if (await TokenService.isResetTokenBlacklisted(decoded)) {
190+
throw new UnauthorisedError("Reset token is invalid");
191+
}
192+
193+
const email = decoded.email;
194+
const user = await findUserByEmail(email);
195+
if (!user) {
196+
throw new NotFoundError(`No user with the email ${email} is found`);
197+
}
198+
const salt = bcrypt.genSaltSync(PASSWORD_SALT);
199+
const hashedPassword = bcrypt.hashSync(password, salt);
200+
const updatedUser = await _updateUserById(user.id, user.username, user.email, hashedPassword);
201+
202+
await TokenService.blacklistResetToken(decoded);
203+
204+
return res.status(200).json({
205+
message: "Password has been resetted",
206+
data: formatUserResponse(updatedUser),
207+
});
208+
}
209+
} catch (err) {
210+
console.error(err);
211+
next(err);
212+
}
213+
}
214+
185215
export function formatUserResponse(user) {
186216
return {
187217
_id: user.id,

backend/user-service/routes/user-routes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
forgetPassword,
66
getAllUsers,
77
getUser,
8+
resetPassword,
89
updateUser,
910
updateUserPrivilege,
1011
} from "../controller/user-controller.js";
@@ -183,4 +184,6 @@ router.delete("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, deleteUser);
183184

184185
router.post("/forgetPassword", forgetPassword);
185186

187+
router.post("/resetPassword", resetPassword);
188+
186189
export default router;

backend/user-service/services/redisService.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ class RedisService {
2222
await this.redisClient.set(key, value, { EX: expiration });
2323
console.log(`Set ${key}: ${value}`);
2424
} catch (error) {
25-
console.error("Error setting key in Redis:", error);
25+
console.error(`Error setting key "${key}"`, error);
26+
}
27+
}
28+
29+
async get(key) {
30+
try {
31+
return await this.redisClient.get(key);
32+
} catch (error) {
33+
console.error(`Error retrieving value associated with the key "${key}": `, error);
2634
}
2735
}
2836

@@ -31,7 +39,7 @@ class RedisService {
3139
const count = await this.redisClient.exists(key);
3240
return count === 1;
3341
} catch (error) {
34-
console.error("Error checking key existence in Redis:", error);
42+
console.error(`Error checking key existence for the key "${key}":`, error);
3543
return false;
3644
}
3745
}

backend/user-service/services/tokenService.js

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import jwt from "jsonwebtoken";
1+
import jwt, { decode } from "jsonwebtoken";
22
import { jwtConfig } from "../config/authConfig.js";
33
import { v4 as uuidv4 } from "uuid";
44
import redisService from "./redisService.js";
@@ -14,11 +14,6 @@ class TokenService {
1414
return accessToken;
1515
}
1616

17-
static generateEmailToken(email) {
18-
const emailToken = jwt.sign({ email, jti: uuidv4() }, jwtConfig.accessTokenSecret, { expiresIn: "5m" }); // TODO: change
19-
return emailToken;
20-
}
21-
2217
static generateRefreshToken(user) {
2318
const refreshToken = jwt.sign(
2419
{ id: user.id, jti: uuidv4() },
@@ -28,6 +23,13 @@ class TokenService {
2823
return refreshToken;
2924
}
3025

26+
static async generateResetToken(email) {
27+
const jti = uuidv4();
28+
const resetToken = jwt.sign({ email, jti }, jwtConfig.resetTokenSecret, jwtConfig.resetTokenOptions);
29+
await redisService.setKeyWithExpiration(this.generateResetTokenKey(email), jti, 900);
30+
return resetToken;
31+
}
32+
3133
static async verifyToken(token, secret) {
3234
return new Promise((resolve, reject) => {
3335
jwt.verify(token, secret, (err, decoded) => {
@@ -39,26 +41,60 @@ class TokenService {
3941
});
4042
}
4143

42-
static async blacklistToken(decodedRefreshToken) {
44+
static async verifyResetToken(token) {
45+
const decoded = await this.verifyToken(token, jwtConfig.resetTokenSecret);
46+
const { email, jti } = decoded;
47+
const expectedResetToken = await redisService.get(this.generateResetTokenKey(email));
48+
if (expectedResetToken !== jti) {
49+
throw new UnauthorisedError("Reset token is invalid or has expired");
50+
}
51+
return decoded;
52+
}
53+
54+
static async blacklistRefreshToken(decodedRefreshToken) {
4355
const { id, jti, exp } = decodedRefreshToken;
56+
const blacklistTokenKey = this.generateBlacklistedRefreshTokenKey(id, jti);
57+
this.blacklistToken(exp, blacklistTokenKey);
58+
}
59+
60+
static async blacklistResetToken(decodedResetToken) {
61+
const { email, jti, exp } = decodedResetToken;
62+
const blacklistTokenKey = this.generateBlacklistedResetTokenKey(email, jti);
63+
this.blacklistToken(exp, blacklistTokenKey);
64+
}
65+
66+
static async blacklistToken(exp, key) {
4467
const currentTime = Math.floor(Date.now() / 1000);
4568
const remainingTime = exp - currentTime;
46-
const blacklistTokenKey = this.generateBlacklistedTokenKey(id, jti);
4769
if (remainingTime > 0) {
48-
await redisService.setKeyWithExpiration(blacklistTokenKey, "blacklisted", remainingTime);
49-
console.log(`Blacklisted token: ${jti}`);
70+
await redisService.setKeyWithExpiration(key, "blacklisted", remainingTime);
71+
console.log(`Blacklisted token: ${key}`);
5072
}
5173
}
5274

5375
static async isRefreshTokenBlacklisted(decodedRefreshToken) {
5476
const { id, jti } = decodedRefreshToken;
55-
const blacklistTokenKey = this.generateBlacklistedTokenKey(id, jti);
77+
const blacklistTokenKey = this.generateBlacklistedRefreshTokenKey(id, jti);
78+
return await redisService.exists(blacklistTokenKey);
79+
}
80+
81+
static async isResetTokenBlacklisted(decodedResetToken) {
82+
const { email, jti } = decodedResetToken;
83+
const blacklistTokenKey = this.generateBlacklistedResetTokenKey(email, jti);
5684
return await redisService.exists(blacklistTokenKey);
5785
}
5886

59-
static generateBlacklistedTokenKey(userId, jti) {
87+
static generateBlacklistedRefreshTokenKey(userId, jti) {
6088
return `${userId}:${jti}`;
6189
}
90+
91+
static generateBlacklistedResetTokenKey(email, jti) {
92+
return `${email}:${jti}`;
93+
}
94+
95+
static generateResetTokenKey(email) {
96+
return `resetToken:${email}`;
97+
}
6298
}
6399

64100
export default TokenService;

frontend/src/data/users/UserImpl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export class UserImpl implements IUser {
3737
async forgetPassword(email: string): Promise<any> {
3838
return this.dataSource.forgetPassword(email);
3939
}
40+
41+
async resetPassword(password: string, token: string) {
42+
return this.dataSource.resetPassword(password, token);
43+
}
4044
}
4145

4246
export const userImpl = new UserImpl();

frontend/src/data/users/UserRemoteDataSource.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export class UserRemoteDataSource extends BaseApi {
5454
async forgetPassword(email: string) {
5555
return await this.post<any>("/users/forgetPassword", { email });
5656
}
57+
58+
async resetPassword(password: string, token: string) {
59+
return await this.post<any>("/users/resetPassword", { password, token });
60+
}
5761
}
5862

5963
export const userRemoteDataSource = new UserRemoteDataSource();

frontend/src/data/users/mockUser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ export class MockUser {
156156
});
157157
}
158158

159-
async forgetPassword(email: string): Promise<any> {
160-
161-
}
159+
async forgetPassword(email: string): Promise<any> {}
160+
161+
async resetPassword(password: string, token: string): Promise<any> {}
162162
}
163163

164164
export const mockUser = new MockUser();

frontend/src/domain/usecases/UserUseCases.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export class UserUseCases {
117117
async forgetPassword(email: string): Promise<void> {
118118
await this.user.forgetPassword(email);
119119
}
120+
121+
async resetPassword(password: string, token: string): Promise<void> {
122+
await this.user.resetPassword(password, token);
123+
}
120124
}
121125

122126
export const userUseCases = new UserUseCases(userImpl);

0 commit comments

Comments
 (0)