Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export const ForgotPasswordForm = () => {
const handleForgotPassword = async ({ email }) => {
setIsLoading(true);
try {
await authService.forgotPassword(email);
const hCaptchaToken = await captchaRef.current.getResponse();
await authService.forgotPassword({
email,
hCaptchaToken,
});
setIsSuccess(true);
} catch (err) {
showError(err);
Expand Down
5 changes: 3 additions & 2 deletions packages/apps/job-launcher/client/src/services/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ForgotPasswordRequest,
ResetPasswordRequest,
SignInRequest,
SignUpRequest,
Expand All @@ -24,8 +25,8 @@ export const signOut = async (refreshToken: string) => {
return data;
};

export const forgotPassword = async (email: string) => {
await api.post('/auth/forgot-password', { email });
export const forgotPassword = async (body: ForgotPasswordRequest) => {
await api.post('/auth/forgot-password', body);
};

export const resetPassword = async (body: ResetPasswordRequest) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/apps/job-launcher/client/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export type SignUpResponse = {
refreshToken: string;
};

export type ForgotPasswordRequest = {
email: string;
hCaptchaToken: string;
};

export type ResetPasswordRequest = {
password: string;
token: string;
Expand Down
1 change: 1 addition & 0 deletions packages/apps/job-launcher/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@nestjs/serve-static": "^4.0.1",
"@nestjs/swagger": "^7.4.2",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^10.0.1",
"@sendgrid/mail": "^8.1.3",
"@types/passport-jwt": "^4.0.1",
Expand Down
44 changes: 28 additions & 16 deletions packages/apps/job-launcher/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { ScheduleModule } from '@nestjs/schedule';
import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { join } from 'path';

import { AppController } from './app.controller';
import { DatabaseModule } from './database/database.module';
import { EnvConfigModule } from './common/config/config.module';
import { envValidator } from './common/config/env-schema';
import { ExceptionFilter } from './common/exceptions/exception.filter';
import { JwtAuthGuard } from './common/guards';
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
import { HttpValidationPipe } from './common/pipes';
import { HealthModule } from './modules/health/health.module';
import Environment from './common/utils/environment';
import { DatabaseModule } from './database/database.module';
import { AuthModule } from './modules/auth/auth.module';
import { UserModule } from './modules/user/user.module';
import { CronJobModule } from './modules/cron-job/cron-job.module';
import { HealthModule } from './modules/health/health.module';
import { JobModule } from './modules/job/job.module';
import { PaymentModule } from './modules/payment/payment.module';
import { Web3Module } from './modules/web3/web3.module';
import { envValidator } from './common/config/env-schema';
import { QualificationModule } from './modules/qualification/qualification.module';
import { StatisticModule } from './modules/statistic/statistic.module';
import { StorageModule } from './modules/storage/storage.module';
import { CronJobModule } from './modules/cron-job/cron-job.module';
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
import { UserModule } from './modules/user/user.module';
import { Web3Module } from './modules/web3/web3.module';
import { WebhookModule } from './modules/webhook/webhook.module';
import { EnvConfigModule } from './common/config/config.module';
import { ExceptionFilter } from './common/exceptions/exception.filter';
import { StatisticModule } from './modules/statistic/statistic.module';
import { QualificationModule } from './modules/qualification/qualification.module';
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
import Environment from './common/utils/environment';

@Module({
providers: [
Expand All @@ -49,8 +49,20 @@ import Environment from './common/utils/environment';
provide: APP_FILTER,
useClass: ExceptionFilter,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
imports: [
ThrottlerModule.forRoot({
throttlers: [
{
ttl: 60000,
limit: 1000,
},
],
}),
ScheduleModule.forRoot(),
ConfigModule.forRoot({
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import {
UseGuards,
UseInterceptors,
} from '@nestjs/common';

import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { ErrorAuth } from '../../common/constants/errors';
import { Public } from '../../common/decorators';
import { ValidationError } from '../../common/errors';
Expand Down Expand Up @@ -144,6 +144,7 @@ export class AuthJwtController {

@Public()
@HttpCode(204)
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Post('/forgot-password')
@ApiOperation({
summary: 'Forgot Password',
Expand All @@ -162,8 +163,11 @@ export class AuthJwtController {
status: 404,
description: 'Not Found. Could not find the requested content.',
})
public async forgotPassword(@Body() data: ForgotPasswordDto): Promise<void> {
await this.authService.forgotPassword(data);
public async forgotPassword(
@Body() data: ForgotPasswordDto,
@Ip() ip: string,
): Promise<void> {
await this.authService.forgotPassword(data, ip);
}

@Public()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export class ForgotPasswordDto {
@IsEmail()
@Transform(({ value }: { value: string }) => value.toLowerCase())
public email: string;

@ApiProperty({ name: 'h_captcha_token' })
@IsString()
public hCaptchaToken: string;
}

export class SignInDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,21 +330,30 @@ describe('AuthService', () => {
it('should throw NotFoundError if user is not found', () => {
findByEmailMock.mockResolvedValue(null);
expect(
authService.forgotPassword({ email: '[email protected]' }),
authService.forgotPassword({
email: '[email protected]',
hCaptchaToken: 'token',
}),
).rejects.toThrow(new NotFoundError(ErrorUser.NotFound));
});

it('should throw ForbiddenError if user is not active', () => {
userEntity.status = UserStatus.INACTIVE;
findByEmailMock.mockResolvedValue(userEntity);
expect(
authService.forgotPassword({ email: '[email protected]' }),
authService.forgotPassword({
email: '[email protected]',
hCaptchaToken: 'token',
}),
).rejects.toThrow(new ForbiddenError(ErrorUser.UserNotActive));
});

it('should remove existing token if it exists', async () => {
findTokenMock.mockResolvedValue(tokenEntity);
await authService.forgotPassword({ email: '[email protected]' });
await authService.forgotPassword({
email: '[email protected]',
hCaptchaToken: 'token',
});

expect(tokenRepository.deleteOne).toHaveBeenCalled();
});
Expand All @@ -353,7 +362,7 @@ describe('AuthService', () => {
sendGridService.sendEmail = jest.fn();
const email = '[email protected]';

await authService.forgotPassword({ email });
await authService.forgotPassword({ email, hCaptchaToken: 'token' });

expect(sendGridService.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,23 @@ export class AuthService {
return { accessToken, refreshToken: newRefreshTokenEntity.uuid };
}

public async forgotPassword(data: ForgotPasswordDto): Promise<void> {
public async forgotPassword(
data: ForgotPasswordDto,
ip?: string,
): Promise<void> {
if (
!(
await verifyToken(
this.authConfigService.hcaptchaProtectionUrl,
this.authConfigService.hCaptchaSiteKey,
this.authConfigService.hCaptchaSecret,
data.hCaptchaToken,
ip,
)
).success
) {
throw new ForbiddenError(ErrorAuth.InvalidCaptchaToken);
}
const userEntity = await this.userRepository.findByEmail(data.email);

if (!userEntity) {
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4426,6 +4426,7 @@ __metadata:
"@nestjs/swagger": "npm:^7.4.2"
"@nestjs/terminus": "npm:^11.0.0"
"@nestjs/testing": "npm:^10.4.6"
"@nestjs/throttler": "npm:^6.4.0"
"@nestjs/typeorm": "npm:^10.0.1"
"@sendgrid/mail": "npm:^8.1.3"
"@types/bcrypt": "npm:^5.0.2"
Expand Down Expand Up @@ -6698,6 +6699,17 @@ __metadata:
languageName: node
linkType: hard

"@nestjs/throttler@npm:^6.4.0":
version: 6.4.0
resolution: "@nestjs/throttler@npm:6.4.0"
peerDependencies:
"@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
"@nestjs/core": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
reflect-metadata: ^0.1.13 || ^0.2.0
checksum: 10c0/796134644e341aad4a403b7431524db97adc31ae8771fc1160a4694a24c295b7a3dd15abcb72b9ea3a0702247b929f501fc5dc74a3f30d915f2667a39ba5c5d7
languageName: node
linkType: hard

"@nestjs/typeorm@npm:^10.0.1":
version: 10.0.2
resolution: "@nestjs/typeorm@npm:10.0.2"
Expand Down
Loading