fix: migrate driver modules to DriverFactoryBase lazy-loading pattern (#18731)
## Summary - Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and `CaptchaModule` from the `forRootAsync` + injection token pattern to the `DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and `FileStorageModule`) - Fixes #18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker processes because the driver was created at module boot time before the DB config cache was loaded - Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`, `CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`, and `FRONTEND_URL` — these can now be safely configured via the database at runtime ## How it works Each migrated module now uses a `DriverFactory` (extending `DriverFactoryBase`) instead of a module-level async factory + Symbol injection token: 1. **Lazy creation**: `getCurrentDriver()` creates the driver on first call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB cache 2. **Auto-recreation**: If config changes in the DB, the next `getCurrentDriver()` call detects the key mismatch and creates a new driver instance 3. **Unified config**: Both server and worker read from the same database — driver config only needs to be set once ### Files deleted (old pattern) - `logic-function-module.factory.ts`, `logic-function-drivers.module.ts`, `logic-function-driver.constants.ts` - `code-interpreter-module.factory.ts` - `captcha.module-factory.ts`, `captcha-driver.constants.ts` ### Files created (new pattern) - `logic-function-driver.factory.ts` - `code-interpreter-driver.factory.ts` - `captcha-driver.factory.ts` Net: **-150 lines** ## Test plan - [x] `npx nx typecheck twenty-server` passes - [x] `npx nx lint:diff-with-main twenty-server` passes - [ ] Integration tests pass (`npx nx run twenty-server:test:integration:with-db-reset`) - [ ] Verify logic functions execute in workflow runs (the original bug) - [ ] Verify code interpreter works in workflow code steps - [ ] Verify captcha validation works on sign-up (when captcha is configured) Made with [Cursor](https://cursor.com)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
|
||||
import { GoogleRecaptchaDriver } from 'src/engine/core-modules/captcha/drivers/google-recaptcha.driver';
|
||||
import { TurnstileDriver } from 'src/engine/core-modules/captcha/drivers/turnstile.driver';
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaDriverFactory extends DriverFactoryBase<CaptchaDriver | null> {
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
) {
|
||||
super(twentyConfigService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
const driver = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
||||
|
||||
if (!driver) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
return `${driver}|${this.getConfigGroupHash(ConfigVariablesGroup.CAPTCHA_CONFIG)}`;
|
||||
}
|
||||
|
||||
protected createDriver(): CaptchaDriver | null {
|
||||
const driver = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
||||
const siteKey = this.twentyConfigService.get('CAPTCHA_SITE_KEY');
|
||||
const secretKey = this.twentyConfigService.get('CAPTCHA_SECRET_KEY');
|
||||
|
||||
if (!driver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!siteKey || !secretKey) {
|
||||
throw new Error('Captcha driver requires site key and secret key');
|
||||
}
|
||||
|
||||
const captchaOptions = { siteKey, secretKey };
|
||||
|
||||
switch (driver) {
|
||||
case CaptchaDriverType.GOOGLE_RECAPTCHA:
|
||||
return new GoogleRecaptchaDriver(
|
||||
captchaOptions,
|
||||
this.secureHttpClientService.getHttpClient({
|
||||
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
|
||||
}),
|
||||
);
|
||||
|
||||
case CaptchaDriverType.TURNSTILE:
|
||||
return new TurnstileDriver(
|
||||
captchaOptions,
|
||||
this.secureHttpClientService.getHttpClient({
|
||||
baseURL:
|
||||
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
}),
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid captcha driver type: ${driver}`);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentDriver(): CaptchaDriver | null {
|
||||
const driver = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
||||
|
||||
if (!driver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return super.getCurrentDriver();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import {
|
||||
type CaptchaDriverOptions,
|
||||
type CaptchaModuleOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
export const captchaModuleFactory = (
|
||||
twentyConfigService: TwentyConfigService,
|
||||
): CaptchaModuleOptions | undefined => {
|
||||
const driver = twentyConfigService.get('CAPTCHA_DRIVER');
|
||||
const siteKey = twentyConfigService.get('CAPTCHA_SITE_KEY');
|
||||
const secretKey = twentyConfigService.get('CAPTCHA_SECRET_KEY');
|
||||
|
||||
if (!driver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!siteKey || !secretKey) {
|
||||
throw new Error('Captcha driver requires site key and secret key');
|
||||
}
|
||||
|
||||
const captchaOptions: CaptchaDriverOptions = {
|
||||
siteKey,
|
||||
secretKey,
|
||||
};
|
||||
|
||||
return {
|
||||
type: driver,
|
||||
options: captchaOptions,
|
||||
};
|
||||
};
|
||||
@@ -1,59 +1,17 @@
|
||||
import { type DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { CaptchaDriverFactory } from 'src/engine/core-modules/captcha/captcha-driver.factory';
|
||||
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
|
||||
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
|
||||
import { GoogleRecaptchaDriver } from 'src/engine/core-modules/captcha/drivers/google-recaptcha.driver';
|
||||
import { TurnstileDriver } from 'src/engine/core-modules/captcha/drivers/turnstile.driver';
|
||||
import {
|
||||
CaptchaDriverType,
|
||||
type CaptchaModuleAsyncOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
|
||||
@Global()
|
||||
export class CaptchaModule {
|
||||
static forRoot(options: CaptchaModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: CAPTCHA_DRIVER,
|
||||
useFactory: async (
|
||||
secureHttpClientService: SecureHttpClientService,
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
...args: any[]
|
||||
) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case CaptchaDriverType.GOOGLE_RECAPTCHA:
|
||||
return new GoogleRecaptchaDriver(
|
||||
config.options,
|
||||
secureHttpClientService.getHttpClient({
|
||||
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
|
||||
}),
|
||||
);
|
||||
case CaptchaDriverType.TURNSTILE:
|
||||
return new TurnstileDriver(
|
||||
config.options,
|
||||
secureHttpClientService.getHttpClient({
|
||||
baseURL:
|
||||
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
}),
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
inject: [SecureHttpClientService, ...(options.inject || [])],
|
||||
};
|
||||
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: CaptchaModule,
|
||||
imports: [SecureHttpClientModule],
|
||||
providers: [CaptchaService, provider],
|
||||
imports: [TwentyConfigModule, SecureHttpClientModule],
|
||||
providers: [CaptchaDriverFactory, CaptchaService],
|
||||
exports: [CaptchaService],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
|
||||
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
|
||||
import { CaptchaDriverFactory } from 'src/engine/core-modules/captcha/captcha-driver.factory';
|
||||
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
import { type CaptchaValidateResult } from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaService implements CaptchaDriver {
|
||||
constructor(@Inject(CAPTCHA_DRIVER) private driver: CaptchaDriver) {}
|
||||
constructor(private readonly captchaDriverFactory: CaptchaDriverFactory) {}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
if (this.driver) {
|
||||
return await this.driver.validate(token);
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
const driver = this.captchaDriverFactory.getCurrentDriver();
|
||||
|
||||
if (!driver) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return driver.validate(token);
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const CAPTCHA_DRIVER = Symbol('CAPTCHA_DRIVER');
|
||||
-22
@@ -1,4 +1,3 @@
|
||||
import { type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum CaptchaDriverType {
|
||||
@@ -15,25 +14,4 @@ export type CaptchaDriverOptions = {
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
export interface GoogleRecaptchaDriverFactoryOptions {
|
||||
type: CaptchaDriverType.GOOGLE_RECAPTCHA;
|
||||
options: CaptchaDriverOptions;
|
||||
}
|
||||
|
||||
export interface TurnstileDriverFactoryOptions {
|
||||
type: CaptchaDriverType.TURNSTILE;
|
||||
options: CaptchaDriverOptions;
|
||||
}
|
||||
|
||||
export type CaptchaModuleOptions =
|
||||
| GoogleRecaptchaDriverFactoryOptions
|
||||
| TurnstileDriverFactoryOptions;
|
||||
|
||||
export type CaptchaModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
|
||||
...args: any[]
|
||||
) => CaptchaModuleOptions | Promise<CaptchaModuleOptions> | undefined;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
export type CaptchaValidateResult = { success: boolean; error?: string };
|
||||
|
||||
Reference in New Issue
Block a user