Skip to content

Commit 12dd1eb

Browse files
committed
Merge branch 'development' into BE/stopmatch
2 parents 918eb10 + c3462f8 commit 12dd1eb

32 files changed

+1210
-162
lines changed

backend/user-service/.env.sample

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@ MONGO_LOCAL_URI=<MONGO_LOCAL_URI>
77
# Secret for creating JWT signature
88
JWT_SECRET=<JWT_SECRET>
99

10-
# admin default credentials
10+
# Admin default credentials
1111
ADMIN_FIRST_NAME=Admin
1212
ADMIN_LAST_NAME=User
1313
ADMIN_USERNAME=administrator
1414
ADMIN_EMAIL=[email protected]
1515
ADMIN_PASSWORD=Admin@123
1616

17-
# firebase
17+
# Firebase
1818
FIREBASE_PROJECT_ID=FIREBASE_PROJECT_ID
1919
FIREBASE_PRIVATE_KEY=FIREBASE_PRIVATE_KEY
2020
FIREBASE_CLIENT_EMAIL=FIREBASE_CLIENT_EMAIL
2121
FIREBASE_STORAGE_BUCKET=FIREBASE_STORAGE_BUCKET
2222

23-
# origins for cors
24-
ORIGINS=http://localhost:5173,http://127.0.0.1:5173
23+
# Origins for cors
24+
ORIGINS=http://localhost:5173,http://127.0.0.1:5173
25+
26+
# Mail service
27+
SERVICE=gmail
28+
USER=EMAIL_ADDRESS
29+
PASS=PASSWORD
30+
31+
# Redis configuration
32+
REDIS_URI=REDIS_URI

backend/user-service/README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,40 @@
88

99
2. To connect to your cloud MongoDB instead of your local MongoDB, set the `NODE_ENV` to `production` instead of `development`.
1010

11-
3. Update `MONGO_CLOUD_URI`, `MONGO_LOCAL_URI`, `FIREBASE_PROJECT_ID`, `FIREBASE_PRIVATE_KEY`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_STORAGE_BUCKET`, `JWT_SECRET`.
11+
3. Update the following variables in the `.env` file:
12+
13+
- `MONGO_CLOUD_URI`
14+
15+
- `MONGO_LOCAL_URI`
16+
17+
- `FIREBASE_PROJECT_ID`
18+
19+
- `FIREBASE_PRIVATE_KEY`
20+
21+
- `FIREBASE_CLIENT_EMAIL`
22+
23+
- `FIREBASE_STORAGE_BUCKET`
24+
25+
- `JWT_SECRET`
26+
27+
- `SERVICE`: Email service to use to send account verification links, e.g. `gmail`.
28+
29+
- `USER`: Email address that you will be using, e.g. `[email protected]`.
30+
31+
- `PASS`: The app password. For gmail accounts, please refer to this [link](https://support.google.com/accounts/answer/185833?hl=en).
32+
33+
- `REDIS_URI`
1234

1335
4. A default admin account (`email: [email protected]` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account.
1436

37+
5. To view the contents stored in Redis,
38+
39+
1. Go to [http://localhost:5540](http://localhost:5540).
40+
41+
2. Click on "Add Redis Database".
42+
43+
3. Enter `host.internal.docker` as the Host.
44+
1545
## Running User Service without Docker
1646

1747
1. Open Command Line/Terminal and navigate into the `user-service` directory.

backend/user-service/config/firebase.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ admin.initializeApp({
1111

1212
const bucket = admin.storage().bucket();
1313

14-
export { bucket };
14+
const auth = admin.auth();
15+
16+
export { bucket, auth };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createClient } from "redis";
2+
import dotenv from "dotenv";
3+
4+
dotenv.config();
5+
6+
const REDIS_URI = process.env.REDIS_URI || "redis://localhost:6379";
7+
8+
const client = createClient({ url: REDIS_URI });
9+
10+
export const connectRedis = async () => {
11+
await client.connect();
12+
client.on("error", (err) => console.log(`Error: ${err}`));
13+
};
14+
15+
// client.on("error", (err) => console.log(`Error: ${err}`));
16+
17+
// (async () => await client.connect())();
18+
19+
export default client;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export async function handleLogin(
1717
return res.status(401).json({ message: "Wrong email and/or password" });
1818
}
1919

20+
if (!user.isVerified) {
21+
return res.status(401).json({
22+
message: "User not verified.",
23+
});
24+
}
25+
2026
const match = await bcrypt.compare(password, user.password);
2127
if (!match) {
2228
return res.status(401).json({ message: "Wrong email and/or password" });

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

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
findUserByUsernameOrEmail as _findUserByUsernameOrEmail,
1212
updateUserById as _updateUserById,
1313
updateUserPrivilegeById as _updateUserPrivilegeById,
14+
updateUserVerification as _updateUserVerification,
1415
} from "../model/repository";
1516
import {
1617
validateEmail,
@@ -22,6 +23,10 @@ import {
2223
import { IUser } from "../model/user-model";
2324
import { upload } from "../config/multer";
2425
import { uploadFileToFirebase } from "../utils/utils";
26+
import redisClient from "../config/redis";
27+
import crypto from "crypto";
28+
import { sendAccVerificationMail } from "../utils/mailer";
29+
import { ACCOUNT_VERIFICATION_SUBJ } from "../utils/constants";
2530

2631
export async function createUser(
2732
req: Request,
@@ -77,8 +82,9 @@ export async function createUser(
7782
email,
7883
hashedPassword
7984
);
85+
8086
return res.status(201).json({
81-
message: `Created new user ${username} successfully`,
87+
message: `Created new user ${username} successfully.`,
8288
data: formatUserResponse(createdUser),
8389
});
8490
} else {
@@ -94,6 +100,74 @@ export async function createUser(
94100
}
95101
}
96102

103+
export const sendVerificationMail = async (
104+
req: Request,
105+
res: Response
106+
): Promise<Response> => {
107+
try {
108+
const { email } = req.body;
109+
const user = await _findUserByEmail(email);
110+
111+
if (!user) {
112+
return res.status(404).json({ message: `User ${email} not found` });
113+
}
114+
115+
const emailToken = crypto.randomBytes(16).toString("hex");
116+
await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes
117+
await sendAccVerificationMail(
118+
email,
119+
ACCOUNT_VERIFICATION_SUBJ,
120+
user.username,
121+
emailToken
122+
);
123+
124+
return res.status(200).json({
125+
message: "Verification email sent. Please check your inbox.",
126+
data: { email, id: user.id },
127+
});
128+
} catch (error) {
129+
return res.status(500).json({
130+
message: "Unknown error when sending verification email!",
131+
error,
132+
});
133+
}
134+
};
135+
136+
export const verifyUser = async (
137+
req: Request,
138+
res: Response
139+
): Promise<Response> => {
140+
try {
141+
const { email, token } = req.params;
142+
143+
const user = await _findUserByEmail(email);
144+
if (!user) {
145+
return res.status(404).json({ message: `User ${email} not found` });
146+
}
147+
148+
const expectedToken = await redisClient.get(email);
149+
150+
if (expectedToken !== token) {
151+
return res
152+
.status(400)
153+
.json({ message: "Invalid token. Please request for a new one." });
154+
}
155+
156+
const updatedUser = await _updateUserVerification(email);
157+
if (!updatedUser) {
158+
return res.status(404).json({ message: `User ${email} not verified.` });
159+
}
160+
161+
return res
162+
.status(200)
163+
.json({ message: `User ${email} verified successfully.` });
164+
} catch (error) {
165+
return res
166+
.status(500)
167+
.json({ message: "Unknown error when verifying user!", error });
168+
}
169+
};
170+
97171
export const createImageLink = async (
98172
req: Request,
99173
res: Response

backend/user-service/jest.config.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import type { Config } from "jest";
7+
// import type { JestConfigWithTsJest } from "ts-jest";
78

89
const config: Config = {
910
// All imported modules in your tests should be mocked automatically
@@ -52,6 +53,8 @@ const config: Config = {
5253
// Make calling deprecated APIs throw helpful error messages
5354
// errorOnDeprecated: false,
5455

56+
// extensionsToTreatAsEsm: [".ts"],
57+
5558
// The default configuration for fake timers
5659
// fakeTimers: {
5760
// "enableGlobally": false
@@ -90,7 +93,9 @@ const config: Config = {
9093
// ],
9194

9295
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
93-
// moduleNameMapper: {},
96+
// moduleNameMapper: {
97+
// "^(\\.{1,2}/.*)\\.js$": "$1",
98+
// },
9499

95100
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
96101
// modulePathIgnorePatterns: [],
@@ -146,7 +151,7 @@ const config: Config = {
146151
// snapshotSerializers: [],
147152

148153
// The test environment that will be used for testing
149-
// testEnvironment: "jest-environment-node",
154+
// testEnvironment: "node",
150155

151156
// Options that will be passed to the testEnvironment
152157
// testEnvironmentOptions: {},
@@ -175,12 +180,38 @@ const config: Config = {
175180
// testRunner: "jest-circus/runner",
176181

177182
// A map from regular expressions to paths to transformers
178-
// transform: undefined,
183+
// transform: {
184+
// "^.+\\.tsx?$": [
185+
// "ts-jest",
186+
// {
187+
// diagnostics: {
188+
// ignoreCodes: [1343],
189+
// },
190+
// astTransformers: {
191+
// before: [
192+
// {
193+
// path: "node_modules/ts-jest-mock-import-meta", // or, alternatively, 'ts-jest-mock-import-meta' directly, without node_modules.
194+
// },
195+
// ],
196+
// },
197+
// },
198+
// ],
199+
// },
200+
// transform: {
201+
// "^.+\\.tsx?$": [
202+
// "ts-jest",
203+
// {
204+
// useESM: true,
205+
// astTransformers: { before: [{ path: "ts-jest-mock-import-meta" }] },
206+
// diagnostics: { ignoreCodes: [1343] },
207+
// },
208+
// ],
209+
// "node_modules/nodemailer-express-handlebars/.+\\.(j|t)sx?$": "ts-jest",
210+
// },
179211

180212
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
181213
// transformIgnorePatterns: [
182-
// "/node_modules/",
183-
// "\\.pnp\\.[^\\/]+$"
214+
// "node_modules/?!(nodemailer-express-handlebars/.*)",
184215
// ],
185216

186217
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them

backend/user-service/model/repository.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ export async function createUser(
2121
username: string,
2222
email: string,
2323
password: string,
24-
isAdmin: boolean = false
24+
isAdmin: boolean = false,
25+
isVerified: boolean = false
2526
): Promise<IUser> {
26-
return new UserModel({
27+
const user = new UserModel({
2728
firstName,
2829
lastName,
2930
username,
3031
email,
3132
password,
3233
isAdmin,
33-
}).save();
34+
});
35+
return user.save();
3436
}
3537

3638
export async function findUserByEmail(email: string): Promise<IUser | null> {
@@ -98,6 +100,20 @@ export async function updateUserPrivilegeById(
98100
);
99101
}
100102

103+
export async function updateUserVerification(
104+
email: string
105+
): Promise<IUser | null> {
106+
return UserModel.findOneAndUpdate(
107+
{ email },
108+
{
109+
$set: {
110+
isVerified: true,
111+
},
112+
},
113+
{ new: true } // return the updated user
114+
);
115+
}
116+
101117
export async function deleteUserById(userId: string): Promise<IUser | null> {
102118
return UserModel.findByIdAndDelete(userId);
103119
}

backend/user-service/model/user-model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export interface IUser extends Document {
1111
firstName: string;
1212
lastName: string;
1313
biography?: string;
14+
15+
isVerified: boolean;
1416
}
1517

1618
const UserModelSchema: Schema<IUser> = new mongoose.Schema({
@@ -54,6 +56,11 @@ const UserModelSchema: Schema<IUser> = new mongoose.Schema({
5456
required: false,
5557
default: "Hello World!",
5658
},
59+
isVerified: {
60+
type: Boolean,
61+
required: true,
62+
default: false,
63+
},
5764
});
5865

5966
const UserModel = mongoose.model<IUser>("UserModel", UserModelSchema);

0 commit comments

Comments
 (0)