Skip to content

Commit fa3e33d

Browse files
committed
Merge branch 'development' into BE/stopmatch
2 parents 12dd1eb + 9923e8f commit fa3e33d

File tree

19 files changed

+591
-61
lines changed

19 files changed

+591
-61
lines changed

backend/user-service/.env.sample

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ USER=EMAIL_ADDRESS
2929
PASS=PASSWORD
3030

3131
# Redis configuration
32-
REDIS_URI=REDIS_URI
32+
REDIS_URI=REDIS_URI
33+
34+
# Test
35+
MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/
36+
REDIS_URI_TEST=redis://test-redis:6379

backend/user-service/config/redis.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import dotenv from "dotenv";
33

44
dotenv.config();
55

6-
const REDIS_URI = process.env.REDIS_URI || "redis://localhost:6379";
6+
const REDIS_URI =
7+
process.env.NODE_ENV === "test"
8+
? process.env.REDIS_URI_TEST
9+
: process.env.REDIS_URI || "redis://localhost:6379";
710

811
const client = createClient({ url: REDIS_URI });
912

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

Lines changed: 103 additions & 9 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
@@ -155,12 +164,10 @@ export const verifyUser = async (
155164

156165
const updatedUser = await _updateUserVerification(email);
157166
if (!updatedUser) {
158-
return res.status(404).json({ message: `User ${email} not verified.` });
167+
return res.status(404).json({ message: `User not verified.` });
159168
}
160169

161-
return res
162-
.status(200)
163-
.json({ message: `User ${email} verified successfully.` });
170+
return res.status(200).json({ message: `User verified successfully.` });
164171
} catch (error) {
165172
return res
166173
.status(500)
@@ -334,6 +341,93 @@ 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+
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+
337431
export async function updateUserPrivilege(
338432
req: Request,
339433
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: 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/scripts/seed.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export async function seedAdminAccount() {
3838
adminUsername,
3939
adminEmail,
4040
hashedPassword,
41+
true,
4142
true
4243
);
4344
console.log("Admin account created successfully.");

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/tests/authRoutes.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import supertest from "supertest";
44
import app from "../app";
55
import UserModel from "../model/user-model";
66

7+
jest.setTimeout(10000);
8+
79
const request = supertest(app);
810

911
const AUTH_BASE_URL = "/api/auth";
@@ -25,6 +27,7 @@ const insertAdminUser = async () => {
2527
email,
2628
password: hashedPassword,
2729
isAdmin: true,
30+
isVerified: true,
2831
}).save();
2932

3033
return { email, password };
@@ -37,6 +40,7 @@ const insertNonAdminUser = async () => {
3740
lastName,
3841
email,
3942
password: hashedPassword,
43+
isVerified: true,
4044
}).save();
4145

4246
return { email, password };

backend/user-service/tests/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import redisClient from "../config/redis";
33

44
beforeAll(async () => {
55
const mongoUri =
6-
process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@mongo:27017/";
6+
process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@test-mongo:27017/";
77

88
await mongoose.connect(mongoUri, {});
99
await redisClient.connect();

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+
`;

0 commit comments

Comments
 (0)