diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..042649a --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +## GOOGLE RECAPTCHA +GG_RECAPTCHA_SECRET_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 22f55ad..b526704 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json +.env \ No newline at end of file diff --git a/package.json b/package.json index bdcd832..2232a2b 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,16 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/cache-manager": "^2.1.1", "@nestjs/common": "^10.0.0", - "@nestjs/config": "^3.1.1", + "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/microservices": "^10.2.8", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.1", - "axios": "^1.6.1", + "axios": "^1.6.7", "bcrypt": "^5.1.1", "cache-manager": "^5.3.1", "cache-manager-redis-store": "^3.0.1", diff --git a/src/app.module.ts b/src/app.module.ts index 4694ef9..f2bddf3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { Customer } from './entity/customer.entity'; import { ConfigModule } from '@nestjs/config'; // import { redisStore } from 'cache-manager-redis-yet'; import { CacheModule } from '@nestjs/cache-manager'; +import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ @@ -36,6 +37,10 @@ import { CacheModule } from '@nestjs/cache-manager'; }), }), TypeOrmModule.forFeature([Customer]), + HttpModule, + ConfigModule.forRoot({ + envFilePath: ['.env'], + }), ], controllers: [AppController, TokenController, OtpController], providers: [ diff --git a/src/controller/token.controller.ts b/src/controller/token.controller.ts index d8d11dd..193a492 100644 --- a/src/controller/token.controller.ts +++ b/src/controller/token.controller.ts @@ -1,5 +1,9 @@ -import { Controller } from '@nestjs/common'; +import { Controller, HttpException } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; +import { GeneralErrorResponse } from 'src/dto/general-error-response.dto'; +import { GeneralServiceResponse } from 'src/dto/general-service-response.dto'; +import { VerifyReCaptchaRequest } from 'src/dto/verify-recaptcha-request.dto'; +import { VerifyReCaptchaResponse } from 'src/dto/verify-recaptcha-response.dto'; import { TokenService } from 'src/service/token.service'; @Controller() @@ -15,4 +19,27 @@ export class TokenController { public async refreshToken(user) { return await this.tokenService.refreshToken(user); } + + @MessagePattern({ cmd: 'verify_recaptcha' }) + public async verifyReCaptcha( + data: VerifyReCaptchaRequest, + ): Promise { + const res = new GeneralServiceResponse(); + const { verified_token } = data; + try { + const result: VerifyReCaptchaResponse = + await this.tokenService.verifyReCaptchaFromEndPoint(verified_token); + res.statusCode = 200; + res.data = result; + } catch (error) { + if (error instanceof HttpException) { + res.statusCode = error.getStatus(); + res.data = error.getResponse(); + } else { + res.statusCode = 500; + res.data = new GeneralErrorResponse(9, error.toString()); + } + } + return res; + } } diff --git a/src/dto/general-error-response.dto.ts b/src/dto/general-error-response.dto.ts new file mode 100644 index 0000000..da46c35 --- /dev/null +++ b/src/dto/general-error-response.dto.ts @@ -0,0 +1,9 @@ +export class GeneralErrorResponse { + constructor(_error_code: number = 0, _detail: any = null) { + this.error_code = _error_code; + this.detail = _detail; + return this; + } + error_code: number; + detail: any; +} diff --git a/src/dto/general-service-response.dto.ts b/src/dto/general-service-response.dto.ts new file mode 100644 index 0000000..eaba1b3 --- /dev/null +++ b/src/dto/general-service-response.dto.ts @@ -0,0 +1,9 @@ +export class GeneralServiceResponse { + constructor() { + this.statusCode = null; + this.data = null; + } + + statusCode: number; + data: any; +} diff --git a/src/dto/verify-recaptcha-request.dto.ts b/src/dto/verify-recaptcha-request.dto.ts new file mode 100644 index 0000000..8ccaf35 --- /dev/null +++ b/src/dto/verify-recaptcha-request.dto.ts @@ -0,0 +1,3 @@ +export class VerifyReCaptchaRequest { + verified_token: string; +} diff --git a/src/dto/verify-recaptcha-response.dto.ts b/src/dto/verify-recaptcha-response.dto.ts new file mode 100644 index 0000000..5badded --- /dev/null +++ b/src/dto/verify-recaptcha-response.dto.ts @@ -0,0 +1,6 @@ +export class VerifyReCaptchaResponse { + success: boolean; // true|false + challenge_ts: number; // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) + apk_package_name: string; // the package name of the app where the reCAPTCHA was solved + // error-codes: string[]; // optional +} diff --git a/src/exceptions/custom-bad-request.exception.ts b/src/exceptions/custom-bad-request.exception.ts new file mode 100644 index 0000000..3b9b689 --- /dev/null +++ b/src/exceptions/custom-bad-request.exception.ts @@ -0,0 +1,8 @@ +import { BadRequestException } from '@nestjs/common'; +import { GeneralErrorResponse } from 'src/dto/general-error-response.dto'; + +export class CustomBadRequestException extends BadRequestException { + constructor(public readonly error_response: GeneralErrorResponse) { + super(error_response); + } +} diff --git a/src/exceptions/custom-not-found.exception.ts b/src/exceptions/custom-not-found.exception.ts new file mode 100644 index 0000000..8356509 --- /dev/null +++ b/src/exceptions/custom-not-found.exception.ts @@ -0,0 +1,8 @@ +import { NotFoundException } from '@nestjs/common'; +import { GeneralErrorResponse } from 'src/dto/general-error-response.dto'; + +export class CustomNotFoundException extends NotFoundException { + constructor(public readonly error_response: GeneralErrorResponse) { + super(error_response); + } +} diff --git a/src/service/token.service.ts b/src/service/token.service.ts index a41e6ba..c687201 100644 --- a/src/service/token.service.ts +++ b/src/service/token.service.ts @@ -13,12 +13,18 @@ import { JWT_SECRET_ACCESS_TOKEN, JWT_SECRET_REFRESH_TOKEN, } from 'src/app.constants'; +import { VerifyReCaptchaResponse } from 'src/dto/verify-recaptcha-response.dto'; +import { HttpService } from '@nestjs/axios'; +import { lastValueFrom } from 'rxjs'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class TokenService { constructor( private readonly jwtService: JwtService, private readonly customerService: CustomerService, + private readonly httpService: HttpService, + private readonly configService: ConfigService, ) {} public async createToken(user: GenericUser) { const data: JwtPayload = { @@ -104,4 +110,17 @@ export class TokenService { return result; } } + + async verifyReCaptchaFromEndPoint( + captcha_value: string, + ): Promise { + const ggReCaptchaSecret = this.configService.get( + 'GG_RECAPTCHA_SECRET_KEY', + ); + const request = this.httpService.post( + `https://www.google.com/recaptcha/api/siteverify?secret=${ggReCaptchaSecret}&response=${captcha_value}`, + ); + const result = await lastValueFrom(request); + return result.data; + } } diff --git a/yarn.lock b/yarn.lock index 5643541..7adc7c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -695,6 +695,11 @@ semver "^7.3.5" tar "^6.1.11" +"@nestjs/axios@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.2.tgz#0078c101a29fb46f5c566d68a4315fddabc083ed" + integrity sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ== + "@nestjs/cache-manager@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@nestjs/cache-manager/-/cache-manager-2.1.1.tgz#abc042c6ac83400c64fd293b48ddb2dec99f6096" @@ -738,15 +743,15 @@ iterare "1.2.1" tslib "2.6.2" -"@nestjs/config@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.1.1.tgz#51e23ed84debd08afb86acf7b92bc4bf341797da" - integrity sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ== +"@nestjs/config@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.2.0.tgz#4ca70f88b0636f86741ba192dd51dd2f01eaa7c1" + integrity sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww== dependencies: - dotenv "16.3.1" + dotenv "16.4.1" dotenv-expand "10.0.0" lodash "4.17.21" - uuid "9.0.0" + uuid "9.0.1" "@nestjs/core@^10.0.0": version "10.2.8" @@ -1589,12 +1594,12 @@ asynckit@^0.4.0: resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.6.1: - version "1.6.1" - resolved "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz" - integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g== +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.4" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2351,7 +2356,12 @@ dotenv-expand@10.0.0: resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@16.3.1, dotenv@^16.0.3: +dotenv@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11" + integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ== + +dotenv@^16.0.3: version "16.3.1" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== @@ -2802,10 +2812,10 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== foreground-child@^3.1.0: version "3.1.1" @@ -5594,11 +5604,6 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - uuid@9.0.1, uuid@^9.0.0: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"