feat: schema version header check (#4563)
closes https://github.com/twentyhq/twenty/issues/4479 tried to catch the error inside various places including https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/integrations/exception-handler/exception-handler.service.ts but it seems like the error never reaches the GraphQL module 😮 any idea where we could intercept such an error `Cannot query field`? --------- Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com>
This commit is contained in:
@ -39,6 +39,23 @@ export type ApiKeyToken = {
|
|||||||
token: Scalars['String']['output'];
|
token: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppToken = {
|
||||||
|
__typename?: 'AppToken';
|
||||||
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
expiresAt: Scalars['DateTime']['output'];
|
||||||
|
id: Scalars['ID']['output'];
|
||||||
|
type: Scalars['String']['output'];
|
||||||
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppTokenEdge = {
|
||||||
|
__typename?: 'AppTokenEdge';
|
||||||
|
/** Cursor for this node. */
|
||||||
|
cursor: Scalars['ConnectionCursor']['output'];
|
||||||
|
/** The node containing the AppToken */
|
||||||
|
node: AppToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthProviders = {
|
export type AuthProviders = {
|
||||||
__typename?: 'AuthProviders';
|
__typename?: 'AuthProviders';
|
||||||
google: Scalars['Boolean']['output'];
|
google: Scalars['Boolean']['output'];
|
||||||
@ -63,6 +80,11 @@ export type AuthTokens = {
|
|||||||
tokens: AuthTokenPair;
|
tokens: AuthTokenPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthorizeApp = {
|
||||||
|
__typename?: 'AuthorizeApp';
|
||||||
|
redirectUrl: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Billing = {
|
export type Billing = {
|
||||||
__typename?: 'Billing';
|
__typename?: 'Billing';
|
||||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
||||||
@ -110,6 +132,10 @@ export type ClientConfig = {
|
|||||||
telemetry: Telemetry;
|
telemetry: Telemetry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateAppTokenInput = {
|
||||||
|
expiresAt: Scalars['DateTime']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateFieldInput = {
|
export type CreateFieldInput = {
|
||||||
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
||||||
description?: InputMaybe<Scalars['String']['input']>;
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -138,6 +164,11 @@ export type CreateObjectInput = {
|
|||||||
nameSingular: Scalars['String']['input'];
|
nameSingular: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateOneAppTokenInput = {
|
||||||
|
/** The record to create */
|
||||||
|
appToken: CreateAppTokenInput;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateOneFieldMetadataInput = {
|
export type CreateOneFieldMetadataInput = {
|
||||||
/** The record to create */
|
/** The record to create */
|
||||||
field: CreateFieldInput;
|
field: CreateFieldInput;
|
||||||
@ -148,20 +179,11 @@ export type CreateOneObjectInput = {
|
|||||||
object: CreateObjectInput;
|
object: CreateObjectInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateOneRefreshTokenInput = {
|
|
||||||
/** The record to create */
|
|
||||||
refreshToken: CreateRefreshTokenInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateOneRelationInput = {
|
export type CreateOneRelationInput = {
|
||||||
/** The record to create */
|
/** The record to create */
|
||||||
relation: CreateRelationInput;
|
relation: CreateRelationInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateRefreshTokenInput = {
|
|
||||||
expiresAt: Scalars['DateTime']['input'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateRelationInput = {
|
export type CreateRelationInput = {
|
||||||
description?: InputMaybe<Scalars['String']['input']>;
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
fromDescription?: InputMaybe<Scalars['String']['input']>;
|
fromDescription?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -215,6 +237,13 @@ export type EmailPasswordResetLink = {
|
|||||||
success: Scalars['Boolean']['output'];
|
success: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExchangeAuthCode = {
|
||||||
|
__typename?: 'ExchangeAuthCode';
|
||||||
|
accessToken: AuthToken;
|
||||||
|
loginToken: AuthToken;
|
||||||
|
refreshToken: AuthToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type FeatureFlag = {
|
export type FeatureFlag = {
|
||||||
__typename?: 'FeatureFlag';
|
__typename?: 'FeatureFlag';
|
||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
@ -338,11 +367,12 @@ export type LoginToken = {
|
|||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkspace: Workspace;
|
activateWorkspace: Workspace;
|
||||||
|
authorizeApp: AuthorizeApp;
|
||||||
challenge: LoginToken;
|
challenge: LoginToken;
|
||||||
checkoutSession: SessionEntity;
|
checkoutSession: SessionEntity;
|
||||||
|
createOneAppToken: AppToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
createOneRefreshToken: RefreshToken;
|
|
||||||
createOneRelation: Relation;
|
createOneRelation: Relation;
|
||||||
createOneRemoteServer: RemoteServer;
|
createOneRemoteServer: RemoteServer;
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
@ -378,6 +408,12 @@ export type MutationActivateWorkspaceArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationAuthorizeAppArgs = {
|
||||||
|
clientId: Scalars['String']['input'];
|
||||||
|
codeChallenge: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationChallengeArgs = {
|
export type MutationChallengeArgs = {
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
password: Scalars['String']['input'];
|
password: Scalars['String']['input'];
|
||||||
@ -390,6 +426,11 @@ export type MutationCheckoutSessionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOneAppTokenArgs = {
|
||||||
|
input: CreateOneAppTokenInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneFieldArgs = {
|
export type MutationCreateOneFieldArgs = {
|
||||||
input: CreateOneFieldMetadataInput;
|
input: CreateOneFieldMetadataInput;
|
||||||
};
|
};
|
||||||
@ -400,11 +441,6 @@ export type MutationCreateOneObjectArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneRefreshTokenArgs = {
|
|
||||||
input: CreateOneRefreshTokenInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneRelationArgs = {
|
export type MutationCreateOneRelationArgs = {
|
||||||
input: CreateOneRelationInput;
|
input: CreateOneRelationInput;
|
||||||
};
|
};
|
||||||
@ -457,7 +493,7 @@ export type MutationImpersonateArgs = {
|
|||||||
|
|
||||||
|
|
||||||
export type MutationRenewTokenArgs = {
|
export type MutationRenewTokenArgs = {
|
||||||
refreshToken: Scalars['String']['input'];
|
appToken: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -576,6 +612,7 @@ export type Query = {
|
|||||||
clientConfig: ClientConfig;
|
clientConfig: ClientConfig;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
|
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||||
field: Field;
|
field: Field;
|
||||||
fields: FieldConnection;
|
fields: FieldConnection;
|
||||||
findAvailableRemoteTablesByServerId: Array<RemoteTable>;
|
findAvailableRemoteTablesByServerId: Array<RemoteTable>;
|
||||||
@ -610,6 +647,12 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryExchangeAuthorizationCodeArgs = {
|
||||||
|
authorizationCode: Scalars['String']['input'];
|
||||||
|
codeVerifier: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFieldArgs = {
|
export type QueryFieldArgs = {
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
@ -699,22 +742,6 @@ export type QueryValidatePasswordResetTokenArgs = {
|
|||||||
passwordResetToken: Scalars['String']['input'];
|
passwordResetToken: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RefreshToken = {
|
|
||||||
__typename?: 'RefreshToken';
|
|
||||||
createdAt: Scalars['DateTime']['output'];
|
|
||||||
expiresAt: Scalars['DateTime']['output'];
|
|
||||||
id: Scalars['ID']['output'];
|
|
||||||
updatedAt: Scalars['DateTime']['output'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RefreshTokenEdge = {
|
|
||||||
__typename?: 'RefreshTokenEdge';
|
|
||||||
/** Cursor for this node. */
|
|
||||||
cursor: Scalars['ConnectionCursor']['output'];
|
|
||||||
/** The node containing the RefreshToken */
|
|
||||||
node: RefreshToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RelationConnection = {
|
export type RelationConnection = {
|
||||||
__typename?: 'RelationConnection';
|
__typename?: 'RelationConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@ -1027,6 +1054,7 @@ export type Workspace = {
|
|||||||
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
currentBillingSubscription?: Maybe<BillingSubscription>;
|
currentBillingSubscription?: Maybe<BillingSubscription>;
|
||||||
|
currentCacheVersion?: Maybe<Scalars['String']['output']>;
|
||||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||||
displayName?: Maybe<Scalars['String']['output']>;
|
displayName?: Maybe<Scalars['String']['output']>;
|
||||||
domainName?: Maybe<Scalars['String']['output']>;
|
domainName?: Maybe<Scalars['String']['output']>;
|
||||||
|
|||||||
@ -831,6 +831,7 @@ export type Workspace = {
|
|||||||
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
currentBillingSubscription?: Maybe<BillingSubscription>;
|
currentBillingSubscription?: Maybe<BillingSubscription>;
|
||||||
|
currentCacheVersion?: Maybe<Scalars['String']>;
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||||
displayName?: Maybe<Scalars['String']>;
|
displayName?: Maybe<Scalars['String']>;
|
||||||
domainName?: Maybe<Scalars['String']>;
|
domainName?: Maybe<Scalars['String']>;
|
||||||
@ -2393,6 +2394,7 @@ export const GetCurrentUserDocument = gql`
|
|||||||
value
|
value
|
||||||
workspaceId
|
workspaceId
|
||||||
}
|
}
|
||||||
|
currentCacheVersion
|
||||||
currentBillingSubscription {
|
currentBillingSubscription {
|
||||||
status
|
status
|
||||||
interval
|
interval
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { previousUrlState } from '@/auth/states/previousUrlState';
|
import { previousUrlState } from '@/auth/states/previousUrlState';
|
||||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
@ -17,6 +18,7 @@ import { ApolloFactory, Options } from '../services/apollo.factory';
|
|||||||
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||||
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
|
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
|
||||||
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
const [isDebugMode] = useRecoilState(isDebugModeState);
|
const [isDebugMode] = useRecoilState(isDebugModeState);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -29,6 +31,11 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
apolloRef.current = new ApolloFactory({
|
apolloRef.current = new ApolloFactory({
|
||||||
uri: `${REACT_APP_SERVER_BASE_URL}/graphql`,
|
uri: `${REACT_APP_SERVER_BASE_URL}/graphql`,
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
|
headers: {
|
||||||
|
...(currentWorkspace?.currentCacheVersion && {
|
||||||
|
'X-Schema-Version': currentWorkspace.currentCacheVersion,
|
||||||
|
}),
|
||||||
|
},
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
query: {
|
query: {
|
||||||
fetchPolicy: 'cache-first',
|
fetchPolicy: 'cache-first',
|
||||||
@ -60,7 +67,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
|
|
||||||
return apolloRef.current.getClient();
|
return apolloRef.current.getClient();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [setTokenPair, isDebugMode, setPreviousUrl]);
|
}, [setTokenPair, isDebugMode, currentWorkspace?.currentCacheVersion, setPreviousUrl]);
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
if (isDefined(apolloRef.current)) {
|
if (isDefined(apolloRef.current)) {
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
|
...options.headers,
|
||||||
authorization: this.tokenPair?.accessToken.token
|
authorization: this.tokenPair?.accessToken.token
|
||||||
? `Bearer ${this.tokenPair?.accessToken.token}`
|
? `Bearer ${this.tokenPair?.accessToken.token}`
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type CurrentWorkspace = Pick<
|
|||||||
| 'subscriptionStatus'
|
| 'subscriptionStatus'
|
||||||
| 'activationStatus'
|
| 'activationStatus'
|
||||||
| 'currentBillingSubscription'
|
| 'currentBillingSubscription'
|
||||||
|
| 'currentCacheVersion'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
|
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export const GET_CURRENT_USER = gql`
|
|||||||
value
|
value
|
||||||
workspaceId
|
workspaceId
|
||||||
}
|
}
|
||||||
|
currentCacheVersion
|
||||||
currentBillingSubscription {
|
currentBillingSubscription {
|
||||||
status
|
status
|
||||||
interval
|
interval
|
||||||
|
|||||||
10
packages/twenty-server/@types/express.d.ts
vendored
Normal file
10
packages/twenty-server/@types/express.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
|
declare module 'express-serve-static-core' {
|
||||||
|
interface Request {
|
||||||
|
user?: User;
|
||||||
|
workspace?: Workspace;
|
||||||
|
cacheVersion?: string | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,9 @@
|
|||||||
import { DynamicModule, Module } from '@nestjs/common';
|
import {
|
||||||
|
DynamicModule,
|
||||||
|
MiddlewareConsumer,
|
||||||
|
Module,
|
||||||
|
RequestMethod,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { GraphQLModule } from '@nestjs/graphql';
|
import { GraphQLModule } from '@nestjs/graphql';
|
||||||
@ -14,6 +19,8 @@ import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.mo
|
|||||||
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
||||||
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.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 { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
||||||
|
import { UserWorkspaceMiddleware } from 'src/engine/middlewares/user-workspace.middleware';
|
||||||
|
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||||
|
|
||||||
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
||||||
import { IntegrationsModule } from './engine/integrations/integrations.module';
|
import { IntegrationsModule } from './engine/integrations/integrations.module';
|
||||||
@ -42,6 +49,8 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
|
|||||||
CoreEngineModule,
|
CoreEngineModule,
|
||||||
// Modules module, contains all business logic modules
|
// Modules module, contains all business logic modules
|
||||||
ModulesModule,
|
ModulesModule,
|
||||||
|
// Needed for the user workspace middleware
|
||||||
|
WorkspaceCacheVersionModule,
|
||||||
// Api modules
|
// Api modules
|
||||||
CoreGraphQLApiModule,
|
CoreGraphQLApiModule,
|
||||||
MetadataGraphQLApiModule,
|
MetadataGraphQLApiModule,
|
||||||
@ -65,4 +74,14 @@ export class AppModule {
|
|||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer
|
||||||
|
.apply(UserWorkspaceMiddleware)
|
||||||
|
.forRoutes({ path: 'graphql', method: RequestMethod.ALL });
|
||||||
|
|
||||||
|
consumer
|
||||||
|
.apply(UserWorkspaceMiddleware)
|
||||||
|
.forRoutes({ path: 'metadata', method: RequestMethod.ALL });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { YogaDriverServerContext } from '@graphql-yoga/nestjs';
|
|
||||||
|
|
||||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
|
||||||
|
|
||||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CreateContextFactory {
|
|
||||||
constructor(private readonly tokenService: TokenService) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
context: YogaDriverServerContext<'express'>,
|
|
||||||
): Promise<GraphQLContext> {
|
|
||||||
// Check if token is present in the request
|
|
||||||
if (this.tokenService.isTokenPresent(context.req)) {
|
|
||||||
const data = await this.tokenService.validateToken(context.req);
|
|
||||||
|
|
||||||
// Inject user and workspace into the context
|
|
||||||
return { ...context, ...data };
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { CreateContextFactory } from './create-context.factory';
|
|
||||||
|
|
||||||
export const graphQLFactories = [CreateContextFactory];
|
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||||
import { graphQLFactories } from 'src/engine/api/graphql/graphql-config/factories';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CoreEngineModule],
|
imports: [CoreEngineModule],
|
||||||
providers: [...graphQLFactories],
|
providers: [],
|
||||||
exports: [...graphQLFactories],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class GraphQLConfigModule {}
|
export class GraphQLConfigModule {}
|
||||||
|
|||||||
@ -26,8 +26,6 @@ import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-th
|
|||||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
||||||
import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing';
|
import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing';
|
||||||
|
|
||||||
import { CreateContextFactory } from './factories/create-context.factory';
|
|
||||||
|
|
||||||
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
|
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
|
||||||
user?: User;
|
user?: User;
|
||||||
workspace?: Workspace;
|
workspace?: Workspace;
|
||||||
@ -38,7 +36,6 @@ export class GraphQLConfigService
|
|||||||
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private readonly createContextFactory: CreateContextFactory,
|
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
@ -52,7 +49,7 @@ export class GraphQLConfigService
|
|||||||
ttl: this.environmentService.get('API_RATE_LIMITING_TTL'),
|
ttl: this.environmentService.get('API_RATE_LIMITING_TTL'),
|
||||||
limit: this.environmentService.get('API_RATE_LIMITING_LIMIT'),
|
limit: this.environmentService.get('API_RATE_LIMITING_LIMIT'),
|
||||||
identifyFn: (context) => {
|
identifyFn: (context) => {
|
||||||
return context.user?.id ?? context.req.ip ?? 'anonymous';
|
return context.req.user?.id ?? context.req.ip ?? 'anonymous';
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
useExceptionHandler({
|
useExceptionHandler({
|
||||||
@ -65,7 +62,6 @@ export class GraphQLConfigService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config: YogaDriverConfig = {
|
const config: YogaDriverConfig = {
|
||||||
context: (context) => this.createContextFactory.create(context),
|
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
include: [CoreEngineModule],
|
include: [CoreEngineModule],
|
||||||
conditionalSchema: async (context) => {
|
conditionalSchema: async (context) => {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
|||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||||
import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module';
|
import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module';
|
||||||
import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory';
|
|
||||||
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
|
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
|
||||||
import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
|
import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
@ -18,11 +17,7 @@ import { ExceptionHandlerService } from 'src/engine/integrations/exception-handl
|
|||||||
driver: YogaDriver,
|
driver: YogaDriver,
|
||||||
useFactory: metadataModuleFactory,
|
useFactory: metadataModuleFactory,
|
||||||
imports: [GraphQLConfigModule],
|
imports: [GraphQLConfigModule],
|
||||||
inject: [
|
inject: [EnvironmentService, ExceptionHandlerService],
|
||||||
EnvironmentService,
|
|
||||||
ExceptionHandlerService,
|
|
||||||
CreateContextFactory,
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
MetadataEngineModule,
|
MetadataEngineModule,
|
||||||
WorkspaceMigrationRunnerModule,
|
WorkspaceMigrationRunnerModule,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { YogaDriverConfig } from '@graphql-yoga/nestjs';
|
import { YogaDriverConfig } from '@graphql-yoga/nestjs';
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory';
|
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
|
||||||
import { useExceptionHandler } from 'src/engine/integrations/exception-handler/hooks/use-exception-handler.hook';
|
import { useExceptionHandler } from 'src/engine/integrations/exception-handler/hooks/use-exception-handler.hook';
|
||||||
@ -12,12 +11,8 @@ import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playgroun
|
|||||||
export const metadataModuleFactory = async (
|
export const metadataModuleFactory = async (
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
createContextFactory: CreateContextFactory,
|
|
||||||
): Promise<YogaDriverConfig> => {
|
): Promise<YogaDriverConfig> => {
|
||||||
const config: YogaDriverConfig = {
|
const config: YogaDriverConfig = {
|
||||||
context(context) {
|
|
||||||
return createContextFactory.create(context);
|
|
||||||
},
|
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
include: [MetadataGraphQLApiModule],
|
include: [MetadataGraphQLApiModule],
|
||||||
renderGraphiQL() {
|
renderGraphiQL() {
|
||||||
@ -29,7 +24,7 @@ export const metadataModuleFactory = async (
|
|||||||
ttl: environmentService.get('API_RATE_LIMITING_TTL'),
|
ttl: environmentService.get('API_RATE_LIMITING_TTL'),
|
||||||
limit: environmentService.get('API_RATE_LIMITING_LIMIT'),
|
limit: environmentService.get('API_RATE_LIMITING_LIMIT'),
|
||||||
identifyFn: (context) => {
|
identifyFn: (context) => {
|
||||||
return context.user?.id ?? context.req.ip ?? 'anonymous';
|
return context.req.user?.id ?? context.req.ip ?? 'anonymous';
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
useExceptionHandler({
|
useExceptionHandler({
|
||||||
|
|||||||
@ -6,7 +6,10 @@ import { Request } from 'express';
|
|||||||
|
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
|
|
||||||
export type GoogleAPIsRequest = Request & {
|
export type GoogleAPIsRequest = Omit<
|
||||||
|
Request,
|
||||||
|
'user' | 'workspace' | 'cacheVersion'
|
||||||
|
> & {
|
||||||
user: {
|
user: {
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
lastName?: string | null;
|
lastName?: string | null;
|
||||||
|
|||||||
@ -6,7 +6,10 @@ import { Request } from 'express';
|
|||||||
|
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
|
|
||||||
export type GoogleRequest = Request & {
|
export type GoogleRequest = Omit<
|
||||||
|
Request,
|
||||||
|
'user' | 'workspace' | 'cacheVersion'
|
||||||
|
> & {
|
||||||
user: {
|
user: {
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
lastName?: string | null;
|
lastName?: string | null;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
|
|||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||||
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
||||||
|
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||||
|
|
||||||
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
||||||
import { Workspace } from './workspace.entity';
|
import { Workspace } from './workspace.entity';
|
||||||
@ -27,6 +28,7 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
imports: [
|
imports: [
|
||||||
BillingModule,
|
BillingModule,
|
||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
|
WorkspaceCacheVersionModule,
|
||||||
NestjsQueryTypeOrmModule.forFeature(
|
NestjsQueryTypeOrmModule.forFeature(
|
||||||
[Workspace, UserWorkspace, FeatureFlagEntity],
|
[Workspace, UserWorkspace, FeatureFlagEntity],
|
||||||
'core',
|
'core',
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a
|
|||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
|
||||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||||
|
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
|
||||||
|
|
||||||
import { Workspace } from './workspace.entity';
|
import { Workspace } from './workspace.entity';
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
export class WorkspaceResolver {
|
export class WorkspaceResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
||||||
private readonly fileUploadService: FileUploadService,
|
private readonly fileUploadService: FileUploadService,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
) {}
|
) {}
|
||||||
@ -105,6 +107,13 @@ export class WorkspaceResolver {
|
|||||||
return 'inactive';
|
return 'inactive';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => String)
|
||||||
|
async currentCacheVersion(
|
||||||
|
@Parent() workspace: Workspace,
|
||||||
|
): Promise<string | null> {
|
||||||
|
return this.workspaceCacheVersionService.getVersion(workspace.id);
|
||||||
|
}
|
||||||
|
|
||||||
@ResolveField(() => BillingSubscription)
|
@ResolveField(() => BillingSubscription)
|
||||||
async currentBillingSubscription(
|
async currentBillingSubscription(
|
||||||
@Parent() workspace: Workspace,
|
@Parent() workspace: Workspace,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const useExceptionHandler = <PluginContext extends GraphQLContext>(
|
|||||||
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
||||||
) as OperationDefinitionNode;
|
) as OperationDefinitionNode;
|
||||||
const operationType = rootOperation.operation;
|
const operationType = rootOperation.operation;
|
||||||
const user = args.contextValue.user;
|
const user = args.contextValue.req.user;
|
||||||
const document = getDocumentString(args.document, print);
|
const document = getDocumentString(args.document, print);
|
||||||
const opName =
|
const opName =
|
||||||
args.operationName ||
|
args.operationName ||
|
||||||
@ -125,5 +125,24 @@ export const useExceptionHandler = <PluginContext extends GraphQLContext>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onValidate: ({ context, validateFn, params: { documentAST, schema } }) => {
|
||||||
|
const errors = validateFn(schema, documentAST);
|
||||||
|
|
||||||
|
if (Array.isArray(errors) && errors.length > 0) {
|
||||||
|
const headers = context.req.headers;
|
||||||
|
const currentSchemaVersion = context.req.cacheVersion;
|
||||||
|
|
||||||
|
const requestSchemaVersion = headers['x-schema-version'];
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestSchemaVersion &&
|
||||||
|
requestSchemaVersion !== currentSchemaVersion
|
||||||
|
) {
|
||||||
|
throw new GraphQLError(
|
||||||
|
`Schema version mismatch, please refresh the page.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||||
|
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserWorkspaceMiddleware implements NestMiddleware {
|
||||||
|
private readonly logger = new Logger(UserWorkspaceMiddleware.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly tokenService: TokenService,
|
||||||
|
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async use(req: Request, res: Response, next: NextFunction) {
|
||||||
|
if (this.tokenService.isTokenPresent(req)) {
|
||||||
|
try {
|
||||||
|
const data = await this.tokenService.validateToken(req);
|
||||||
|
const cacheVersion = await this.workspaceCacheVersionService.getVersion(
|
||||||
|
data.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
req.user = data.user;
|
||||||
|
req.workspace = data.workspace;
|
||||||
|
req.cacheVersion = cacheVersion;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error while validating token in middleware.', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user