Skip to content

Commit dddd7a1

Browse files
committed
Merge branch 'development' into feat/users
2 parents 5900652 + a2f9baa commit dddd7a1

File tree

28 files changed

+725
-117
lines changed

28 files changed

+725
-117
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,33 @@ on:
77

88
env:
99
NODE_VERSION: 20
10-
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
11-
FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }}
12-
FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }}
13-
FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
14-
JWT_SECRET: ${{ secrets.JWT_SECRET }}
1510

1611
permissions:
1712
contents: read
1813

1914
jobs:
20-
ci:
15+
frontend-ci:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
- name: Setting node version
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: ${{ env.NODE_VERSION }}
24+
- name: Install dependencies
25+
working-directory: frontend
26+
run: npm install
27+
- name: Linting
28+
working-directory: frontend
29+
run: npm run lint
30+
- name: Frontend tests
31+
run: docker compose -f docker-compose-test.yml run --rm test-frontend
32+
backend-ci:
2133
runs-on: ubuntu-latest
2234
strategy:
2335
matrix:
24-
service: [frontend, backend/question-service, backend/user-service]
36+
service: [question-service, user-service]
2537
steps:
2638
- name: Checkout code
2739
uses: actions/checkout@v4
@@ -30,21 +42,16 @@ jobs:
3042
with:
3143
node-version: ${{ env.NODE_VERSION }}
3244
- name: Install dependencies
33-
working-directory: ${{ matrix.service }}
45+
working-directory: backend/${{ matrix.service }}
3446
run: npm install
3547
- name: Linting
36-
working-directory: ${{ matrix.service }}
48+
working-directory: backend/${{ matrix.service }}
3749
run: npm run lint
38-
- name: Set .env variables
39-
working-directory: ${{ matrix.service }}
40-
run: |
41-
touch .env
42-
echo "FIREBASE_PROJECT_ID=${{ env.FIREBASE_PROJECT_ID }}" >> .env
43-
echo "FIREBASE_PRIVATE_KEY=${{ env.FIREBASE_PRIVATE_KEY }}" >> .env
44-
echo "FIREBASE_CLIENT_EMAIL=${{ env.FIREBASE_CLIENT_EMAIL }}" >> .env
45-
echo "FIREBASE_CLIENT_EMAIL=${{ env.FIREBASE_CLIENT_EMAIL }}" >> .env
46-
echo "FIREBASE_STORAGE_BUCKET=${{ env.FIREBASE_STORAGE_BUCKET }}" >> .env
47-
echo "JWT_SECRET=${{ env.JWT_SECRET }}" >> .env
48-
- name: Tests
49-
working-directory: ${{ matrix.service }}
50-
run: npm test
50+
- name: Backend tests
51+
env:
52+
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
53+
FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }}
54+
FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }}
55+
FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
56+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
57+
run: docker compose -f docker-compose-test.yml run --rm test-${{ matrix.service }}

backend/question-service/.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ FIREBASE_STORAGE_BUCKET=>FIREBASE_STORAGE_BUCKET>
1212
ORIGINS=http://localhost:5173,http://127.0.0.1:5173
1313

1414
USER_SERVICE_URL=http://user-service:3001/api
15+
16+
MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/

backend/question-service/tests/setup.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import mongoose from "mongoose";
2-
import { MongoMemoryServer } from "mongodb-memory-server";
3-
4-
let mongo: MongoMemoryServer;
52

63
beforeAll(async () => {
7-
mongo = await MongoMemoryServer.create();
8-
const mongoUri = mongo.getUri();
4+
const mongoUri =
5+
process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@test-mongo:27017/";
96

107
if (mongoose.connection.readyState !== 0) {
118
await mongoose.disconnect();
@@ -24,9 +21,5 @@ afterEach(async () => {
2421
});
2522

2623
afterAll(async () => {
27-
if (mongo) {
28-
await mongo.stop();
29-
}
30-
3124
await mongoose.connection.close();
3225
});

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

0 commit comments

Comments
 (0)