Skip to content

Commit 29ff84c

Browse files
committed
Convert user service to TS
1 parent 90535e8 commit 29ff84c

File tree

9 files changed

+241
-130
lines changed

9 files changed

+241
-130
lines changed

backend/user-service/.env.sample

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ PORT=3001
33

44
# Secret for creating JWT signature
55
JWT_SECRET=you-can-replace-this-with-your-own-secret
6+
7+
# admin default credentials
8+
ADMIN_USERNAME=administrator
9+
ADMIN_EMAIL=[email protected]
10+
ADMIN_PASSWORD=Admin@123

backend/user-service/README.md

Lines changed: 99 additions & 87 deletions
Large diffs are not rendered by default.

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

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import {
1212
updateUserById as _updateUserById,
1313
updateUserPrivilegeById as _updateUserPrivilegeById,
1414
} from "../model/repository";
15-
import { validateEmail, validateUsername, validatePassword } from "../utils/validators";
15+
import {
16+
validateEmail,
17+
validateUsername,
18+
validatePassword,
19+
validateName,
20+
validateBiography,
21+
} from "../utils/validators";
1622
import { IUser } from "../model/user-model";
1723

1824
export async function createUser(req: Request, res: Response): Promise<Response> {
@@ -87,59 +93,102 @@ export async function getAllUsers(req: Request, res: Response): Promise<Response
8793

8894
export async function updateUser(req: Request, res: Response): Promise<Response> {
8995
try {
90-
const { username, email, password, profile_picture_url, first_name, last_name, biography } =
96+
const { username, email, password, profilePictureUrl, firstName, lastName, biography } =
9197
req.body;
92-
if (
93-
username ||
94-
email ||
95-
password ||
96-
profile_picture_url ||
97-
first_name ||
98-
last_name ||
99-
biography
100-
) {
98+
if (username || email || password || profilePictureUrl || firstName || lastName || biography) {
10199
const userId = req.params.id;
100+
102101
if (!isValidObjectId(userId)) {
103102
return res.status(404).json({ message: `User ${userId} not found` });
104103
}
104+
105105
const user = await _findUserById(userId);
106106
if (!user) {
107107
return res.status(404).json({ message: `User ${userId} not found` });
108108
}
109-
if (username || email) {
110-
let existingUser = await _findUserByUsername(username);
109+
110+
if (username) {
111+
const { isValid: isValidUsername, message: usernameMessage } = validateUsername(username);
112+
if (!isValidUsername) {
113+
return res.status(400).json({ message: usernameMessage });
114+
}
115+
116+
const existingUser = await _findUserByUsername(username);
111117
if (existingUser && existingUser.id !== userId) {
112118
return res.status(409).json({ message: "username already exists" });
113119
}
114-
existingUser = await _findUserByEmail(email);
120+
}
121+
122+
if (email) {
123+
const { isValid: isValidEmail, message: emailMessage } = validateEmail(email);
124+
if (!isValidEmail) {
125+
return res.status(400).json({ message: emailMessage });
126+
}
127+
128+
const existingUser = await _findUserByEmail(email);
115129
if (existingUser && existingUser.id !== userId) {
116130
return res.status(409).json({ message: "email already exists" });
117131
}
118132
}
119133

120134
let hashedPassword: string | undefined;
121135
if (password) {
136+
const { isValid: isValidPassword, message: passwordMessage } = validatePassword(password);
137+
if (!isValidPassword) {
138+
return res.status(400).json({ message: passwordMessage });
139+
}
140+
122141
const salt = bcrypt.genSaltSync(10);
123142
hashedPassword = bcrypt.hashSync(password, salt);
124143
}
144+
145+
if (firstName) {
146+
const { isValid: isValidFirstName, message: firstNameMessage } = validateName(
147+
firstName,
148+
"first name"
149+
);
150+
if (!isValidFirstName) {
151+
return res.status(400).json({ message: firstNameMessage });
152+
}
153+
}
154+
155+
if (lastName) {
156+
const { isValid: isValidLastName, message: lastNameMessage } = validateName(
157+
lastName,
158+
"last name"
159+
);
160+
if (!isValidLastName) {
161+
return res.status(400).json({ message: lastNameMessage });
162+
}
163+
}
164+
165+
if (biography) {
166+
const { isValid: isValidBiography, message: biographyMessage } =
167+
validateBiography(biography);
168+
if (!isValidBiography) {
169+
return res.status(400).json({ message: biographyMessage });
170+
}
171+
}
172+
125173
const updatedUser = await _updateUserById(
126174
userId,
127175
username,
128176
email,
129177
hashedPassword,
130-
profile_picture_url,
131-
first_name,
132-
last_name,
178+
profilePictureUrl,
179+
firstName,
180+
lastName,
133181
biography
134182
);
135183
return res.status(200).json({
136184
message: `Updated data for user ${userId}`,
137185
data: formatUserResponse(updatedUser as IUser),
138186
});
139187
} else {
140-
return res
141-
.status(400)
142-
.json({ message: "No field to update: username and email and password are all missing!" });
188+
return res.status(400).json({
189+
message:
190+
"No field to update. Update one of the following fields: username, email, password, profilePictureUrl, firstName, lastName, biography",
191+
});
143192
}
144193
} catch (err) {
145194
console.error(err);
@@ -203,9 +252,9 @@ export function formatUserResponse(user: IUser) {
203252
isAdmin: user.isAdmin,
204253
createdAt: user.createdAt,
205254

206-
profile_picture_url: user.profile_picture_url,
207-
first_name: user.first_name,
208-
last_name: user.last_name,
255+
profilePictureUrl: user.profilePictureUrl,
256+
firstName: user.firstName,
257+
lastName: user.lastName,
209258
biography: user.biography,
210259
};
211260
}

backend/user-service/model/repository.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ export async function connectToDB() {
1616
export async function createUser(
1717
username: string,
1818
email: string,
19-
password: string
19+
password: string,
20+
isAdmin: boolean = false
2021
): Promise<IUser> {
2122
return new UserModel({
2223
username,
2324
email,
2425
password,
25-
first_name: faker.person.firstName(),
26-
last_name: faker.person.lastName(),
26+
firstName: faker.person.firstName(),
27+
lastName: faker.person.lastName(),
28+
isAdmin,
2729
}).save();
2830
}
2931

@@ -57,9 +59,9 @@ export async function updateUserById(
5759
username: string,
5860
email: string,
5961
password: string | undefined,
60-
profile_picture_url: string,
61-
first_name: string,
62-
last_name: string,
62+
profilePictureUrl: string,
63+
firstName: string,
64+
lastName: string,
6365
biography: string
6466
): Promise<IUser | null> {
6567
return UserModel.findByIdAndUpdate(
@@ -69,9 +71,9 @@ export async function updateUserById(
6971
username,
7072
email,
7173
password,
72-
profile_picture_url,
73-
first_name,
74-
last_name,
74+
profilePictureUrl,
75+
firstName,
76+
lastName,
7577
biography,
7678
},
7779
},

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export interface IUser extends Document {
77
createdAt?: Date;
88
isAdmin: boolean;
99

10-
profile_picture_url?: string;
11-
first_name?: string;
12-
last_name?: string;
10+
profilePictureUrl?: string;
11+
firstName?: string;
12+
lastName?: string;
1313
biography?: string;
1414
}
1515

@@ -37,15 +37,15 @@ const UserModelSchema: Schema<IUser> = new mongoose.Schema({
3737
required: true,
3838
default: false,
3939
},
40-
profile_picture_url: {
40+
profilePictureUrl: {
4141
type: String,
4242
required: false,
4343
},
44-
first_name: {
44+
firstName: {
4545
type: String,
4646
required: false,
4747
},
48-
last_name: {
48+
lastName: {
4949
type: String,
5050
required: false,
5151
},

backend/user-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"main": "app.ts",
66
"type": "module",
77
"scripts": {
8+
"seed": "tsx scripts/seed.ts",
89
"start": "tsx server.ts",
10+
"dev": "tsx watch server.ts",
911
"test": "echo \"Error: no test specified\" && exit 1"
1012
},
1113
"keywords": [],

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
updateUser,
99
updateUserPrivilege,
1010
} from "../controller/user-controller.js";
11-
import { verifyAccessToken, verifyIsAdmin, verifyIsOwnerOrAdmin } from "../middleware/basic-access-control.js";
11+
import {
12+
verifyAccessToken,
13+
verifyIsAdmin,
14+
verifyIsOwnerOrAdmin,
15+
} from "../middleware/basic-access-control.js";
1216

1317
const router = express.Router();
1418

@@ -18,7 +22,7 @@ router.patch("/:id/privilege", verifyAccessToken, verifyIsAdmin, updateUserPrivi
1822

1923
router.post("/", createUser);
2024

21-
router.get("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, getUser);
25+
router.get("/:id", verifyAccessToken, getUser);
2226

2327
router.patch("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, updateUser);
2428

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import bcrypt from "bcrypt";
2+
import { connectToDB, createUser, findUserByEmail } from "../model/repository";
3+
4+
export async function seedAdminAccount() {
5+
await connectToDB();
6+
7+
const adminUsername = process.env.ADMIN_USERNAME || "administrator";
8+
const adminEmail = process.env.ADMIN_EMAIL || "[email protected]";
9+
const adminPassword = process.env.ADMIN_PASSWORD || "Admin@123";
10+
11+
if (!process.env.ADMIN_USERNAME || !process.env.ADMIN_EMAIL || !process.env.ADMIN_PASSWORD) {
12+
console.error(
13+
"Admin account not seeded in .env. Using default admin account credentials (username: administrator, email: [email protected], password: Admin@123)"
14+
);
15+
}
16+
17+
try {
18+
const existingAdmin = await findUserByEmail(adminEmail);
19+
if (existingAdmin) {
20+
console.error("Admin account already exists in the database.");
21+
process.exit(1);
22+
}
23+
24+
const salt = bcrypt.genSaltSync(10);
25+
const hashedPassword = bcrypt.hashSync(adminPassword, salt);
26+
27+
await createUser(adminUsername, adminEmail, hashedPassword, true);
28+
29+
console.log("Admin account created successfully.");
30+
process.exit(0);
31+
} catch (err) {
32+
console.error("Error seeding admin account:", err);
33+
process.exit(1);
34+
}
35+
}
36+
37+
seedAdminAccount();

backend/user-service/types/request.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ export interface AuthenticatedRequest extends Request {
66
username: string;
77
email: string;
88
isAdmin: boolean;
9-
profile_picture_url?: string;
10-
first_name?: string;
11-
last_name?: string;
9+
profilePictureUrl?: string;
10+
firstName?: string;
11+
lastName?: string;
1212
biography?: string;
1313
};
1414
}

0 commit comments

Comments
 (0)