diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index eb12fc6e02..8f752863e2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -12,6 +12,8 @@ import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-mod import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -27,6 +29,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly workspaceDomainsService: WorkspaceDomainsService, + private readonly featureFlagService: FeatureFlagService, ) { super(); } @@ -46,7 +49,21 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( ); } - new GoogleAPIsOauthExchangeCodeForTokenStrategy(this.twentyConfigService); + const { workspaceId } = + await this.transientTokenService.verifyTransientToken( + state.transientToken, + ); + + const isDraftEmailEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED, + workspaceId, + ); + + new GoogleAPIsOauthExchangeCodeForTokenStrategy( + this.twentyConfigService, + isDraftEmailEnabled, + ); setRequestExtraParams(request, { transientToken: state.transientToken, diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index a04c20c182..6013ea6f9b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -12,6 +12,8 @@ import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -25,6 +27,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly workspaceDomainsService: WorkspaceDomainsService, + private readonly featureFlagService: FeatureFlagService, ) { super({ prompt: 'select_account', @@ -68,7 +71,16 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { ); } - new GoogleAPIsOauthRequestCodeStrategy(this.twentyConfigService); + const isDraftEmailEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED, + workspaceId, + ); + + new GoogleAPIsOauthRequestCodeStrategy( + this.twentyConfigService, + isDraftEmailEnabled, + ); return (await super.canActivate(context)) as boolean; } catch (err) { diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.service.util.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.service.util.spec.ts index 5aaa83a139..6b15ecf6a2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.service.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.service.util.spec.ts @@ -92,8 +92,24 @@ describe('GoogleAPIScopesService', () => { expect(result).toBe(false); }); - it('should work with the current Google API scopes', () => { - // What is currently returned by Google + it('should work with the current Google API scopes when draft email is disabled', () => { + const actualGoogleScopes = [ + 'https://www.googleapis.com/auth/calendar.events', + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/gmail.send', + 'https://www.googleapis.com/auth/profile.emails.read', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'openid', + ]; + const expectedScopes = getGoogleApisOauthScopes(false); + + const result = includesExpectedScopes(actualGoogleScopes, expectedScopes); + + expect(result).toBe(true); + }); + + it('should work with the current Google API scopes when draft email is enabled', () => { const actualGoogleScopes = [ 'https://www.googleapis.com/auth/calendar.events', 'https://www.googleapis.com/auth/gmail.readonly', @@ -104,7 +120,7 @@ describe('GoogleAPIScopesService', () => { 'https://www.googleapis.com/auth/userinfo.profile', 'openid', ]; - const expectedScopes = getGoogleApisOauthScopes(); + const expectedScopes = getGoogleApisOauthScopes(true); const result = includesExpectedScopes(actualGoogleScopes, expectedScopes); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.ts index b3849a2563..d0e34e263c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis-scopes.ts @@ -30,6 +30,7 @@ export class GoogleAPIScopesService { public async getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent( accessToken: string, + isDraftEmailEnabled = false, ): Promise<{ scopes: string[]; isValid: boolean }> { try { const httpClient = this.secureHttpClientService.getHttpClient(); @@ -40,7 +41,7 @@ export class GoogleAPIScopesService { ); const scopes = response.data.scope.split(' '); - const expectedScopes = getGoogleApisOauthScopes(); + const expectedScopes = getGoogleApisOauthScopes(isDraftEmailEnabled); return { scopes, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts index f2ea715809..b9524d2391 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts @@ -10,6 +10,7 @@ import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/go import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service'; import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -181,6 +182,12 @@ describe('GoogleAPIsService', () => { provide: getQueueToken(MessageQueue.calendarQueue), useValue: mockCalendarQueueService, }, + { + provide: FeatureFlagService, + useValue: { + isFeatureEnabled: jest.fn().mockResolvedValue(false), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index 01ddbe0bcd..dd0618a6a5 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -13,6 +13,8 @@ import { CreateMessageChannelService } from 'src/engine/core-modules/auth/servic import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/google-apis-scopes'; import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service'; import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @@ -63,6 +65,7 @@ export class GoogleAPIsService { private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService, private readonly googleAPIScopesService: GoogleAPIScopesService, private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService, + private readonly featureFlagService: FeatureFlagService, ) {} async refreshGoogleRefreshToken(input: { @@ -92,9 +95,15 @@ export class GoogleAPIsService { 'MESSAGING_PROVIDER_GMAIL_ENABLED', ); + const isDraftEmailEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED, + workspaceId, + ); + const { scopes, isValid } = await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent( input.accessToken, + isDraftEmailEnabled, ); if (!isValid) { diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts index d21df7ae6f..5082ef2d5d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts @@ -14,8 +14,11 @@ export abstract class GoogleAPIsOauthCommonStrategy extends PassportStrategy( Strategy, 'google-apis', ) { - constructor(twentyConfigService: TwentyConfigService) { - const scopes = getGoogleApisOauthScopes(); + constructor( + twentyConfigService: TwentyConfigService, + isDraftEmailEnabled = false, + ) { + const scopes = getGoogleApisOauthScopes(isDraftEmailEnabled); super({ clientID: twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'), diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts index 03add995c1..0a80b9ba4e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts @@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = { @Injectable() export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauthCommonStrategy { - constructor(twentyConfigService: TwentyConfigService) { - super(twentyConfigService); + constructor( + twentyConfigService: TwentyConfigService, + isDraftEmailEnabled = false, + ) { + super(twentyConfigService, isDraftEmailEnabled); } async validate( diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts index 0249bf6f62..f689fd75ab 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts @@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = { @Injectable() export class GoogleAPIsOauthRequestCodeStrategy extends GoogleAPIsOauthCommonStrategy { - constructor(twentyConfigService: TwentyConfigService) { - super(twentyConfigService); + constructor( + twentyConfigService: TwentyConfigService, + isDraftEmailEnabled = false, + ) { + super(twentyConfigService, isDraftEmailEnabled); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts index bf15abced7..2a59feee44 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts @@ -1,7 +1,7 @@ /** email, profile and openid permission can be called without the https://www.googleapis.com/auth/ prefix * see https://developers.google.com/identity/protocols/oauth2/scopes */ -export const getGoogleApisOauthScopes = () => { +export const getGoogleApisOauthScopes = (isDraftEmailEnabled = false) => { return [ 'email', 'profile', @@ -9,6 +9,8 @@ export const getGoogleApisOauthScopes = () => { 'https://www.googleapis.com/auth/calendar.events', 'https://www.googleapis.com/auth/profile.emails.read', 'https://www.googleapis.com/auth/gmail.send', - 'https://www.googleapis.com/auth/gmail.compose', + ...(isDraftEmailEnabled + ? ['https://www.googleapis.com/auth/gmail.compose'] + : []), ]; };