Skip to content

Commit 2c17a6b

Browse files
ojn03/feat-updateUser (#12)
* feat: completed updateUser endpoint resolves #5 * style: used strict comparison operators and modified error messages * added error handling for objectID * feat: added input validation to UpdateUserDTO * fix: applied suggestions from PR review * fix: added domain specific URL validators for DTO * fix: converted ObjectId primary column to numbers and finalized PR suggestions * fix: strict equality and https restricted protocol --------- Co-authored-by: Harrison Kim <kim.harr@northeastern.edu>
1 parent d3b012c commit 2c17a6b

File tree

6 files changed

+225
-24
lines changed

6 files changed

+225
-24
lines changed

apps/backend/src/users/types.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,33 @@
1+
// TODO: Probably want these types to be available to both the frontend and backend in a "common" folder
12
export enum Status {
2-
ADMIN = 'ADMIN',
3-
STANDARD = 'STANDARD',
3+
MEMBER = 'Member',
4+
RECRUITER = 'Recruiter',
5+
ADMIN = 'Admin',
6+
ALUMNI = 'Alumni',
7+
APPLICANT = 'Applicant',
8+
}
9+
10+
export enum Team {
11+
SFTT = 'Speak For The Trees',
12+
CONSTELLATION = 'Constellation',
13+
JPAL = 'J-PAL',
14+
BREAKTIME = 'Breaktime',
15+
GI = 'Green Infrastructure',
16+
CI = 'Core Infrastructure',
17+
EBOARD = 'E-Board',
18+
}
19+
20+
export enum Role {
21+
DIRECTOR_OF_ENGINEERING = 'Director of Engineering',
22+
DIRECTOR_OF_PRODUCT = 'Director of Product',
23+
DIRECTOR_OF_FINANCE = 'Director of Finance',
24+
DIRECTOR_OF_MARKETING = 'Director of Marketing',
25+
DIRECTOR_OF_RECRUITMENT = 'Director of Recruitment',
26+
DIRECTOR_OF_OPERATIONS = 'Director of Operations',
27+
DIRECTOR_OF_EVENTS = 'Director of Events',
28+
DIRECTOR_OF_DESIGN = 'Director of Design',
29+
PRODUCT_MANAGER = 'Product Manager',
30+
PRODUCT_DESIGNER = 'Product Designer',
31+
TECH_LEAD = 'Technical Lead',
32+
DEVELOPER = 'Developer',
433
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Status, Role, Team } from './types';
2+
import {
3+
IsEmail,
4+
IsOptional,
5+
IsEnum,
6+
IsArray,
7+
ArrayMinSize,
8+
ArrayUnique,
9+
IsUrl,
10+
} from 'class-validator';
11+
12+
export class UpdateUserDTO {
13+
@IsOptional()
14+
@IsEnum(Status)
15+
status?: Status;
16+
17+
@IsOptional()
18+
@IsEmail()
19+
email?: string;
20+
21+
@IsOptional()
22+
profilePicture?: string;
23+
24+
@IsOptional()
25+
@IsUrl({
26+
protocols: ['https'],
27+
require_protocol: true,
28+
host_whitelist: ['www.linkedin.com'],
29+
})
30+
linkedin?: string;
31+
32+
@IsOptional()
33+
@IsUrl({
34+
protocols: ['https'],
35+
require_protocol: true,
36+
host_whitelist: ['github.com'],
37+
})
38+
github?: string;
39+
40+
@IsOptional()
41+
@IsEnum(Team)
42+
team?: Team;
43+
44+
@IsOptional()
45+
@IsArray()
46+
@ArrayMinSize(1)
47+
@ArrayUnique()
48+
@IsEnum(Role, { each: true })
49+
role?: Role[];
50+
}
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { Entity, Column, ObjectIdColumn, ObjectId } from 'typeorm';
2-
3-
import type { Status } from './types';
1+
import { Entity, Column } from 'typeorm';
2+
import { Status, Role, Team } from './types';
43

54
@Entity()
65
export class User {
7-
@ObjectIdColumn()
8-
_id: ObjectId;
6+
// @ObjectIdColumn()
7+
// _id: ObjectId;
98

109
@Column({ primary: true })
11-
id: number;
10+
userId: number;
1211

1312
@Column()
1413
status: Status;
@@ -21,4 +20,19 @@ export class User {
2120

2221
@Column()
2322
email: string;
23+
24+
@Column()
25+
profilePicture: string | null;
26+
27+
@Column()
28+
linkedin: string | null;
29+
30+
@Column()
31+
github: string | null;
32+
33+
@Column()
34+
team: Team | null;
35+
36+
@Column()
37+
role: Role[] | null;
2438
}

apps/backend/src/users/users.controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import {
2+
Body,
23
Controller,
34
Delete,
45
Get,
56
Param,
67
ParseIntPipe,
78
UseGuards,
89
UseInterceptors,
10+
Patch,
911
} from '@nestjs/common';
1012
import { UsersService } from './users.service';
1113
import { AuthGuard } from '@nestjs/passport';
1214
import { User } from './user.entity';
1315
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';
16+
import { UpdateUserDTO } from './update-user.dto';
1417

1518
@Controller('users')
1619
@UseInterceptors(CurrentUserInterceptor)
@@ -27,4 +30,12 @@ export class UsersController {
2730
removeUser(@Param('id') id: string) {
2831
return this.usersService.remove(parseInt(id));
2932
}
33+
34+
@Patch(':userId')
35+
async updateUser(
36+
@Body() updateUserDTO: UpdateUserDTO,
37+
@Param('userId', ParseIntPipe) userId: number,
38+
): Promise<User> {
39+
return this.usersService.updateUser(updateUserDTO, userId);
40+
}
3041
}
Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,118 @@
1-
import { Injectable, NotFoundException } from '@nestjs/common';
1+
import {
2+
Injectable,
3+
BadRequestException,
4+
UnauthorizedException,
5+
NotFoundException,
6+
} from '@nestjs/common';
27
import { InjectRepository } from '@nestjs/typeorm';
3-
import { Repository } from 'typeorm';
4-
8+
import { MongoRepository } from 'typeorm';
59
import { User } from './user.entity';
10+
import { UpdateUserDTO } from './update-user.dto';
611
import { Status } from './types';
12+
import { getCurrentUser } from './utils';
713

814
@Injectable()
915
export class UsersService {
10-
constructor(@InjectRepository(User) private repo: Repository<User>) {}
16+
constructor(
17+
@InjectRepository(User)
18+
private usersRepository: MongoRepository<User>,
19+
) {}
1120

1221
async create(email: string, firstName: string, lastName: string) {
13-
const userId = (await this.repo.count()) + 1;
14-
const user = this.repo.create({
15-
id: userId,
16-
status: Status.STANDARD,
22+
const userId = (await this.usersRepository.count()) + 1;
23+
const user = this.usersRepository.create({
24+
userId,
25+
status: Status.MEMBER,
1726
firstName,
1827
lastName,
1928
email,
2029
});
2130

22-
return this.repo.save(user);
31+
return this.usersRepository.save(user);
2332
}
2433

25-
findOne(id: number) {
26-
if (!id) {
27-
return null;
34+
async findAll(getAllMembers: boolean): Promise<User[]> {
35+
if (!getAllMembers) return [];
36+
37+
const currentUser = getCurrentUser();
38+
39+
if (currentUser.status === Status.APPLICANT) {
40+
throw new UnauthorizedException();
2841
}
2942

30-
return this.repo.findOneBy({ id });
43+
const users: User[] = await this.usersRepository.find({
44+
where: {
45+
status: { $not: { $eq: Status.APPLICANT } },
46+
},
47+
});
48+
49+
return users;
50+
}
51+
52+
async findOne(userId: number) {
53+
const user = await this.usersRepository.findOneBy({ userId });
54+
55+
if (!user) {
56+
throw new BadRequestException('User not found');
57+
}
58+
59+
const currentUser = getCurrentUser();
60+
61+
const currentStatus = currentUser.status;
62+
const targetStatus = user.status;
63+
switch (currentStatus) {
64+
//admin can access all users
65+
case Status.ADMIN:
66+
break;
67+
//recruiter can access applicant, and themselves
68+
case Status.RECRUITER:
69+
if (targetStatus == Status.APPLICANT) {
70+
break;
71+
} else if (currentUser.userId !== user.userId) {
72+
throw new BadRequestException('User not found');
73+
}
74+
break;
75+
//everyone else can only access themselves
76+
default:
77+
if (currentUser.userId !== user.userId) {
78+
throw new BadRequestException('User not found');
79+
}
80+
}
81+
82+
return user;
83+
}
84+
85+
async updateUser(
86+
updateUserDTO: UpdateUserDTO,
87+
userId: number,
88+
): Promise<User> {
89+
const user: User = await this.usersRepository.findOne({
90+
where: {
91+
userId: { $eq: userId },
92+
},
93+
});
94+
95+
if (!user) {
96+
throw new BadRequestException(`User ${userId} not found.`);
97+
}
98+
99+
const currentUser = getCurrentUser();
100+
101+
if (currentUser.status !== Status.ADMIN && userId !== currentUser.userId) {
102+
throw new UnauthorizedException();
103+
}
104+
105+
await this.usersRepository.update({ userId }, updateUserDTO);
106+
return await this.usersRepository.findOne({
107+
where: {
108+
userId: { $eq: userId },
109+
},
110+
});
31111
}
32112

113+
/* TODO merge these methods with the above methods */
33114
find(email: string) {
34-
return this.repo.find({ where: { email } });
115+
return this.usersRepository.find({ where: { email } });
35116
}
36117

37118
async update(id: number, attrs: Partial<User>) {
@@ -43,7 +124,7 @@ export class UsersService {
43124

44125
Object.assign(user, attrs);
45126

46-
return this.repo.save(user);
127+
return this.usersRepository.save(user);
47128
}
48129

49130
async remove(id: number) {
@@ -53,6 +134,7 @@ export class UsersService {
53134
throw new NotFoundException('User not found');
54135
}
55136

56-
return this.repo.remove(user);
137+
return this.usersRepository.remove(user);
57138
}
139+
/* TODO merge these methods with the above methods */
58140
}

apps/backend/src/users/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Status } from './types';
2+
import { User } from './user.entity';
3+
4+
export const getCurrentUser = (): User => ({
5+
userId: 999,
6+
status: Status.ADMIN,
7+
firstName: 'jimmy',
8+
lastName: 'jimmy2',
9+
email: 'jimmy.jimmy2@mail.com',
10+
profilePicture: null,
11+
linkedin: null,
12+
github: null,
13+
team: null,
14+
role: null,
15+
});

0 commit comments

Comments
 (0)