Skip to content

Commit 43d20ea

Browse files
committed
Update forget password functionality
1 parent db94bc9 commit 43d20ea

File tree

10 files changed

+390
-33
lines changed

10 files changed

+390
-33
lines changed

backend/user-service/controller/user-controller.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
updateUserById as _updateUserById,
1313
updateUserPrivilegeById as _updateUserPrivilegeById,
1414
updateUserVerification as _updateUserVerification,
15+
updateUserPassword as _updateUserPassword,
1516
} from "../model/repository";
1617
import {
1718
validateEmail,
@@ -25,8 +26,13 @@ import { upload } from "../config/multer";
2526
import { uploadFileToFirebase } from "../utils/utils";
2627
import redisClient from "../config/redis";
2728
import crypto from "crypto";
28-
import { sendAccVerificationMail } from "../utils/mailer";
29-
import { ACCOUNT_VERIFICATION_SUBJ } from "../utils/constants";
29+
import { sendMail } from "../utils/mailer";
30+
import {
31+
ACCOUNT_VERIFICATION_SUBJ,
32+
ACCOUNT_VERIFICATION_TEMPLATE,
33+
RESET_PASSWORD_SUBJ,
34+
RESET_PASSWORD_TEMPLATE,
35+
} from "../utils/constants";
3036

3137
export async function createUser(
3238
req: Request,
@@ -114,10 +120,11 @@ export const sendVerificationMail = async (
114120

115121
const emailToken = crypto.randomBytes(16).toString("hex");
116122
await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes
117-
await sendAccVerificationMail(
123+
await sendMail(
118124
email,
119125
ACCOUNT_VERIFICATION_SUBJ,
120126
user.username,
127+
ACCOUNT_VERIFICATION_TEMPLATE,
121128
emailToken
122129
);
123130

@@ -334,6 +341,87 @@ export async function updateUser(
334341
}
335342
}
336343

344+
export const sendResetPasswordMail = async (
345+
req: Request,
346+
res: Response
347+
): Promise<Response> => {
348+
try {
349+
const { email } = req.body;
350+
const user = await _findUserByEmail(email);
351+
352+
if (!user) {
353+
return res.status(404).json({ message: `User not found` });
354+
}
355+
356+
const emailToken = crypto.randomBytes(16).toString("hex");
357+
await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes
358+
await sendMail(
359+
email,
360+
RESET_PASSWORD_SUBJ,
361+
user.username,
362+
RESET_PASSWORD_TEMPLATE,
363+
emailToken
364+
);
365+
366+
return res.status(200).json({
367+
message: "Reset password email sent. Please check your inbox.",
368+
data: { email, id: user.id },
369+
});
370+
} catch (error) {
371+
return res.status(500).json({
372+
message: "Unknown error when sending reset password email!",
373+
error,
374+
});
375+
}
376+
};
377+
378+
export const resetPassword = async (
379+
req: Request,
380+
res: Response
381+
): Promise<Response> => {
382+
try {
383+
const { email, token, password } = req.body;
384+
385+
const user = await _findUserByEmail(email);
386+
if (!user) {
387+
return res.status(404).json({ message: `User not found` });
388+
}
389+
390+
const expectedToken = await redisClient.get(email);
391+
392+
if (expectedToken !== token) {
393+
return res
394+
.status(400)
395+
.json({ message: "Invalid token. Please request for a new one." });
396+
}
397+
398+
const { isValid: isValidPassword, message: passwordMessage } =
399+
validatePassword(password);
400+
if (!isValidPassword) {
401+
return res.status(400).json({ message: passwordMessage });
402+
}
403+
404+
const salt = bcrypt.genSaltSync(10);
405+
const hashedPassword = bcrypt.hashSync(password, salt);
406+
407+
const updatedUser = await _updateUserPassword(email, hashedPassword);
408+
409+
if (!updatedUser) {
410+
return res
411+
.status(404)
412+
.json({ message: `User's password not reset.` });
413+
}
414+
415+
return res
416+
.status(200)
417+
.json({ message: `User's password successfully reset.` });
418+
} catch (error) {
419+
return res
420+
.status(500)
421+
.json({ message: "Unknown error when resetting user password!", error });
422+
}
423+
};
424+
337425
export async function updateUserPrivilege(
338426
req: Request,
339427
res: Response

backend/user-service/model/repository.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function createUser(
3131
email,
3232
password,
3333
isAdmin,
34+
isVerified,
3435
});
3536
return user.save();
3637
}
@@ -114,6 +115,21 @@ export async function updateUserVerification(
114115
);
115116
}
116117

118+
export async function updateUserPassword(
119+
email: string,
120+
password: string
121+
): Promise<IUser | null> {
122+
return UserModel.findOneAndUpdate(
123+
{ email },
124+
{
125+
$set: {
126+
password,
127+
},
128+
},
129+
{ new: true } // return the updated user
130+
);
131+
}
132+
117133
export async function deleteUserById(userId: string): Promise<IUser | null> {
118134
return UserModel.findByIdAndDelete(userId);
119135
}

backend/user-service/routes/user-routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import {
1010
updateUser,
1111
updateUserPrivilege,
1212
verifyUser,
13+
sendResetPasswordMail,
14+
resetPassword,
1315
} from "../controller/user-controller";
1416
import {
1517
verifyAccessToken,
1618
verifyIsAdmin,
1719
verifyIsOwnerOrAdmin,
1820
} from "../middleware/basic-access-control";
21+
import { send } from "process";
1922

2023
const router = express.Router();
2124

@@ -34,6 +37,10 @@ router.post("/images", createImageLink);
3437

3538
router.post("/send-verification-email", sendVerificationMail);
3639

40+
router.post("/send-reset-password-email", sendResetPasswordMail);
41+
42+
router.post("/reset-password", resetPassword);
43+
3744
router.get("/:id", getUser);
3845

3946
router.get("/verify-email/:email/:token", verifyUser);

backend/user-service/swagger.yml

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,22 @@ components:
7171
password:
7272
type: string
7373
required: true
74-
EmailVerification:
74+
Email:
7575
properties:
7676
email:
7777
type: string
7878
required: true
79+
ResetPassword:
80+
properties:
81+
email:
82+
type: string
83+
required: true
84+
token:
85+
type: string
86+
required: true
87+
password:
88+
type: string
89+
required: true
7990
UserResponse:
8091
properties:
8192
message:
@@ -347,7 +358,73 @@ paths:
347358
content:
348359
application/json:
349360
schema:
350-
$ref: "#/components/schemas/EmailVerification"
361+
$ref: "#/components/schemas/Email"
362+
responses:
363+
200:
364+
description: Successful Response
365+
content:
366+
application/json:
367+
schema:
368+
type: object
369+
properties:
370+
message:
371+
type: string
372+
description: Message
373+
404:
374+
description: Not Found
375+
content:
376+
application/json:
377+
schema:
378+
$ref: "#/components/schemas/ErrorResponse"
379+
500:
380+
description: Internal Server Error
381+
content:
382+
application/json:
383+
schema:
384+
$ref: "#/components/schemas/ServerErrorResponse"
385+
/api/users/send-reset-password-email:
386+
post:
387+
summary: Send reset password email
388+
tags:
389+
- users
390+
requestBody:
391+
content:
392+
application/json:
393+
schema:
394+
$ref: "#/components/schemas/Email"
395+
responses:
396+
200:
397+
description: Successful Response
398+
content:
399+
application/json:
400+
schema:
401+
type: object
402+
properties:
403+
message:
404+
type: string
405+
description: Message
406+
404:
407+
description: Not Found
408+
content:
409+
application/json:
410+
schema:
411+
$ref: "#/components/schemas/ErrorResponse"
412+
500:
413+
description: Internal Server Error
414+
content:
415+
application/json:
416+
schema:
417+
$ref: "#/components/schemas/ServerErrorResponse"
418+
/api/users/reset-password:
419+
post:
420+
summary: Reset password
421+
tags:
422+
- users
423+
requestBody:
424+
content:
425+
application/json:
426+
schema:
427+
$ref: "#/components/schemas/ResetPassword"
351428
responses:
352429
200:
353430
description: Successful Response

backend/user-service/utils/constants.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,23 @@ export const ACCOUNT_VERIFICATION_TEMPLATE = `
1717
</body>
1818
</html>
1919
`;
20+
21+
export const RESET_PASSWORD_SUBJ = "Password Reset Link";
22+
23+
export const RESET_PASSWORD_TEMPLATE = `
24+
<html>
25+
<head>
26+
<title>Password Reset</title>
27+
<meta charset="utf-8" />
28+
</head>
29+
<body>
30+
<p>Hello {{username}}!</p>
31+
<p>
32+
You have requested to reset your password. Please use this token: <strong>{{token}}</strong> to reset your password.
33+
</p>
34+
<p>If you did not request for a password reset, please ignore this email.</p>
35+
<p>Regards,</p>
36+
<p>Peerprep G28</p>
37+
</body>
38+
</html>
39+
`;

backend/user-service/utils/mailer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import nodemailer from "nodemailer";
22
import dotenv from "dotenv";
33
import Handlebars from "handlebars";
4-
import { ACCOUNT_VERIFICATION_TEMPLATE } from "./constants";
54

65
dotenv.config();
76

@@ -14,13 +13,14 @@ const transporter = nodemailer.createTransport({
1413
auth: { user: USER, pass: PASS },
1514
});
1615

17-
export const sendAccVerificationMail = async (
16+
export const sendMail = async (
1817
to: string,
1918
subject: string,
2019
username: string,
20+
htmlTemplate: string,
2121
token: string
2222
) => {
23-
const template = Handlebars.compile(ACCOUNT_VERIFICATION_TEMPLATE);
23+
const template = Handlebars.compile(htmlTemplate);
2424
const replacement = { username, token };
2525
const html = template(replacement);
2626
const options = {

frontend/src/contexts/AuthContext.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ type AuthContextType = {
1818
) => void;
1919
login: (email: string, password: string) => void;
2020
logout: () => void;
21-
resetPassword: (email: string) => void;
2221
user: User | null;
2322
setUser: React.Dispatch<React.SetStateAction<User | null>>;
2423
loading: boolean;
@@ -102,19 +101,20 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => {
102101
toast.success(SUCCESS_LOG_OUT);
103102
};
104103

105-
const resetPassword = (email: string) => {
106-
userClient.post("/users/reset-password", { email }).then(() => {
107-
toast.success("Reset link sent to your email");
108-
});
109-
};
110-
111104
if (loading) {
112105
return <Loader />;
113106
}
114107

115108
return (
116109
<AuthContext.Provider
117-
value={{ signup, login, logout, user, setUser, resetPassword, loading }}
110+
value={{
111+
signup,
112+
login,
113+
logout,
114+
user,
115+
setUser,
116+
loading,
117+
}}
118118
>
119119
{children}
120120
</AuthContext.Provider>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.fullheight {
2+
display: flex;
3+
}
4+
5+
.center {
6+
justify-content: center;
7+
align-items: center;
8+
}

0 commit comments

Comments
 (0)