Skip to content

Commit 13b5d46

Browse files
committed
Merge branch 'ms3-frontend' into ms3-jmsandiegoo/frontend-onboarding
2 parents 09644ab + 8acd140 commit 13b5d46

File tree

107 files changed

+1418
-18619
lines changed

Some content is hidden

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

107 files changed

+1418
-18619
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: 114 additions & 4 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,7 @@ export class AppService {
4956
const tokens = await this.generateTokens({
5057
id: userId,
5158
email: newUser.email,
59+
roles: newUser.roles,
5260
});
5361
await this.updateRefreshToken({
5462
id: userId,
@@ -85,6 +93,7 @@ export class AppService {
8593
const tokens = await this.generateTokens({
8694
id: userId,
8795
email: user.email,
96+
roles: user.roles,
8897
});
8998
await this.updateRefreshToken({
9099
id: userId,
@@ -132,6 +141,7 @@ export class AppService {
132141
const tokens = await this.generateTokens({
133142
id: id,
134143
email: user.email,
144+
roles: user.roles,
135145
});
136146
await this.updateRefreshToken({ id, refreshToken: tokens.refresh_token });
137147

@@ -141,6 +151,102 @@ export class AppService {
141151
}
142152
}
143153

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

181287
// Could include other fields like roles in the future
182288
private async generateTokens(payload: TokenPayload): Promise<Token> {
183-
const { id, email } = payload;
289+
const { id, email, roles } = payload;
184290

185291
const [accessToken, refreshToken] = await Promise.all([
186292
this.jwtService.signAsync(
187293
{
188294
sub: id,
189295
email,
296+
roles,
190297
},
191298
{
192299
secret: process.env.JWT_SECRET,
@@ -197,6 +304,7 @@ export class AppService {
197304
{
198305
sub: id,
199306
email,
307+
roles,
200308
},
201309
{
202310
secret: process.env.JWT_REFRESH_SECRET,
@@ -257,6 +365,7 @@ export class AppService {
257365
const jwtTokens = await this.generateTokens({
258366
id: user._id.toString(),
259367
email: user.email,
368+
roles: user.roles,
260369
});
261370

262371
await this.updateRefreshToken({
@@ -358,6 +467,7 @@ export class AppService {
358467
const jwtTokens = await this.generateTokens({
359468
id: user._id.toString(),
360469
email: user.email,
470+
roles: user.roles,
361471
});
362472

363473
await this.updateRefreshToken({
@@ -412,7 +522,7 @@ export class AppService {
412522
return {
413523
...response.data,
414524
email: emailResponse.data.find((email) => email.primary)?.email,
415-
}
525+
};
416526
} catch (error) {
417527
throw new RpcException(
418528
`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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface TokenPayload {
22
id: string;
33
email: string;
4+
roles: string[];
45
}

0 commit comments

Comments
 (0)