Skip to content

Commit 24b9880

Browse files
committed
enhance forgot password flow with hCaptcha validation
1 parent 82d55a3 commit 24b9880

File tree

7 files changed

+48
-10
lines changed

7 files changed

+48
-10
lines changed

packages/apps/job-launcher/client/src/components/Auth/ForgotPasswordForm.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export const ForgotPasswordForm = () => {
2626
const handleForgotPassword = async ({ email }) => {
2727
setIsLoading(true);
2828
try {
29-
await authService.forgotPassword(email);
29+
const hCaptchaToken = await captchaRef.current.getResponse();
30+
await authService.forgotPassword({
31+
email,
32+
hCaptchaToken,
33+
});
3034
setIsSuccess(true);
3135
} catch (err) {
3236
showError(err);

packages/apps/job-launcher/client/src/services/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ForgotPasswordRequest,
23
ResetPasswordRequest,
34
SignInRequest,
45
SignUpRequest,
@@ -24,8 +25,8 @@ export const signOut = async (refreshToken: string) => {
2425
return data;
2526
};
2627

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

3132
export const resetPassword = async (body: ResetPasswordRequest) => {

packages/apps/job-launcher/client/src/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export type SignUpResponse = {
1717
refreshToken: string;
1818
};
1919

20+
export type ForgotPasswordRequest = {
21+
email: string;
22+
hCaptchaToken: string;
23+
};
24+
2025
export type ResetPasswordRequest = {
2126
password: string;
2227
token: string;

packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ export class AuthJwtController {
166166
@Body() data: ForgotPasswordDto,
167167
@Ip() ip: string,
168168
): Promise<void> {
169-
console.log('IP:', ip);
170-
await this.authService.forgotPassword(data);
169+
await this.authService.forgotPassword(data, ip);
171170
}
172171

173172
@Public()

packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export class ForgotPasswordDto {
1313
@IsEmail()
1414
@Transform(({ value }: { value: string }) => value.toLowerCase())
1515
public email: string;
16+
17+
@ApiProperty({ name: 'h_captcha_token' })
18+
@IsString()
19+
public hCaptchaToken: string;
1620
}
1721

1822
export class SignInDto {

packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,21 +330,30 @@ describe('AuthService', () => {
330330
it('should throw NotFoundError if user is not found', () => {
331331
findByEmailMock.mockResolvedValue(null);
332332
expect(
333-
authService.forgotPassword({ email: '[email protected]' }),
333+
authService.forgotPassword({
334+
335+
hCaptchaToken: 'token',
336+
}),
334337
).rejects.toThrow(new NotFoundError(ErrorUser.NotFound));
335338
});
336339

337340
it('should throw ForbiddenError if user is not active', () => {
338341
userEntity.status = UserStatus.INACTIVE;
339342
findByEmailMock.mockResolvedValue(userEntity);
340343
expect(
341-
authService.forgotPassword({ email: '[email protected]' }),
344+
authService.forgotPassword({
345+
346+
hCaptchaToken: 'token',
347+
}),
342348
).rejects.toThrow(new ForbiddenError(ErrorUser.UserNotActive));
343349
});
344350

345351
it('should remove existing token if it exists', async () => {
346352
findTokenMock.mockResolvedValue(tokenEntity);
347-
await authService.forgotPassword({ email: '[email protected]' });
353+
await authService.forgotPassword({
354+
355+
hCaptchaToken: 'token',
356+
});
348357

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

356-
await authService.forgotPassword({ email });
365+
await authService.forgotPassword({ email, hCaptchaToken: 'token' });
357366

358367
expect(sendGridService.sendEmail).toHaveBeenCalledWith(
359368
expect.objectContaining({

packages/apps/job-launcher/server/src/modules/auth/auth.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,23 @@ export class AuthService {
181181
return { accessToken, refreshToken: newRefreshTokenEntity.uuid };
182182
}
183183

184-
public async forgotPassword(data: ForgotPasswordDto): Promise<void> {
184+
public async forgotPassword(
185+
data: ForgotPasswordDto,
186+
ip?: string,
187+
): Promise<void> {
188+
if (
189+
!(
190+
await verifyToken(
191+
this.authConfigService.hcaptchaProtectionUrl,
192+
this.authConfigService.hCaptchaSiteKey,
193+
this.authConfigService.hCaptchaSecret,
194+
data.hCaptchaToken,
195+
ip,
196+
)
197+
).success
198+
) {
199+
throw new ForbiddenError(ErrorAuth.InvalidCaptchaToken);
200+
}
185201
const userEntity = await this.userRepository.findByEmail(data.email);
186202

187203
if (!userEntity) {

0 commit comments

Comments
 (0)