Skip to content

Commit dc7cd77

Browse files
authored
Merge pull request #58 from CS3219-AY2425S1/ms3
Milestone 3
2 parents 2b905e3 + bbbfdcd commit dc7cd77

File tree

156 files changed

+3706
-18702
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+3706
-18702
lines changed

backend/auth-service/.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@ GITHUB_CLIENT_ID=GITHUB_CLIENT_ID
1111
GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET
1212
GITHUB_CALLBACK_URL=http://localhost:4000/api/auth/github/callback
1313

14+
# Gmail Service
15+
NODEMAILER_GMAIL_USER=GMAIL_EMAIL
16+
NODEMAILER_GMAIL_PASSWORD=GMAIL_PASSWORD
17+
1418
# Database
15-
MONGO_CONNECTION_STRING=MONGO_CONNECTION_STRING
19+
MONGO_CONNECTION_STRING=MONGO_CONNECTION_STRING
20+
21+
# Frontend
22+
FRONTEND_URL=http://localhost:3000

backend/auth-service/package-lock.json

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

backend/auth-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"class-transformer": "^0.5.1",
3333
"class-validator": "^0.14.1",
3434
"google-auth-library": "^9.14.1",
35+
"nodemailer": "^6.9.15",
3536
"passport": "^0.7.0",
3637
"passport-github2": "^0.1.12",
3738
"passport-google-oauth20": "^2.0.0",
@@ -47,6 +48,7 @@
4748
"@types/express": "^4.17.17",
4849
"@types/jest": "^29.5.2",
4950
"@types/node": "^20.3.1",
51+
"@types/nodemailer": "^6.4.16",
5052
"@types/passport-google-oauth20": "^2.0.16",
5153
"@types/supertest": "^6.0.0",
5254
"@typescript-eslint/eslint-plugin": "^8.0.0",

backend/auth-service/src/app.controller.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,4 @@ describe('AppController', () => {
1414
appController = app.get<AppController>(AppController);
1515
});
1616

17-
describe('root', () => {
18-
it('should return "Hello World!"', () => {
19-
expect(appController.getHello()).toBe('Hello World!');
20-
});
21-
});
2217
});

backend/auth-service/src/app.controller.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Controller } from '@nestjs/common';
22
import { AppService } from './app.service';
33
import { MessagePattern, Payload } from '@nestjs/microservices';
4-
import { AuthDto, AuthIdDto, RefreshTokenDto } from './dto';
4+
import {
5+
AuthDto,
6+
AuthIdDto,
7+
RefreshTokenDto,
8+
ResetPasswordDto,
9+
ResetPasswordRequestDto,
10+
} from './dto';
511

612
@Controller()
713
export class AppController {
@@ -22,11 +28,26 @@ export class AppController {
2228
return this.appService.logout(dto);
2329
}
2430

31+
@MessagePattern({ cmd: 'request-reset-password' })
32+
requestResetPassword(@Payload() dto: ResetPasswordRequestDto) {
33+
return this.appService.generateResetPasswordRequest(dto);
34+
}
35+
36+
@MessagePattern({ cmd: 'reset-password' })
37+
resetPassword(@Payload() dto: ResetPasswordDto) {
38+
return this.appService.resetPassword(dto);
39+
}
40+
2541
@MessagePattern({ cmd: 'refresh-token' })
2642
refreshToken(@Payload() dto: RefreshTokenDto) {
2743
return this.appService.refreshToken(dto);
2844
}
2945

46+
@MessagePattern({ cmd: 'validate-password-reset-token' })
47+
validatePasswordResetToken(token: string) {
48+
return this.appService.validatePasswordResetToken(token);
49+
}
50+
3051
@MessagePattern({ cmd: 'validate-access-token' })
3152
validateToken(accessToken: string) {
3253
return this.appService.validateAccessToken(accessToken);

backend/auth-service/src/app.service.ts

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import * as bcrypt from 'bcryptjs';
2-
import { Inject, Injectable } from '@nestjs/common';
2+
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
33
import { JwtService } from '@nestjs/jwt';
44
import { Credentials, OAuth2Client } from 'google-auth-library';
55
import { ClientProxy } from '@nestjs/microservices';
6-
import { AuthDto, AuthIdDto, RefreshTokenDto } from './dto';
6+
import {
7+
AuthDto,
8+
AuthIdDto,
9+
RefreshTokenDto,
10+
ResetPasswordDto,
11+
ResetPasswordRequestDto,
12+
} from './dto';
713
import { HttpService } from '@nestjs/axios';
814
import { RpcException } from '@nestjs/microservices';
915
import { firstValueFrom } from 'rxjs';
1016
import axios, { AxiosResponse } from 'axios';
1117
import { Token, TokenPayload } from './interfaces';
1218
import { AccountProvider } from './constants/account-provider.enum';
19+
import * as nodemailer from 'nodemailer';
1320

1421
const SALT_ROUNDS = 10;
1522

@@ -49,6 +56,8 @@ export class AppService {
4956
const tokens = await this.generateTokens({
5057
id: userId,
5158
email: newUser.email,
59+
isOnboarded: newUser.isOnboarded,
60+
roles: newUser.roles,
5261
});
5362
await this.updateRefreshToken({
5463
id: userId,
@@ -85,6 +94,8 @@ export class AppService {
8594
const tokens = await this.generateTokens({
8695
id: userId,
8796
email: user.email,
97+
isOnboarded: user.isOnboarded,
98+
roles: user.roles,
8899
});
89100
await this.updateRefreshToken({
90101
id: userId,
@@ -132,6 +143,8 @@ export class AppService {
132143
const tokens = await this.generateTokens({
133144
id: id,
134145
email: user.email,
146+
isOnboarded: user.isOnboarded,
147+
roles: user.roles,
135148
});
136149
await this.updateRefreshToken({ id, refreshToken: tokens.refresh_token });
137150

@@ -141,6 +154,102 @@ export class AppService {
141154
}
142155
}
143156

157+
public async generateResetPasswordRequest(dto: ResetPasswordRequestDto): Promise<boolean> {
158+
const user = await firstValueFrom(
159+
this.userClient.send(
160+
{
161+
cmd: 'get-user-by-email',
162+
},
163+
dto.email,
164+
),
165+
);
166+
167+
if (!user) {
168+
throw new RpcException('User not found');
169+
}
170+
171+
const resetToken = this.jwtService.sign(
172+
{ userId: user._id.toString(), email: dto.email, type: 'reset-password' },
173+
{
174+
secret: process.env.JWT_SECRET,
175+
expiresIn: '1hr',
176+
},
177+
);
178+
179+
// Send reset password email
180+
await this.sendResetEmail(user.email, resetToken);
181+
182+
return true;
183+
}
184+
185+
public async resetPassword(dto: ResetPasswordDto): Promise<boolean> {
186+
const { userId, email } = await this.validatePasswordResetToken(dto.token);
187+
188+
const hashedPassword = await bcrypt.hash(dto.password, SALT_ROUNDS);
189+
190+
const response = await firstValueFrom(
191+
this.userClient.send(
192+
{ cmd: 'update-user-password' },
193+
{ id: userId, password: hashedPassword },
194+
),
195+
);
196+
197+
if (!response) {
198+
throw new RpcException('Error resetting password');
199+
}
200+
201+
return true;
202+
}
203+
204+
public async validatePasswordResetToken(token: string): Promise<any> {
205+
try {
206+
const decoded = this.jwtService.verify(token, {
207+
secret: process.env.JWT_SECRET,
208+
});
209+
const { userId, email, type } = decoded;
210+
if (type !== 'reset-password') {
211+
throw new RpcException({
212+
statusCode: HttpStatus.UNAUTHORIZED,
213+
message: 'Unauthorized: Invalid or expired password reset token',
214+
});
215+
}
216+
return { userId, email };
217+
} catch (err) {
218+
throw new RpcException({
219+
statusCode: HttpStatus.UNAUTHORIZED,
220+
message: 'Unauthorized: Invalid or expired password reset token',
221+
});
222+
}
223+
}
224+
225+
private async sendResetEmail(email: string, token: string) {
226+
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; // To change next time
227+
228+
const transporter = nodemailer.createTransport({
229+
service: 'gmail',
230+
host: 'smtp.gmail.com',
231+
port: 465,
232+
secure: true,
233+
auth: {
234+
user: process.env.NODEMAILER_GMAIL_USER,
235+
pass: process.env.NODEMAILER_GMAIL_PASSWORD,
236+
},
237+
});
238+
239+
const mailOptions = {
240+
241+
to: email,
242+
subject: 'Password Reset for PeerPrep',
243+
text: `Click here to reset your password: ${resetUrl}`,
244+
};
245+
246+
transporter.sendMail(mailOptions, (error, info) => {
247+
if (error) {
248+
throw new RpcException(`Error sending reset email: ${error.message}`);
249+
}
250+
});
251+
}
252+
144253
public async validateAccessToken(accessToken: string): Promise<any> {
145254
try {
146255
const decoded = this.jwtService.verify(accessToken, {
@@ -180,23 +289,23 @@ export class AppService {
180289

181290
// Could include other fields like roles in the future
182291
private async generateTokens(payload: TokenPayload): Promise<Token> {
183-
const { id, email } = payload;
292+
const { id, ...rest } = payload;
184293

185294
const [accessToken, refreshToken] = await Promise.all([
186295
this.jwtService.signAsync(
187-
{
188-
sub: id,
189-
email,
190-
},
191-
{
192-
secret: process.env.JWT_SECRET,
193-
expiresIn: '15m', // 15 minute
296+
{
297+
sub: id,
298+
...rest,
299+
},
300+
{
301+
secret: process.env.JWT_SECRET,
302+
expiresIn: '1h', // 1 hour
194303
},
195304
),
196305
this.jwtService.signAsync(
197306
{
198307
sub: id,
199-
email,
308+
...rest,
200309
},
201310
{
202311
secret: process.env.JWT_REFRESH_SECRET,
@@ -257,6 +366,8 @@ export class AppService {
257366
const jwtTokens = await this.generateTokens({
258367
id: user._id.toString(),
259368
email: user.email,
369+
isOnboarded: user.isOnboarded,
370+
roles: user.roles,
260371
});
261372

262373
await this.updateRefreshToken({
@@ -358,6 +469,8 @@ export class AppService {
358469
const jwtTokens = await this.generateTokens({
359470
id: user._id.toString(),
360471
email: user.email,
472+
isOnboarded: user.isOnboarded,
473+
roles: user.roles,
361474
});
362475

363476
await this.updateRefreshToken({
@@ -412,7 +525,7 @@ export class AppService {
412525
return {
413526
...response.data,
414527
email: emailResponse.data.find((email) => email.primary)?.email,
415-
}
528+
};
416529
} catch (error) {
417530
throw new RpcException(
418531
`Unable to retrieve Github user profile: ${error.message}`,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export { AuthDto } from './auth.dto';
22
export { AuthIdDto } from './auth-id.dto';
33
export { RefreshTokenDto } from './refresh-token.dto';
4+
export { ResetPasswordRequestDto } from './reset-password-request.dto';
5+
export { ResetPasswordDto } from './reset-password.dto';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
2+
3+
export class ResetPasswordRequestDto {
4+
@IsEmail()
5+
@IsNotEmpty()
6+
email: string;
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { IsNotEmpty, IsString } from 'class-validator';
2+
3+
export class ResetPasswordDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
token: string;
7+
8+
@IsString()
9+
@IsNotEmpty()
10+
password: string;
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export interface TokenPayload {
22
id: string;
33
email: string;
4+
isOnboarded: boolean;
5+
roles: string[];
46
}

0 commit comments

Comments
 (0)