Skip to content

Commit 9923e8f

Browse files
authored
Merge pull request #66 from guanquann/forget-password
Forget password functionality
2 parents 50ebcfc + e702f45 commit 9923e8f

File tree

12 files changed

+507
-11
lines changed

12 files changed

+507
-11
lines changed

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

Lines changed: 101 additions & 5 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,
@@ -113,11 +119,14 @@ export const sendVerificationMail = async (
113119
}
114120

115121
const emailToken = crypto.randomBytes(16).toString("hex");
116-
await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes
117-
await sendAccVerificationMail(
122+
await redisClient.set(`email_verification:${email}`, emailToken, {
123+
EX: 60 * 5,
124+
}); // expire in 5 minutes
125+
await sendMail(
118126
email,
119127
ACCOUNT_VERIFICATION_SUBJ,
120128
user.username,
129+
ACCOUNT_VERIFICATION_TEMPLATE,
121130
emailToken
122131
);
123132

@@ -145,7 +154,7 @@ export const verifyUser = async (
145154
return res.status(404).json({ message: `User ${email} not found` });
146155
}
147156

148-
const expectedToken = await redisClient.get(email);
157+
const expectedToken = await redisClient.get(`email_verification:${email}`);
149158

150159
if (expectedToken !== token) {
151160
return res
@@ -332,6 +341,93 @@ export async function updateUser(
332341
}
333342
}
334343

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

backend/user-service/model/repository.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ export async function updateUserVerification(
115115
);
116116
}
117117

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+
118133
export async function deleteUserById(userId: string): Promise<IUser | null> {
119134
return UserModel.findByIdAndDelete(userId);
120135
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
updateUser,
1111
updateUserPrivilege,
1212
verifyUser,
13+
sendResetPasswordMail,
14+
resetPassword,
1315
} from "../controller/user-controller";
1416
import {
1517
verifyAccessToken,
@@ -34,6 +36,10 @@ router.post("/images", createImageLink);
3436

3537
router.post("/send-verification-email", sendVerificationMail);
3638

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

3945
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/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Landing from "./pages/Landing";
1010
import Home from "./pages/Home";
1111
import SignUp from "./pages/SignUp";
1212
import LogIn from "./pages/LogIn";
13+
import ForgetPassword from "./pages/ForgetPassword";
1314
import Matched from "./pages/Matched";
1415
import Timeout from "./pages/Timeout";
1516
import ProtectedRoutes from "./components/ProtectedRoutes";
@@ -72,6 +73,7 @@ function App() {
7273
path="verifyEmail/:userId?"
7374
element={<EmailVerification />}
7475
/>
76+
<Route path="forget-password" element={<ForgetPassword />} />
7577
<Route path="*" element={<Navigate to="/auth/login" />} />
7678
</Route>
7779
</Routes>

frontend/src/contexts/AuthContext.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => {
108108

109109
return (
110110
<AuthContext.Provider
111-
value={{ signup, login, logout, user, setUser, loading }}
111+
value={{
112+
signup,
113+
login,
114+
logout,
115+
user,
116+
setUser,
117+
loading,
118+
}}
112119
>
113120
{children}
114121
</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)