diff --git a/.editorconfig b/.editorconfig index cc04e59..e4a61f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ trim_trailing_whitespace = true [*.ts] quote_type = single -ij_typescript_use_double_quotes = true +ij_typescript_use_double_quotes = false ij_typescript_use_semicolon_after_statement = true max_line_length = 140 ij_smart_tabs = true diff --git a/README.md b/README.md index f9b895b..9e0ab08 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This package provides protection for endpoints using [reCAPTCHA](https://www.goo * [Graphql application](#usage-in-graphql-application) * [Validate in service](#validate-in-service) * [Validate in service (Enterprise)](#validate-in-service-enterprise) + * [Dynamic Recaptcha configuration](#dynamic-recaptcha-configuration) * [Error handling](#error-handling) * [Contribution](#contribution) * [License](#license) @@ -471,6 +472,57 @@ export class SomeService { } ``` +### Dynamic Recaptcha configuration +The `RecaptchaConfigRef` class provides a convenient way to modify Recaptcha validation parameters within your application. +This can be particularly useful in scenarios where the administration of Recaptcha is managed dynamically, such as by an administrator. +The class exposes methods that allow the customization of various Recaptcha options. + + +**RecaptchaConfigRef API:** + +```typescript +@Injectable() +class RecaptchaConfigRef { + // Sets the secret key for Recaptcha validation. + setSecretKey(secretKey: string): this; + + // Sets enterprise-specific options for Recaptcha validation + setEnterpriseOptions(options: GoogleRecaptchaEnterpriseOptions): this; + + // Sets the score threshold for Recaptcha validation. + setScore(score: ScoreValidator): this; + + // Sets conditions under which Recaptcha validation should be skipped. + setSkipIf(skipIf: SkipIfValue): this; +} +``` + +**Usage example:** + +```typescript +@Injectable() +export class RecaptchaAdminService implements OnApplicationBootstrap { + constructor(private readonly recaptchaConfigRef: RecaptchaConfigRef) { + } + + async onApplicationBootstrap(): Promise { + // TODO: Pull recaptcha configs from your database + + this.recaptchaConfigRef + .setSecretKey('SECRET_KEY_VALUE') + .setScore(0.3); + } + + async updateSecretKey(secretKey: string): Promise { + // TODO: Save new secret key to your database + + this.recaptchaConfigRef.setSecretKey(secretKey); + } +} +``` + +After call `this.recaptchaConfigRef.setSecretKey(...)` - `@Recaptcha` guard and `GoogleRecaptchaValidator` will use new secret key. + ### Error handling **GoogleRecaptchaException** diff --git a/src/google-recaptcha.module.ts b/src/google-recaptcha.module.ts index dd89f7c..d345849 100644 --- a/src/google-recaptcha.module.ts +++ b/src/google-recaptcha.module.ts @@ -15,6 +15,7 @@ import { Agent } from 'https'; import { RecaptchaValidatorResolver } from './services/recaptcha-validator.resolver'; import { EnterpriseReasonTransformer } from './services/enterprise-reason.transformer'; import { xor } from './helpers/xor'; +import { RecaptchaConfigRef } from './models/recaptcha-config-ref'; export class GoogleRecaptchaModule { private static axiosDefaultConfig: AxiosRequestConfig = { @@ -39,6 +40,10 @@ export class GoogleRecaptchaModule { provide: RECAPTCHA_LOGGER, useFactory: () => options.logger || new Logger(), }, + { + provide: RecaptchaConfigRef, + useFactory: () => new RecaptchaConfigRef(options), + }, ]; this.validateOptions(options); @@ -72,6 +77,11 @@ export class GoogleRecaptchaModule { useFactory: (options: GoogleRecaptchaModuleOptions) => options.logger || new Logger(), inject: [RECAPTCHA_OPTIONS], }, + { + provide: RecaptchaConfigRef, + useFactory: (opts: GoogleRecaptchaModuleOptions) => new RecaptchaConfigRef(opts), + inject: [RECAPTCHA_OPTIONS], + }, GoogleRecaptchaGuard, GoogleRecaptchaValidator, GoogleRecaptchaEnterpriseValidator, diff --git a/src/guards/google-recaptcha.guard.ts b/src/guards/google-recaptcha.guard.ts index f1c5893..cc29d75 100644 --- a/src/guards/google-recaptcha.guard.ts +++ b/src/guards/google-recaptcha.guard.ts @@ -1,15 +1,15 @@ import { CanActivate, ExecutionContext, Inject, Injectable, Logger } from '@nestjs/common'; -import { RECAPTCHA_LOGGER, RECAPTCHA_OPTIONS, RECAPTCHA_VALIDATION_OPTIONS } from '../provider.declarations'; +import { RECAPTCHA_LOGGER, RECAPTCHA_VALIDATION_OPTIONS } from '../provider.declarations'; import { GoogleRecaptchaException } from '../exceptions/google-recaptcha.exception'; import { Reflector } from '@nestjs/core'; import { RecaptchaRequestResolver } from '../services/recaptcha-request.resolver'; import { VerifyResponseDecoratorOptions } from '../interfaces/verify-response-decorator-options'; -import { GoogleRecaptchaModuleOptions } from '../interfaces/google-recaptcha-module-options'; import { RecaptchaValidatorResolver } from '../services/recaptcha-validator.resolver'; import { GoogleRecaptchaContext } from '../enums/google-recaptcha-context'; import { AbstractGoogleRecaptchaValidator } from '../services/validators/abstract-google-recaptcha-validator'; import { GoogleRecaptchaEnterpriseValidator } from '../services/validators/google-recaptcha-enterprise.validator'; import { LiteralObject } from '../interfaces/literal-object'; +import { RecaptchaConfigRef } from '../models/recaptcha-config-ref'; @Injectable() export class GoogleRecaptchaGuard implements CanActivate { @@ -18,13 +18,14 @@ export class GoogleRecaptchaGuard implements CanActivate { private readonly requestResolver: RecaptchaRequestResolver, private readonly validatorResolver: RecaptchaValidatorResolver, @Inject(RECAPTCHA_LOGGER) private readonly logger: Logger, - @Inject(RECAPTCHA_OPTIONS) private readonly options: GoogleRecaptchaModuleOptions + private readonly configRef: RecaptchaConfigRef, ) {} async canActivate(context: ExecutionContext): Promise { const request: LiteralObject = this.requestResolver.resolve(context); - const skip = typeof this.options.skipIf === 'function' ? await this.options.skipIf(request) : !!this.options.skipIf; + const skipIfValue = this.configRef.valueOf.skipIf; + const skip = typeof skipIfValue === 'function' ? await skipIfValue(request) : !!skipIfValue; if (skip) { return true; @@ -33,18 +34,18 @@ export class GoogleRecaptchaGuard implements CanActivate { const options: VerifyResponseDecoratorOptions = this.reflector.get(RECAPTCHA_VALIDATION_OPTIONS, context.getHandler()); const [response, remoteIp] = await Promise.all([ - options?.response ? await options.response(request) : await this.options.response(request), - options?.remoteIp ? await options.remoteIp(request) : await this.options.remoteIp && this.options.remoteIp(request), + options?.response ? await options.response(request) : await this.configRef.valueOf.response(request), + options?.remoteIp ? await options.remoteIp(request) : await this.configRef.valueOf.remoteIp && this.configRef.valueOf.remoteIp(request), ]); - const score = options?.score || this.options.score; + const score = options?.score || this.configRef.valueOf.score; const action = options?.action; const validator = this.validatorResolver.resolve(); request.recaptchaValidationResult = await validator.validate({ response, remoteIp, score, action }); - if (this.options.debug) { + if (this.configRef.valueOf.debug) { const loggerCtx = this.resolveLogContext(validator); this.logger.debug(request.recaptchaValidationResult.toObject(), `${loggerCtx}.result`); } diff --git a/src/index.ts b/src/index.ts index 3517bb9..649104c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,4 @@ export { GoogleRecaptchaValidator } from './services/validators/google-recaptcha export { GoogleRecaptchaEnterpriseValidator } from './services/validators/google-recaptcha-enterprise.validator'; export { RecaptchaVerificationResult } from './models/recaptcha-verification-result'; export { ClassificationReason } from './enums/classification-reason'; +export { RecaptchaConfigRef } from './models/recaptcha-config-ref'; diff --git a/src/interfaces/google-recaptcha-guard-options.ts b/src/interfaces/google-recaptcha-guard-options.ts index e2b19cc..e7b2f5b 100644 --- a/src/interfaces/google-recaptcha-guard-options.ts +++ b/src/interfaces/google-recaptcha-guard-options.ts @@ -1,8 +1,8 @@ -import { RecaptchaRemoteIpProvider, RecaptchaResponseProvider, ScoreValidator } from "../types"; +import { RecaptchaRemoteIpProvider, RecaptchaResponseProvider, ScoreValidator, SkipIfValue } from '../types'; export interface GoogleRecaptchaGuardOptions { response: RecaptchaResponseProvider; remoteIp?: RecaptchaRemoteIpProvider; - skipIf?: boolean | ((request: Req) => boolean | Promise); + skipIf?: SkipIfValue; score?: ScoreValidator; } diff --git a/src/interfaces/verify-response-decorator-options.ts b/src/interfaces/verify-response-decorator-options.ts index d6c900c..c3e7c9c 100644 --- a/src/interfaces/verify-response-decorator-options.ts +++ b/src/interfaces/verify-response-decorator-options.ts @@ -1,4 +1,4 @@ -import { RecaptchaRemoteIpProvider, RecaptchaResponseProvider, ScoreValidator } from "../types"; +import { RecaptchaRemoteIpProvider, RecaptchaResponseProvider, ScoreValidator } from '../types'; export interface VerifyResponseDecoratorOptions { response?: RecaptchaResponseProvider; diff --git a/src/models/recaptcha-config-ref.ts b/src/models/recaptcha-config-ref.ts new file mode 100644 index 0000000..444c9be --- /dev/null +++ b/src/models/recaptcha-config-ref.ts @@ -0,0 +1,38 @@ +import { GoogleRecaptchaModuleOptions } from '../interfaces/google-recaptcha-module-options'; +import { GoogleRecaptchaEnterpriseOptions } from '../interfaces/google-recaptcha-enterprise-options'; +import { ScoreValidator, SkipIfValue } from '../types'; + +export class RecaptchaConfigRef { + get valueOf(): GoogleRecaptchaModuleOptions { + return this.value; + } + + constructor(private readonly value: GoogleRecaptchaModuleOptions) { + } + + setSecretKey(secretKey: string): this { + this.value.secretKey = secretKey; + this.value.enterprise = undefined; + + return this; + } + + setEnterpriseOptions(options: GoogleRecaptchaEnterpriseOptions): this { + this.value.secretKey = undefined; + this.value.enterprise = options; + + return this; + } + + setScore(score: ScoreValidator): this { + this.value.score = score; + + return this; + } + + setSkipIf(skipIf: SkipIfValue): this { + this.value.skipIf = skipIf; + + return this; + } +} diff --git a/src/services/recaptcha-validator.resolver.ts b/src/services/recaptcha-validator.resolver.ts index 08cf419..8a0019d 100644 --- a/src/services/recaptcha-validator.resolver.ts +++ b/src/services/recaptcha-validator.resolver.ts @@ -1,24 +1,24 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { AbstractGoogleRecaptchaValidator } from './validators/abstract-google-recaptcha-validator'; -import { RECAPTCHA_OPTIONS } from '../provider.declarations'; -import { GoogleRecaptchaModuleOptions } from '../interfaces/google-recaptcha-module-options'; import { GoogleRecaptchaValidator } from './validators/google-recaptcha.validator'; import { GoogleRecaptchaEnterpriseValidator } from './validators/google-recaptcha-enterprise.validator'; +import { RecaptchaConfigRef } from '../models/recaptcha-config-ref'; @Injectable() export class RecaptchaValidatorResolver { constructor( - @Inject(RECAPTCHA_OPTIONS) private readonly options: GoogleRecaptchaModuleOptions, + private readonly configRef: RecaptchaConfigRef, protected readonly googleRecaptchaValidator: GoogleRecaptchaValidator, - protected readonly googleRecaptchaEnterpriseValidator: GoogleRecaptchaEnterpriseValidator + protected readonly googleRecaptchaEnterpriseValidator: GoogleRecaptchaEnterpriseValidator, ) {} resolve(): AbstractGoogleRecaptchaValidator { - if (this.options.secretKey) { + const configValue = this.configRef.valueOf; + if (configValue.secretKey) { return this.googleRecaptchaValidator; } - if (Object.keys(this.options.enterprise || {}).length) { + if (Object.keys(configValue.enterprise || {}).length) { return this.googleRecaptchaEnterpriseValidator; } diff --git a/src/services/validators/abstract-google-recaptcha-validator.ts b/src/services/validators/abstract-google-recaptcha-validator.ts index f511a87..5afe05c 100644 --- a/src/services/validators/abstract-google-recaptcha-validator.ts +++ b/src/services/validators/abstract-google-recaptcha-validator.ts @@ -1,10 +1,10 @@ import { VerifyResponseOptions } from '../../interfaces/verify-response-decorator-options'; import { ScoreValidator } from '../../types'; -import { GoogleRecaptchaModuleOptions } from '../../interfaces/google-recaptcha-module-options'; import { RecaptchaVerificationResult } from '../../models/recaptcha-verification-result'; +import { RecaptchaConfigRef } from '../../models/recaptcha-config-ref'; export abstract class AbstractGoogleRecaptchaValidator { - protected constructor(protected readonly options: GoogleRecaptchaModuleOptions) {} + protected constructor(protected readonly options: RecaptchaConfigRef) {} abstract validate(options: VerifyResponseOptions): Promise>; @@ -13,11 +13,11 @@ export abstract class AbstractGoogleRecaptchaValidator { return options.action === action; } - return this.options.actions ? this.options.actions.includes(action) : true; + return this.options.valueOf.actions ? this.options.valueOf.actions.includes(action) : true; } protected isValidScore(score: number, validator?: ScoreValidator): boolean { - const finalValidator = validator || this.options.score; + const finalValidator = validator || this.options.valueOf.score; if (finalValidator) { if (typeof finalValidator === 'function') { diff --git a/src/services/validators/google-recaptcha-enterprise.validator.ts b/src/services/validators/google-recaptcha-enterprise.validator.ts index 5e99a7c..af4c9f4 100644 --- a/src/services/validators/google-recaptcha-enterprise.validator.ts +++ b/src/services/validators/google-recaptcha-enterprise.validator.ts @@ -1,6 +1,5 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; -import { RECAPTCHA_AXIOS_INSTANCE, RECAPTCHA_LOGGER, RECAPTCHA_OPTIONS } from '../../provider.declarations'; -import { GoogleRecaptchaModuleOptions } from '../../interfaces/google-recaptcha-module-options'; +import { RECAPTCHA_AXIOS_INSTANCE, RECAPTCHA_LOGGER } from '../../provider.declarations'; import { VerifyResponseOptions } from '../../interfaces/verify-response-decorator-options'; import { AbstractGoogleRecaptchaValidator } from './abstract-google-recaptcha-validator'; import { RecaptchaVerificationResult } from '../../models/recaptcha-verification-result'; @@ -13,6 +12,7 @@ import { EnterpriseReasonTransformer } from '../enterprise-reason.transformer'; import { getErrorInfo } from '../../helpers/get-error-info'; import { AxiosInstance } from 'axios'; import { LiteralObject } from '../../interfaces/literal-object'; +import { RecaptchaConfigRef } from '../../models/recaptcha-config-ref'; type VerifyResponse = [VerifyResponseEnterprise, LiteralObject]; @@ -23,10 +23,10 @@ export class GoogleRecaptchaEnterpriseValidator extends AbstractGoogleRecaptchaV constructor( @Inject(RECAPTCHA_AXIOS_INSTANCE) private readonly axios: AxiosInstance, @Inject(RECAPTCHA_LOGGER) private readonly logger: Logger, - @Inject(RECAPTCHA_OPTIONS) options: GoogleRecaptchaModuleOptions, + configRef: RecaptchaConfigRef, private readonly enterpriseReasonTransformer: EnterpriseReasonTransformer ) { - super(options); + super(configRef); } async validate(options: VerifyResponseOptions): Promise> { @@ -73,11 +73,11 @@ export class GoogleRecaptchaEnterpriseValidator extends AbstractGoogleRecaptchaV } private verifyResponse(response: string, expectedAction: string, remoteIp: string): Promise { - const projectId = this.options.enterprise.projectId; + const projectId = this.options.valueOf.enterprise.projectId; const body: { event: VerifyTokenEnterpriseEvent } = { event: { expectedAction, - siteKey: this.options.enterprise.siteKey, + siteKey: this.options.valueOf.enterprise.siteKey, token: response, userIpAddress: remoteIp, }, @@ -88,25 +88,25 @@ export class GoogleRecaptchaEnterpriseValidator extends AbstractGoogleRecaptchaV const config: axios.AxiosRequestConfig = { headers: this.headers, params: { - key: this.options.enterprise.apiKey, + key: this.options.valueOf.enterprise.apiKey, }, }; - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug({ body }, `${GoogleRecaptchaContext.GoogleRecaptchaEnterprise}.request`); } return this.axios.post(url, body, config) .then((res) => res.data) .then((data): VerifyResponse => { - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug(data, `${GoogleRecaptchaContext.GoogleRecaptchaEnterprise}.response`); } return [data, null]; }) .catch((err: axios.AxiosError): VerifyResponse => { - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug(getErrorInfo(err), `${GoogleRecaptchaContext.GoogleRecaptchaEnterprise}.error`); } diff --git a/src/services/validators/google-recaptcha.validator.ts b/src/services/validators/google-recaptcha.validator.ts index 8d3bf7a..ebbae67 100644 --- a/src/services/validators/google-recaptcha.validator.ts +++ b/src/services/validators/google-recaptcha.validator.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; -import { RECAPTCHA_AXIOS_INSTANCE, RECAPTCHA_LOGGER, RECAPTCHA_OPTIONS } from '../../provider.declarations'; +import { RECAPTCHA_AXIOS_INSTANCE, RECAPTCHA_LOGGER } from '../../provider.declarations'; import * as qs from 'querystring'; import * as axios from 'axios'; import { GoogleRecaptchaNetwork } from '../../enums/google-recaptcha-network'; @@ -7,12 +7,12 @@ import { VerifyResponseOptions } from '../../interfaces/verify-response-decorato import { VerifyResponseV2, VerifyResponseV3 } from '../../interfaces/verify-response'; import { ErrorCode } from '../../enums/error-code'; import { GoogleRecaptchaNetworkException } from '../../exceptions/google-recaptcha-network.exception'; -import { GoogleRecaptchaModuleOptions } from '../../interfaces/google-recaptcha-module-options'; import { AbstractGoogleRecaptchaValidator } from './abstract-google-recaptcha-validator'; import { RecaptchaVerificationResult } from '../../models/recaptcha-verification-result'; import { GoogleRecaptchaContext } from '../../enums/google-recaptcha-context'; import { getErrorInfo } from '../../helpers/get-error-info'; import { AxiosInstance } from 'axios'; +import { RecaptchaConfigRef } from "../../models/recaptcha-config-ref"; @Injectable() export class GoogleRecaptchaValidator extends AbstractGoogleRecaptchaValidator { @@ -23,9 +23,9 @@ export class GoogleRecaptchaValidator extends AbstractGoogleRecaptchaValidator(response: string, remoteIp?: string): Promise { - const body = qs.stringify({ secret: this.options.secretKey, response, remoteip: remoteIp }); - const url = this.options.network || this.defaultNetwork; + const body = qs.stringify({ secret: this.options.valueOf.secretKey, response, remoteip: remoteIp }); + const url = this.options.valueOf.network || this.defaultNetwork; const config: axios.AxiosRequestConfig = { headers: this.headers, }; - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug({ body }, `${GoogleRecaptchaContext.GoogleRecaptcha}.request`); } return this.axios.post(url, body, config) .then((res) => res.data) .then((data) => { - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug(data, `${GoogleRecaptchaContext.GoogleRecaptcha}.response`); } @@ -101,7 +101,7 @@ export class GoogleRecaptchaValidator extends AbstractGoogleRecaptchaValidator { - if (this.options.debug) { + if (this.options.valueOf.debug) { this.logger.debug(getErrorInfo(err), `${GoogleRecaptchaContext.GoogleRecaptcha}.error`); } diff --git a/src/types.ts b/src/types.ts index 4cddc31..1d93559 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,4 +6,6 @@ export type RecaptchaRemoteIpProvider = (req) => string | Promise; export type ScoreValidator = number | ((score: number) => boolean); +export type SkipIfValue = boolean | ((request: Req) => boolean | Promise); + export type RecaptchaContextType = ContextType | 'graphql'; diff --git a/test/google-recaptcha-guard.spec.ts b/test/google-recaptcha-guard.spec.ts index b1c2cb7..0fd87bf 100644 --- a/test/google-recaptcha-guard.spec.ts +++ b/test/google-recaptcha-guard.spec.ts @@ -10,6 +10,7 @@ import { RecaptchaRequestResolver } from '../src/services/recaptcha-request.reso import { Logger } from '@nestjs/common'; import { createGoogleRecaptchaEnterpriseValidator } from './helpers/create-google-recaptcha-enterprise-validator'; import { RecaptchaValidatorResolver } from '../src/services/recaptcha-validator.resolver'; +import { RecaptchaConfigRef } from '../src/models/recaptcha-config-ref'; describe('Google recaptcha guard', () => { let network: TestRecaptchaNetwork; @@ -35,12 +36,16 @@ describe('Google recaptcha guard', () => { const options = { ...validatorOptions, ...guardOptions }; const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); + const configRef = new RecaptchaConfigRef(options); + const validatorResolver = new RecaptchaValidatorResolver(configRef, validator, enterpriseValidator); - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), { - ...options, - skipIf: true, - }); + const guard = new GoogleRecaptchaGuard( + new Reflector(), + new RecaptchaRequestResolver(), + validatorResolver, + new Logger(), + new RecaptchaConfigRef({ ...options, skipIf: true }), + ); const context = createExecutionContext(controller.submit, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); @@ -53,12 +58,19 @@ describe('Google recaptcha guard', () => { const options = { ...validatorOptions, ...guardOptions }; const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); - - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), { - ...options, - skipIf: (): boolean => true, - }); + const validatorResolver = new RecaptchaValidatorResolver( + new RecaptchaConfigRef(options), + validator, + enterpriseValidator, + ); + + const guard = new GoogleRecaptchaGuard( + new Reflector(), + new RecaptchaRequestResolver(), + validatorResolver, + new Logger(), + new RecaptchaConfigRef({ ...options, skipIf: (): boolean => true }), + ); const context = createExecutionContext(controller.submitOverridden.prototype, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); @@ -71,9 +83,19 @@ describe('Google recaptcha guard', () => { const options = { ...validatorOptions, ...guardOptions }; const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); - - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), options); + const validatorResolver = new RecaptchaValidatorResolver( + new RecaptchaConfigRef(options), + validator, + enterpriseValidator, + ); + + const guard = new GoogleRecaptchaGuard( + new Reflector(), + new RecaptchaRequestResolver(), + validatorResolver, + new Logger(), + new RecaptchaConfigRef(options), + ); const context = createExecutionContext(controller.submit, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); @@ -92,12 +114,19 @@ describe('Google recaptcha guard', () => { const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); - - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), { - ...guardOptions, - ...validatorOptions, - }); + const validatorResolver = new RecaptchaValidatorResolver( + new RecaptchaConfigRef(options), + validator, + enterpriseValidator, + ); + + const guard = new GoogleRecaptchaGuard( + new Reflector(), + new RecaptchaRequestResolver(), + validatorResolver, + new Logger(), + new RecaptchaConfigRef({ ...guardOptions, ...validatorOptions }), + ); const context = createExecutionContext(controller.submit, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); @@ -119,9 +148,9 @@ describe('Google recaptcha guard', () => { const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); + const validatorResolver = new RecaptchaValidatorResolver(new RecaptchaConfigRef(options), validator, enterpriseValidator); - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), options); + const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), new RecaptchaConfigRef(options)); const context = createExecutionContext(controller.submit, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); @@ -135,9 +164,13 @@ describe('Google recaptcha guard', () => { const validator = createGoogleRecaptchaValidator(options); const enterpriseValidator = createGoogleRecaptchaEnterpriseValidator(options); - const validatorResolver = new RecaptchaValidatorResolver(options, validator, enterpriseValidator); + const validatorResolver = new RecaptchaValidatorResolver( + new RecaptchaConfigRef(options), + validator, + enterpriseValidator, + ); - const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), options); + const guard = new GoogleRecaptchaGuard(new Reflector(), new RecaptchaRequestResolver(), validatorResolver, new Logger(), new RecaptchaConfigRef(options)); const context = createExecutionContext(controller.submit, { body: { recaptcha: 'RECAPTCHA_TOKEN' } }); diff --git a/test/helpers/create-google-recaptcha-enterprise-validator.ts b/test/helpers/create-google-recaptcha-enterprise-validator.ts index ec97f7a..95dbcbc 100644 --- a/test/helpers/create-google-recaptcha-enterprise-validator.ts +++ b/test/helpers/create-google-recaptcha-enterprise-validator.ts @@ -2,7 +2,13 @@ import { Logger } from '@nestjs/common'; import { GoogleRecaptchaEnterpriseValidator, GoogleRecaptchaModuleOptions } from '../../src'; import { EnterpriseReasonTransformer } from '../../src/services/enterprise-reason.transformer'; import axios from 'axios'; +import { RecaptchaConfigRef } from '../../src/models/recaptcha-config-ref'; export function createGoogleRecaptchaEnterpriseValidator(options: GoogleRecaptchaModuleOptions): GoogleRecaptchaEnterpriseValidator { - return new GoogleRecaptchaEnterpriseValidator(axios.create(), new Logger(), options, new EnterpriseReasonTransformer()); + return new GoogleRecaptchaEnterpriseValidator( + axios.create(), + new Logger(), + new RecaptchaConfigRef(options), + new EnterpriseReasonTransformer(), + ); } diff --git a/test/helpers/create-google-recaptcha-validator.ts b/test/helpers/create-google-recaptcha-validator.ts index bff8f6f..9044143 100644 --- a/test/helpers/create-google-recaptcha-validator.ts +++ b/test/helpers/create-google-recaptcha-validator.ts @@ -2,7 +2,12 @@ import { GoogleRecaptchaValidator } from '../../src/services/validators/google-r import { Logger } from '@nestjs/common'; import { GoogleRecaptchaModuleOptions } from '../../src'; import axios from 'axios'; +import { RecaptchaConfigRef } from '../../src/models/recaptcha-config-ref'; export function createGoogleRecaptchaValidator(options: GoogleRecaptchaModuleOptions): GoogleRecaptchaValidator { - return new GoogleRecaptchaValidator(axios.create(options.axiosConfig), new Logger(), options); + return new GoogleRecaptchaValidator( + axios.create(options.axiosConfig), + new Logger(), + new RecaptchaConfigRef(options), + ); } diff --git a/test/recaptcha-config-ref.spec.ts b/test/recaptcha-config-ref.spec.ts new file mode 100644 index 0000000..f2ab523 --- /dev/null +++ b/test/recaptcha-config-ref.spec.ts @@ -0,0 +1,61 @@ +import { GoogleRecaptchaModuleOptions } from '../src'; +import { RecaptchaConfigRef } from '../src/models/recaptcha-config-ref'; +import { GoogleRecaptchaEnterpriseOptions } from '../src/interfaces/google-recaptcha-enterprise-options'; + +describe('RecaptchaConfigRef', () => { + const options: GoogleRecaptchaModuleOptions = { + secretKey: 'SECRET', + response: () => 'RESPONSE', + }; + + test('setSecretKey', () => { + const ref = new RecaptchaConfigRef(options); + expect(ref.valueOf.secretKey).toBe(options.secretKey); + + ref.setSecretKey('NEW_ONE'); + expect(ref.valueOf.secretKey).toBe('NEW_ONE'); + expect(options.secretKey).toBe('NEW_ONE'); + + expect(ref.valueOf.enterprise).toBeUndefined(); + }); + + test('setSecretKey', () => { + const ref = new RecaptchaConfigRef(options); + expect(ref.valueOf.secretKey).toBe(options.secretKey); + + const eOpts: GoogleRecaptchaEnterpriseOptions = { + apiKey: 'e_api_key', + projectId: 'e_project_id', + siteKey: 'e_site_key', + }; + + ref.setEnterpriseOptions(eOpts); + expect(ref.valueOf.enterprise.apiKey).toBe(eOpts.apiKey); + expect(ref.valueOf.enterprise.projectId).toBe(eOpts.projectId); + expect(ref.valueOf.enterprise.siteKey).toBe(eOpts.siteKey); + expect(ref.valueOf.secretKey).toBeUndefined(); + + expect(options.enterprise.apiKey).toBe(eOpts.apiKey); + expect(options.enterprise.projectId).toBe(eOpts.projectId); + expect(options.enterprise.siteKey).toBe(eOpts.siteKey); + expect(options.secretKey).toBeUndefined(); + }); + + test('setScore', () => { + const ref = new RecaptchaConfigRef(options); + expect(ref.valueOf.secretKey).toBe(options.secretKey); + + ref.setScore(0.5); + expect(ref.valueOf.score).toBe(0.5); + expect(options.score).toBe(0.5); + }); + + test('setSkipIf', () => { + const ref = new RecaptchaConfigRef(options); + expect(ref.valueOf.secretKey).toBe(options.secretKey); + + ref.setSkipIf(true); + expect(ref.valueOf.skipIf).toBeTruthy(); + expect(options.skipIf).toBeTruthy(); + }); +}); diff --git a/test/recaptcha-validator-resolver.spec.ts b/test/recaptcha-validator-resolver.spec.ts index d5c0f44..efaaff0 100644 --- a/test/recaptcha-validator-resolver.spec.ts +++ b/test/recaptcha-validator-resolver.spec.ts @@ -1,12 +1,13 @@ import { GoogleRecaptchaEnterpriseValidator, GoogleRecaptchaModuleOptions, GoogleRecaptchaValidator } from '../src'; import { RecaptchaValidatorResolver } from '../src/services/recaptcha-validator.resolver'; +import { RecaptchaConfigRef } from '../src/models/recaptcha-config-ref'; describe('RecaptchaValidatorResolver', () => { const validator = new GoogleRecaptchaValidator(null, null, null); const enterpriseValidator = new GoogleRecaptchaEnterpriseValidator(null, null, null, null); const createResolver = (options: GoogleRecaptchaModuleOptions) => - new RecaptchaValidatorResolver(options, validator, enterpriseValidator); + new RecaptchaValidatorResolver(new RecaptchaConfigRef(options), validator, enterpriseValidator); test('resolve', () => { const moduleOptions: GoogleRecaptchaModuleOptions = { diff --git a/tsconfig.json b/tsconfig.json index d05bf68..f248b2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "experimentalDecorators": true, "declarationMap": false, "target": "es2017", - "sourceMap": true, + "sourceMap": false, "outDir": "./dist", "baseUrl": "./src", "rootDir": "./src"