Skip to content

Commit f6b5314

Browse files
committed
Add email verification on signup
1 parent 0ad0bca commit f6b5314

File tree

14 files changed

+332
-12
lines changed

14 files changed

+332
-12
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/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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
client.on("error", (err) => console.log(`Error: ${err}`));
11+
12+
await client.connect();
13+
14+
export default client;

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

Lines changed: 48 additions & 0 deletions
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 { sendMail } from "../utils/mailer";
29+
import { ACCOUNT_VERIFICATION_SUBJ } from "../utils/constants";
2530

2631
export async function createUser(
2732
req: Request,
@@ -77,6 +82,14 @@ export async function createUser(
7782
email,
7883
hashedPassword
7984
);
85+
86+
const emailToken = crypto.randomBytes(16).toString("hex");
87+
await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes
88+
const emailText = `Hello ${username},\n\n
89+
Please click on the following link to verify your account:\n\nhttp://localhost:3001/api/users/verify-email/${email}/${emailToken}\n\n
90+
If you did not request this, please ignore this email.`;
91+
await sendMail(email, ACCOUNT_VERIFICATION_SUBJ, emailText);
92+
8093
return res.status(201).json({
8194
message: `Created new user ${username} successfully`,
8295
data: formatUserResponse(createdUser),
@@ -94,6 +107,41 @@ export async function createUser(
94107
}
95108
}
96109

110+
export const verifyUser = async (
111+
req: Request,
112+
res: Response
113+
): Promise<Response> => {
114+
try {
115+
const { email, token } = req.params;
116+
117+
const user = await _findUserByEmail(email);
118+
if (!user) {
119+
return res.status(404).json({ message: `User ${email} not found` });
120+
}
121+
122+
const expectedToken = await redisClient.get(email);
123+
124+
if (expectedToken !== token) {
125+
return res
126+
.status(400)
127+
.json({ message: "Invalid token. Please request for a new one." });
128+
}
129+
130+
const updatedUser = await _updateUserVerification(email);
131+
if (!updatedUser) {
132+
return res.status(404).json({ message: `User ${email} not verified.` });
133+
}
134+
135+
return res
136+
.status(200)
137+
.json({ message: `User ${email} verified successfully.` });
138+
} catch {
139+
return res
140+
.status(500)
141+
.json({ message: "Unknown error when verifying user!" });
142+
}
143+
};
144+
97145
export const createImageLink = async (
98146
req: Request,
99147
res: Response

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);

backend/user-service/package-lock.json

Lines changed: 116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/user-service/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@types/jsonwebtoken": "^9.0.7",
2424
"@types/multer": "^1.4.12",
2525
"@types/node": "^22.5.5",
26+
"@types/nodemailer": "^6.4.16",
2627
"@types/supertest": "^6.0.2",
2728
"@types/swagger-ui-express": "^4.1.6",
2829
"@types/uuid": "^10.0.0",
@@ -50,6 +51,8 @@
5051
"jsonwebtoken": "^9.0.2",
5152
"mongoose": "^8.5.4",
5253
"multer": "^1.4.5-lts.1",
54+
"nodemailer": "^6.9.15",
55+
"redis": "^4.7.0",
5356
"swagger-ui-express": "^5.0.1",
5457
"uuid": "^10.0.0",
5558
"validator": "^13.12.0",

0 commit comments

Comments
 (0)