## Context
Introduces a middleware that automatically sets the workspace auth
context in AsyncLocalStorage for HTTP requests, making it available
throughout the request lifecycle without explicit parameter passing.
The motivation behind this change is to reduce boilerplate and simplify
the developer experience when working with workspace data in HTTP
request handlers.
The Problem (Before)
Every HTTP request handler that needed to access workspace data had to:
- Extract auth-related info from decorators (@AuthWorkspace(),
@AuthUserWorkspaceId(), etc.) in controller/resolver and pass down to
services
- Build or pass the authContext explicitly (sometimes with type
assertion which was flaky)
Then call executeInWorkspaceContext(authContext, async () => { ... })
## Changes
- Add WorkspaceAuthContextMiddleware that extracts auth context from the
request and stores it in AsyncLocalStorage
- Register middleware for GraphQL, metadata, and REST routes (runs after
hydration middlewares)
- Simplify executeInWorkspaceContext signature: fn is now the first
parameter, authContext is optional second
- If authContext is not provided, it's automatically retrieved from the
storage (set by middleware)
- Update all callers (~120 files) to use the new parameter order
- Fixes a bug in search where system auth context was used, bypassing
RLS feature.
135 lines
4.9 KiB
TypeScript
135 lines
4.9 KiB
TypeScript
import {
|
|
type DynamicModule,
|
|
type MiddlewareConsumer,
|
|
Module,
|
|
RequestMethod,
|
|
} from '@nestjs/common';
|
|
import { GraphQLModule } from '@nestjs/graphql';
|
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
|
|
|
import { existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
import { YogaDriver, type YogaDriverConfig } from '@graphql-yoga/nestjs';
|
|
import { SentryModule } from '@sentry/nestjs/setup';
|
|
|
|
import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module';
|
|
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
|
|
import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
|
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
|
import { McpModule } from 'src/engine/api/mcp/mcp.module';
|
|
import { RestApiModule } from 'src/engine/api/rest/rest-api.module';
|
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
|
import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module';
|
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
|
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
|
import { WorkspaceAuthContextMiddleware } from 'src/engine/core-modules/auth/middlewares/workspace-auth-context.middleware';
|
|
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';
|
|
import { MiddlewareModule } from 'src/engine/middlewares/middleware.module';
|
|
import { RestCoreMiddleware } from 'src/engine/middlewares/rest-core.middleware';
|
|
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
|
|
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
|
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
|
import { ModulesModule } from 'src/modules/modules.module';
|
|
|
|
import { ClickHouseModule } from './database/clickHouse/clickHouse.module';
|
|
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
|
import { I18nModule } from './engine/core-modules/i18n/i18n.module';
|
|
|
|
// TODO: Remove this middleware when all the rest endpoints are migrated to TwentyORM
|
|
const MIGRATED_REST_METHODS = [
|
|
RequestMethod.DELETE,
|
|
RequestMethod.POST,
|
|
RequestMethod.PATCH,
|
|
RequestMethod.PUT,
|
|
RequestMethod.GET,
|
|
];
|
|
|
|
@Module({
|
|
imports: [
|
|
SentryModule.forRoot(),
|
|
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
|
driver: YogaDriver,
|
|
imports: [GraphQLConfigModule, MetricsModule, DataloaderModule],
|
|
useClass: GraphQLConfigService,
|
|
}),
|
|
TwentyORMModule,
|
|
GlobalWorkspaceDataSourceModule,
|
|
ClickHouseModule,
|
|
// Core engine module, contains all the core modules
|
|
CoreEngineModule,
|
|
// Modules module, contains all business logic modules
|
|
ModulesModule,
|
|
// Needed for the user workspace middleware
|
|
WorkspaceCacheStorageModule,
|
|
// Api modules
|
|
CoreGraphQLApiModule,
|
|
MetadataGraphQLApiModule,
|
|
RestApiModule,
|
|
McpModule,
|
|
DataSourceModule,
|
|
MiddlewareModule,
|
|
WorkspaceMetadataVersionModule,
|
|
// I18n module for translations
|
|
I18nModule,
|
|
// Conditional modules
|
|
...AppModule.getConditionalModules(),
|
|
],
|
|
})
|
|
export class AppModule {
|
|
private static getConditionalModules(): DynamicModule[] {
|
|
const modules: DynamicModule[] = [];
|
|
const frontPath = join(__dirname, 'front');
|
|
|
|
// NestJS DevTools - can be useful for debugging and profiling
|
|
/* if (process.env.NODE_ENV === NodeEnvironment.DEVELOPMENT) {
|
|
modules.push(
|
|
DevtoolsModule.register({
|
|
http: true,
|
|
}),
|
|
);
|
|
} */
|
|
|
|
if (existsSync(frontPath)) {
|
|
modules.push(
|
|
ServeStaticModule.forRoot({
|
|
rootPath: frontPath,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Messaque Queue explorer only for sync driver
|
|
// Maybe we don't need to conditionaly register the explorer, because we're creating a jobs module
|
|
// that will expose classes that are only used in the queue worker
|
|
/*
|
|
if (process.env.MESSAGE_QUEUE_TYPE === MessageQueueDriverType.Sync) {
|
|
modules.push(MessageQueueModule.registerExplorer());
|
|
}
|
|
*/
|
|
|
|
return modules;
|
|
}
|
|
|
|
configure(consumer: MiddlewareConsumer) {
|
|
consumer
|
|
.apply(
|
|
GraphQLHydrateRequestFromTokenMiddleware,
|
|
WorkspaceAuthContextMiddleware,
|
|
)
|
|
.forRoutes({ path: 'graphql', method: RequestMethod.ALL });
|
|
|
|
consumer
|
|
.apply(
|
|
GraphQLHydrateRequestFromTokenMiddleware,
|
|
WorkspaceAuthContextMiddleware,
|
|
)
|
|
.forRoutes({ path: 'metadata', method: RequestMethod.ALL });
|
|
|
|
for (const method of MIGRATED_REST_METHODS) {
|
|
consumer
|
|
.apply(RestCoreMiddleware, WorkspaceAuthContextMiddleware)
|
|
.forRoutes({ path: 'rest/*path', method });
|
|
}
|
|
}
|
|
}
|