From c66261a38bfb8fbb4c4b7204d82b26420c638176 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:08:49 +0530 Subject: [PATCH] Reuse OAuth access tokens (#15089) --- .../jwt/services/jwt-wrapper.service.ts | 12 +- .../calendar-event-import-manager.module.ts | 2 + .../google-calendar-driver.module.ts | 3 +- .../providers/google-calendar.provider.ts | 30 --- .../google-calendar-get-events.service.ts | 11 +- .../microsoft-calendar-driver.module.ts | 6 +- .../microsoft-calendar-get-events.service.ts | 10 +- ...icrosoft-calendar-import-events.service.ts | 10 +- ...calendar-account-authentication.service.ts | 98 ++++++++++ .../services/calendar-fetch-events.service.ts | 19 +- .../services/calendar-get-events.service.ts | 7 +- .../google-email-alias-manager.service.ts | 9 +- .../microsoft-email-alias-manager.service.ts | 8 +- .../email-alias-manager.module.ts | 2 - .../email-alias-manager.service.spec.ts | 8 +- .../google-oauth2-client-manager.service.ts | 9 +- .../microsoft-oauth2-client-auth-provider.ts | 11 ++ ...microsoft-oauth2-client-manager.service.ts | 74 +------- .../services/oauth2-client-manager.service.ts | 34 ++-- ...d-account-refresh-tokens-manager.module.ts | 7 +- .../google-api-refresh-access-token.module.ts | 11 -- ...google-api-refresh-access-token.service.ts | 51 ------ ...crosoft-api-refresh-access-token.module.ts | 3 +- ...is-access-token-expired-or-invalid.util.ts | 25 +++ ...ted-account-refresh-tokens.service.spec.ts | 171 ++++++++++++++++++ ...onnected-account-refresh-tokens.service.ts | 54 +++++- .../gmail/gmail-get-all-folders.service.ts | 14 +- .../microsoft-get-all-folders.service.ts | 11 +- .../message-folder-driver.interface.ts | 7 +- .../messaging-folder-sync-manager.module.ts | 2 + .../gmail/messaging-gmail-driver.module.ts | 7 +- .../gmail/providers/gmail-client.provider.ts | 30 --- .../gmail/providers/oauth2-client.provider.ts | 28 --- .../gmail-get-message-list.service.spec.ts | 62 +++++-- .../gmail-get-message-list.service.ts | 21 ++- .../services/gmail-get-messages.service.ts | 2 +- .../messaging-microsoft-driver.module.ts | 6 - .../providers/microsoft-client.provider.ts | 24 --- .../microsoft-fetch-by-batch.service.ts | 10 +- ...osoft-get-message-list.service.dev.spec.ts | 18 +- .../microsoft-get-message-list.service.ts | 10 +- ...microsoft-get-messages.service.dev.spec.ts | 6 +- .../microsoft-get-messages.service.spec.ts | 13 +- .../microsoft-get-messages.service.ts | 7 +- .../messaging-import-manager.module.ts | 2 + .../messaging-send-message-gmail.spec.ts | 56 +++--- ...essaging-account-authentication.service.ts | 4 +- .../messaging-send-message.service.ts | 21 +-- .../types/get-message-lists-args.type.ts | 7 +- 49 files changed, 632 insertions(+), 421 deletions(-) delete mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service.ts create mode 100644 packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-auth-provider.ts delete mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module.ts delete mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service.ts create mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util.ts create mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts delete mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider.ts delete mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider.ts delete mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider.ts diff --git a/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts b/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts index f1dbfd0b28..a7493654b3 100644 --- a/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts +++ b/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts @@ -16,16 +16,16 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { - type JwtPayload, - JwtTokenTypeEnum, - type TransientTokenJwtPayload, - type RefreshTokenJwtPayload, - type WorkspaceAgnosticTokenJwtPayload, type AccessTokenJwtPayload, type FileTokenJwtPayload, + type JwtPayload, + JwtTokenTypeEnum, + type RefreshTokenJwtPayload, + type TransientTokenJwtPayload, + type WorkspaceAgnosticTokenJwtPayload, } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @Injectable() export class JwtWrapperService { diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index bac02ae201..888cd2674b 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -23,6 +23,7 @@ import { MicrosoftCalendarDriverModule } from 'src/modules/calendar/calendar-eve import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job'; import { CalendarOngoingStaleJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job'; +import { CalendarAccountAuthenticationService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service'; import { CalendarEventImportErrorHandlerService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service'; import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service'; @@ -51,6 +52,7 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres MetricsModule, ], providers: [ + CalendarAccountAuthenticationService, CalendarChannelSyncStatusService, CalendarEventsImportService, CalendarFetchEventsService, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts index 5150ef3f8d..3f515a547b 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; -import { GoogleCalendarClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider'; import { GoogleCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; @Module({ imports: [TwentyConfigModule, OAuth2ClientManagerModule], - providers: [GoogleCalendarClientProvider, GoogleCalendarGetEventsService], + providers: [GoogleCalendarGetEventsService], exports: [GoogleCalendarGetEventsService], }) export class GoogleCalendarDriverModule {} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts deleted file mode 100644 index 0176b2b46d..0000000000 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type calendar_v3 as calendarV3, google } from 'googleapis'; - -import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; - -@Injectable() -export class GoogleCalendarClientProvider { - constructor( - private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, - ) {} - - public async getGoogleCalendarClient( - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' - >, - ): Promise { - const oAuth2Client = - await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); - - const googleCalendarClient = google.calendar({ - version: 'v3', - auth: oAuth2Client, - }); - - return googleCalendarClient; - } -} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts index 3b26c65cd0..0e891d52a9 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts @@ -1,13 +1,14 @@ +// import { Injectable, Logger } from '@nestjs/common'; import { type GaxiosError } from 'gaxios'; import { type calendar_v3 as calendarV3 } from 'googleapis'; -import { GoogleCalendarClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider'; import { formatGoogleCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util'; import { parseGaxiosError } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/parse-gaxios-error.util'; import { parseGoogleCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/parse-google-calendar-error.util'; import { type GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() @@ -15,7 +16,7 @@ export class GoogleCalendarGetEventsService { private readonly logger = new Logger(GoogleCalendarGetEventsService.name); constructor( - private readonly googleCalendarClientProvider: GoogleCalendarClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, ) {} public async getCalendarEvents( @@ -25,11 +26,13 @@ export class GoogleCalendarGetEventsService { >, syncCursor?: string, ): Promise { - const googleCalendarClient = - await this.googleCalendarClientProvider.getGoogleCalendarClient( + const oAuth2Client = + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( connectedAccount, ); + const googleCalendarClient = oAuth2Client.calendar({ version: 'v3' }); + let nextSyncToken: string | null | undefined; let nextPageToken: string | undefined; const events: calendarV3.Schema$Event[] = []; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module.ts index d6635421e3..143382d7b7 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module.ts @@ -1,16 +1,14 @@ import { Module } from '@nestjs/common'; -import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { MicrosoftCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service'; import { MicrosoftCalendarImportEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service'; -import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; +import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; @Module({ - imports: [TwentyConfigModule], + imports: [OAuth2ClientManagerModule], providers: [ MicrosoftCalendarGetEventsService, MicrosoftCalendarImportEventsService, - MicrosoftOAuth2ClientManagerService, ], exports: [ MicrosoftCalendarGetEventsService, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service.ts index cbabef09f6..60fdf4271f 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service.ts @@ -12,27 +12,27 @@ import { } from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util'; import { type GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; -import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils'; @Injectable() export class MicrosoftCalendarGetEventsService { constructor( - private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, ) {} public async getCalendarEvents( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' + 'provider' | 'accessToken' | 'refreshToken' | 'id' >, syncCursor?: string, ): Promise { try { const microsoftClient = - await this.microsoftOAuth2ClientManagerService.getOAuth2Client( - connectedAccount.refreshToken, + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, ); const eventIds: string[] = []; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts index 999f2ca884..9cfa0d6a16 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts @@ -9,27 +9,27 @@ import { import { formatMicrosoftCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util'; import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util'; import { type FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; -import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils'; @Injectable() export class MicrosoftCalendarImportEventsService { constructor( - private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, ) {} public async getCalendarEvents( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' + 'provider' | 'accessToken' | 'refreshToken' | 'id' >, changedEventIds: string[], ): Promise { try { const microsoftClient = - await this.microsoftOAuth2ClientManagerService.getOAuth2Client( - connectedAccount.refreshToken, + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, ); const events: Event[] = []; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service.ts new file mode 100644 index 0000000000..b0790dc700 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { + CalendarEventImportDriverException, + CalendarEventImportDriverExceptionCode, +} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; +import { ConnectedAccountRefreshAccessTokenExceptionCode } from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception'; +import { + ConnectedAccountRefreshTokensService, + type ConnectedAccountTokens, +} from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + +interface ValidateAndRefreshConnectedAccountAuthenticationParams { + connectedAccount: ConnectedAccountWorkspaceEntity; + workspaceId: string; + calendarChannelId: string; +} + +@Injectable() +export class CalendarAccountAuthenticationService { + constructor( + private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService, + ) {} + + async validateAndRefreshConnectedAccountAuthentication({ + connectedAccount, + workspaceId, + calendarChannelId: messageChannelId, + }: ValidateAndRefreshConnectedAccountAuthenticationParams): Promise { + if ( + connectedAccount.provider === ConnectedAccountProvider.IMAP_SMTP_CALDAV && + isDefined(connectedAccount.connectionParameters?.SMTP) + ) { + await this.validateCalDavCredentialsForConnectedAccount({ + connectedAccount, + workspaceId, + calendarChannelId: messageChannelId, + }); + + return { + accessToken: '', + refreshToken: '', + }; + } + + return await this.refreshAccessTokenForOAuthProvider({ + connectedAccount, + workspaceId, + calendarChannelId: messageChannelId, + }); + } + + private async validateCalDavCredentialsForConnectedAccount({ + connectedAccount, + }: ValidateAndRefreshConnectedAccountAuthenticationParams): Promise { + if ( + !isDefined(connectedAccount.connectionParameters) || + !isDefined(connectedAccount.connectionParameters?.CALDAV) + ) { + throw { + code: CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + message: 'Missing CALDAV credentials in connectionParameters', + }; + } + } + + private async refreshAccessTokenForOAuthProvider({ + connectedAccount, + workspaceId, + }: ValidateAndRefreshConnectedAccountAuthenticationParams): Promise { + try { + return await this.connectedAccountRefreshTokensService.refreshAndSaveTokens( + connectedAccount, + workspaceId, + ); + } catch (error) { + switch (error.code) { + case ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR: + throw new CalendarEventImportDriverException( + error.message, + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED: + case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND: + throw new CalendarEventImportDriverException( + error.message, + CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + default: + throw error; + } + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts index 1161f3cbff..75b2ef4921 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts @@ -8,6 +8,7 @@ import { CalendarEventImportDriverException, CalendarEventImportDriverExceptionCode, } from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; +import { CalendarAccountAuthenticationService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service'; import { CalendarEventImportErrorHandlerService, CalendarEventImportSyncStep, @@ -32,6 +33,7 @@ export class CalendarFetchEventsService { private readonly getCalendarEventsService: CalendarGetCalendarEventsService, private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService, private readonly calendarEventsImportService: CalendarEventsImportService, + private readonly calendarAccountAuthenticationService: CalendarAccountAuthenticationService, ) {} public async fetchCalendarEvents( @@ -50,9 +52,24 @@ export class CalendarFetchEventsService { ); try { + const { accessToken, refreshToken } = + await this.calendarAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( + { + connectedAccount, + workspaceId, + calendarChannelId: calendarChannel.id, + }, + ); + + const connectedAccountWithFreshTokens = { + ...connectedAccount, + accessToken, + refreshToken, + }; + const getCalendarEventsResponse = await this.getCalendarEventsService.getCalendarEvents( - connectedAccount, + connectedAccountWithFreshTokens, calendarChannel.syncCursor, ); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts index 505ad31648..01eb3fbc24 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts @@ -30,7 +30,12 @@ export class CalendarGetCalendarEventsService { public async getCalendarEvents( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'connectionParameters' | 'handle' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'connectionParameters' + | 'handle' >, syncCursor?: string, ): Promise { diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service.ts index 75deb31abb..71e40c2dd4 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { google } from 'googleapis'; - import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -15,11 +13,12 @@ export class GoogleEmailAliasManagerService { connectedAccount: ConnectedAccountWorkspaceEntity, ) { const oAuth2Client = - await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( + connectedAccount, + ); - const people = google.people({ + const people = oAuth2Client.people({ version: 'v1', - auth: oAuth2Client, }); const emailsResponse = await people.people.get({ diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service.ts index e7ef614f2a..7015f576b2 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service.ts @@ -2,25 +2,27 @@ import { Injectable } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageImportDriverException, MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils'; @Injectable() export class MicrosoftEmailAliasManagerService { constructor( - private readonly microsoftClientProvider: MicrosoftClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, ) {} public async getHandleAliases( connectedAccount: ConnectedAccountWorkspaceEntity, ) { const microsoftClient = - await this.microsoftClientProvider.getMicrosoftClient(connectedAccount); + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, + ); const response = await microsoftClient .api('/me?$select=proxyAddresses') diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts index 364d030b04..fa69131387 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts @@ -4,7 +4,6 @@ import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/em import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service'; import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; @Module({ imports: [OAuth2ClientManagerModule], @@ -12,7 +11,6 @@ import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-ma EmailAliasManagerService, GoogleEmailAliasManagerService, MicrosoftEmailAliasManagerService, - MicrosoftClientProvider, ], exports: [EmailAliasManagerService], }) diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts index 870c86429e..18104d5ba3 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts @@ -1,14 +1,14 @@ import { Test, type TestingModule } from '@nestjs/testing'; -import { type Repository } from 'typeorm'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { type Repository } from 'typeorm'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service'; import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/microsoft-email-alias-manager.service'; import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { EmailAliasManagerService } from './email-alias-manager.service'; @@ -42,9 +42,9 @@ describe('Email Alias Manager Service', () => { }, MicrosoftEmailAliasManagerService, { - provide: MicrosoftClientProvider, + provide: OAuth2ClientManagerService, useValue: { - getMicrosoftClient: jest.fn().mockResolvedValue({ + getMicrosoftOAuth2Client: jest.fn().mockResolvedValue({ api: jest.fn().mockReturnValue({ get: jest .fn() diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service.ts index 96e65ef707..1a5a177c71 100644 --- a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { type OAuth2Client } from 'google-auth-library'; -import { google } from 'googleapis'; +import { google, GoogleApis } from 'googleapis'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -12,7 +11,7 @@ export class GoogleOAuth2ClientManagerService { private readonly logger: Logger, ) {} - public async getOAuth2Client(refreshToken: string): Promise { + public async getOAuth2Client(refreshToken: string): Promise { const gmailClientId = this.twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'); const gmailClientSecret = this.twentyConfigService.get( 'AUTH_GOOGLE_CLIENT_SECRET', @@ -28,7 +27,9 @@ export class GoogleOAuth2ClientManagerService { refresh_token: refreshToken, }); - return oAuth2Client; + google.options({ auth: oAuth2Client }); + + return google; } catch (error) { this.logger.error( `Error in ${GoogleOAuth2ClientManagerService.name}`, diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-auth-provider.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-auth-provider.ts new file mode 100644 index 0000000000..165eb1d51d --- /dev/null +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-auth-provider.ts @@ -0,0 +1,11 @@ +import type { AuthenticationProvider } from '@microsoft/microsoft-graph-client'; + +export class MicrosoftOAuth2ClientAuthProvider + implements AuthenticationProvider +{ + constructor(private readonly accessToken: string) {} + + public async getAccessToken(): Promise { + return this.accessToken; + } +} diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts index 4f5961ead5..2d6876c419 100644 --- a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts @@ -1,78 +1,18 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import { - type AuthProvider, - type AuthProviderCallback, - Client, -} from '@microsoft/microsoft-graph-client'; +import { Client } from '@microsoft/microsoft-graph-client'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { ConnectedAccountRefreshAccessTokenExceptionCode } from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception'; +import { MicrosoftOAuth2ClientAuthProvider } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-auth-provider'; @Injectable() export class MicrosoftOAuth2ClientManagerService { - private readonly logger = new Logger( - MicrosoftOAuth2ClientManagerService.name, - ); - constructor(private readonly twentyConfigService: TwentyConfigService) {} + public async getOAuth2Client(accessToken: string): Promise { + const authProvider = new MicrosoftOAuth2ClientAuthProvider(accessToken); - public async getOAuth2Client(refreshToken: string): Promise { - const authProvider: AuthProvider = async ( - callback: AuthProviderCallback, - ) => { - try { - const urlData = new URLSearchParams(); - - urlData.append( - 'client_id', - this.twentyConfigService.get('AUTH_MICROSOFT_CLIENT_ID'), - ); - urlData.append('scope', 'https://graph.microsoft.com/.default'); - urlData.append('refresh_token', refreshToken); - urlData.append( - 'client_secret', - this.twentyConfigService.get('AUTH_MICROSOFT_CLIENT_SECRET'), - ); - urlData.append('grant_type', 'refresh_token'); - - const res = await fetch( - `https://login.microsoftonline.com/common/oauth2/v2.0/token`, - { - method: 'POST', - body: urlData, - }, - ); - - const data = await res.json(); - - if (!res.ok) { - if (data) { - const accessTokenSliced = data?.access_token?.slice(0, 10); - const refreshTokenSliced = data?.refresh_token?.slice(0, 10); - - delete data.access_token; - delete data.refresh_token; - this.logger.error(data); - this.logger.error(`accessTokenSliced: ${accessTokenSliced}`); - this.logger.error(`refreshTokenSliced: ${refreshTokenSliced}`); - } - - this.logger.error(res); - throw new Error( - `${MicrosoftOAuth2ClientManagerService.name} error: ${ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED}`, - ); - } - - callback(null, data.access_token); - } catch (error) { - callback(error, null); - } - }; - - const client = Client.init({ + const client = Client.initWithMiddleware({ defaultVersion: 'v1.0', debugLogging: false, - authProvider: authProvider, + authProvider, }); return client; diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts index 6fa9781e95..be72285479 100644 --- a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts @@ -1,34 +1,38 @@ import { Injectable } from '@nestjs/common'; -import { type OAuth2Client } from 'google-auth-library'; -import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { type Client } from '@microsoft/microsoft-graph-client'; +import { GoogleApis } from 'googleapis'; import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service'; +import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() export class OAuth2ClientManagerService { constructor( private readonly googleOAuth2ClientManagerService: GoogleOAuth2ClientManagerService, + private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService, ) {} - public async getOAuth2Client( + public async getGoogleOAuth2Client( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, 'provider' | 'refreshToken' >, - ): Promise { - const { refreshToken } = connectedAccount; + ): Promise { + return this.googleOAuth2ClientManagerService.getOAuth2Client( + connectedAccount.refreshToken, + ); + } - switch (connectedAccount.provider) { - case ConnectedAccountProvider.GOOGLE: - return this.googleOAuth2ClientManagerService.getOAuth2Client( - refreshToken, - ); - default: - throw new Error( - `OAuth2 client manager for provider ${connectedAccount.provider} is not implemented`, - ); - } + public async getMicrosoftOAuth2Client( + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'accessToken' + >, + ): Promise { + return this.microsoftOAuth2ClientManagerService.getOAuth2Client( + connectedAccount.accessToken, + ); } } diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts index 8165f89d8b..80b80a1eb0 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module.ts @@ -1,14 +1,11 @@ import { Module } from '@nestjs/common'; -import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module'; +import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { MicrosoftAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module'; import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; @Module({ - imports: [ - GoogleAPIRefreshAccessTokenModule, - MicrosoftAPIRefreshAccessTokenModule, - ], + imports: [JwtModule, MicrosoftAPIRefreshAccessTokenModule], providers: [ConnectedAccountRefreshTokensService], exports: [ConnectedAccountRefreshTokensService], }) diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module.ts deleted file mode 100644 index 7bf8a7753d..0000000000 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service'; -import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; - -@Module({ - imports: [MessagingCommonModule], - providers: [GoogleAPIRefreshAccessTokenService], - exports: [GoogleAPIRefreshAccessTokenService], -}) -export class GoogleAPIRefreshAccessTokenModule {} diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service.ts deleted file mode 100644 index 3e08a30c33..0000000000 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import axios from 'axios'; -import { z } from 'zod'; - -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { type ConnectedAccountTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; - -export type GoogleTokens = { - accessToken: string; -}; - -interface GoogleRefreshTokenResponse { - access_token: string; - id_token?: string; - token_type?: string; - expires_in?: number; - scope?: string; -} -@Injectable() -export class GoogleAPIRefreshAccessTokenService { - constructor(private readonly twentyConfigService: TwentyConfigService) {} - - async refreshAccessToken( - refreshToken: string, - ): Promise { - const response = await axios.post( - 'https://oauth2.googleapis.com/token', - { - client_id: this.twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'), - client_secret: this.twentyConfigService.get( - 'AUTH_GOOGLE_CLIENT_SECRET', - ), - refresh_token: refreshToken, - grant_type: 'refresh_token', - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - z.string().parse(response.data.access_token); - - return { - accessToken: response.data.access_token, - refreshToken, - }; - } -} diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module.ts index b560ab406f..f4580ee207 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; +import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; @Module({ - imports: [TwentyConfigModule], + imports: [TwentyConfigModule, JwtModule], providers: [MicrosoftAPIRefreshAccessTokenService], exports: [MicrosoftAPIRefreshAccessTokenService], }) diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util.ts new file mode 100644 index 0000000000..110cbb20d2 --- /dev/null +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util.ts @@ -0,0 +1,25 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import jwt from 'jsonwebtoken'; + +export const isAccessTokenExpiredOrInvalid = ( + token: string, + expirationBufferInSeconds = 5 * 60, +): boolean => { + if (!isNonEmptyString(token)) { + return true; + } + + try { + const payload = jwt.decode(token) as { exp?: number } | null; + + if (!payload || typeof payload.exp !== 'number') { + return true; + } + + const currentTime = Math.floor(Date.now() / 1000); + + return payload.exp < currentTime + expirationBufferInSeconds; + } catch { + return true; + } +}; diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts new file mode 100644 index 0000000000..150bbf244f --- /dev/null +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts @@ -0,0 +1,171 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; +import { isAccessTokenExpiredOrInvalid } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util'; +import { + ConnectedAccountRefreshAccessTokenException, + ConnectedAccountRefreshAccessTokenExceptionCode, +} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception'; +import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + +import { ConnectedAccountRefreshTokensService } from './connected-account-refresh-tokens.service'; + +jest.mock( + 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util', +); + +describe('ConnectedAccountRefreshTokensService', () => { + let service: ConnectedAccountRefreshTokensService; + let microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService; + let twentyORMManager: TwentyORMManager; + + const mockWorkspaceId = 'workspace-123'; + const mockConnectedAccountId = 'account-456'; + const mockAccessToken = 'valid-access-token'; + const mockRefreshToken = 'valid-refresh-token'; + const mockNewAccessToken = 'new-access-token'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ConnectedAccountRefreshTokensService, + { + provide: MicrosoftAPIRefreshAccessTokenService, + useValue: { + refreshTokens: jest.fn(), + }, + }, + { + provide: TwentyORMManager, + useValue: { + getRepository: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get( + ConnectedAccountRefreshTokensService, + ); + microsoftAPIRefreshAccessTokenService = + module.get( + MicrosoftAPIRefreshAccessTokenService, + ); + twentyORMManager = module.get(TwentyORMManager); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('refreshAndSaveTokens', () => { + it('should reuse valid access token without refreshing', async () => { + const connectedAccount = { + id: mockConnectedAccountId, + provider: ConnectedAccountProvider.MICROSOFT, + accessToken: mockAccessToken, + refreshToken: mockRefreshToken, + } as ConnectedAccountWorkspaceEntity; + + (isAccessTokenExpiredOrInvalid as jest.Mock).mockReturnValue(false); + + const result = await service.refreshAndSaveTokens( + connectedAccount, + mockWorkspaceId, + ); + + expect(result).toEqual({ + accessToken: mockAccessToken, + refreshToken: mockRefreshToken, + }); + expect( + microsoftAPIRefreshAccessTokenService.refreshTokens, + ).not.toHaveBeenCalled(); + expect(twentyORMManager.getRepository).not.toHaveBeenCalled(); + }); + + it('should refresh and save new Microsoft token when expired', async () => { + const connectedAccount = { + id: mockConnectedAccountId, + provider: ConnectedAccountProvider.MICROSOFT, + accessToken: mockAccessToken, + refreshToken: mockRefreshToken, + } as ConnectedAccountWorkspaceEntity; + + const mockRepository = { update: jest.fn() }; + const newTokens = { + accessToken: mockNewAccessToken, + refreshToken: mockRefreshToken, + }; + + (isAccessTokenExpiredOrInvalid as jest.Mock).mockReturnValue(true); + jest + .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') + .mockResolvedValue(newTokens); + jest + .spyOn(twentyORMManager, 'getRepository') + .mockResolvedValue(mockRepository as any); + + const result = await service.refreshAndSaveTokens( + connectedAccount, + mockWorkspaceId, + ); + + expect(result).toEqual(newTokens); + expect( + microsoftAPIRefreshAccessTokenService.refreshTokens, + ).toHaveBeenCalledWith(mockRefreshToken); + expect(mockRepository.update).toHaveBeenCalledWith( + { id: mockConnectedAccountId }, + newTokens, + ); + }); + + it('should throw when refresh token is missing', async () => { + const connectedAccount = { + id: mockConnectedAccountId, + provider: ConnectedAccountProvider.GOOGLE, + accessToken: mockAccessToken, + refreshToken: null, + } as unknown as ConnectedAccountWorkspaceEntity; + + await expect( + service.refreshAndSaveTokens(connectedAccount, mockWorkspaceId), + ).rejects.toThrow( + new ConnectedAccountRefreshAccessTokenException( + `No refresh token found for connected account ${mockConnectedAccountId} in workspace ${mockWorkspaceId}`, + ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND, + ), + ); + }); + + it('should throw when Microsoft refresh fails with axios error', async () => { + const connectedAccount = { + id: mockConnectedAccountId, + provider: ConnectedAccountProvider.MICROSOFT, + accessToken: mockAccessToken, + refreshToken: mockRefreshToken, + } as ConnectedAccountWorkspaceEntity; + + const axiosError = { + message: 'Request failed', + response: { + status: 400, + data: { error: 'invalid_grant', error_description: 'Token expired' }, + }, + }; + + (isAccessTokenExpiredOrInvalid as jest.Mock).mockReturnValue(true); + jest + .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') + .mockRejectedValue(axiosError); + + await expect( + service.refreshAndSaveTokens(connectedAccount, mockWorkspaceId), + ).rejects.toThrow(ConnectedAccountRefreshAccessTokenException); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts index 7c21f79c21..652a91e065 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts @@ -4,8 +4,8 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable } from 'twenty-shared/utils'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service'; import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; +import { isAccessTokenExpiredOrInvalid } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/is-access-token-expired-or-invalid.util'; import { ConnectedAccountRefreshAccessTokenException, ConnectedAccountRefreshAccessTokenExceptionCode, @@ -25,7 +25,6 @@ export class ConnectedAccountRefreshTokensService { ); constructor( - private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService, private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService, private readonly twentyORMManager: TwentyORMManager, ) {} @@ -34,7 +33,7 @@ export class ConnectedAccountRefreshTokensService { connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, ): Promise { - const refreshToken = connectedAccount.refreshToken; + const { refreshToken, accessToken } = connectedAccount; if (!refreshToken) { throw new ConnectedAccountRefreshAccessTokenException( @@ -43,6 +42,26 @@ export class ConnectedAccountRefreshTokensService { ); } + const isAccessTokenValid = await this.checkAccessTokenValidity( + connectedAccount, + accessToken, + ); + + if (isAccessTokenValid) { + this.logger.debug( + `Reusing valid access token for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}`, + ); + + return { + accessToken, + refreshToken, + }; + } + + this.logger.log( + `Access token expired for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}, refreshing...`, + ); + const connectedAccountTokens = await this.refreshTokens( connectedAccount, refreshToken, @@ -62,6 +81,30 @@ export class ConnectedAccountRefreshTokensService { return connectedAccountTokens; } + async checkAccessTokenValidity( + connectedAccount: ConnectedAccountWorkspaceEntity, + accessToken: string, + ): Promise { + switch (connectedAccount.provider) { + case ConnectedAccountProvider.GOOGLE: { + // Google's OAuth2Client handles token auto-refresh internally, no need to check token validity + return true; + } + case ConnectedAccountProvider.MICROSOFT: { + const isExpired = isAccessTokenExpiredOrInvalid(accessToken); + + return !isExpired; + } + case ConnectedAccountProvider.IMAP_SMTP_CALDAV: + return true; + default: + return assertUnreachable( + connectedAccount.provider, + `Provider ${connectedAccount.provider} not supported`, + ); + } + } + async refreshTokens( connectedAccount: ConnectedAccountWorkspaceEntity, refreshToken: string, @@ -70,9 +113,10 @@ export class ConnectedAccountRefreshTokensService { try { switch (connectedAccount.provider) { case ConnectedAccountProvider.GOOGLE: - return await this.googleAPIRefreshAccessTokenService.refreshAccessToken( + return { + accessToken: '', refreshToken, - ); + }; case ConnectedAccountProvider.MICROSOFT: return await this.microsoftAPIRefreshAccessTokenService.refreshTokens( refreshToken, diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts index 3b3d8f4875..8abc2b952e 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts @@ -5,9 +5,9 @@ import { MessageFolderDriver, } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service'; @Injectable() @@ -15,7 +15,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver { private readonly logger = new Logger(GmailGetAllFoldersService.name); constructor( - private readonly gmailClientProvider: GmailClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, private readonly gmailHandleErrorService: GmailHandleErrorService, ) {} @@ -26,12 +26,16 @@ export class GmailGetAllFoldersService implements MessageFolderDriver { async getAllMessageFolders( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' + 'provider' | 'refreshToken' | 'accessToken' | 'id' | 'handle' >, ): Promise { try { - const gmailClient = - await this.gmailClientProvider.getGmailClient(connectedAccount); + const oAuth2Client = + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( + connectedAccount, + ); + + const gmailClient = oAuth2Client.gmail({ version: 'v1' }); const response = await gmailClient.users.labels .list({ userId: 'me' }) diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts index de3f3e2aee..b13209a1b2 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts @@ -5,8 +5,8 @@ import { MessageFolderDriver, } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service'; import { StandardFolder } from 'src/modules/messaging/message-import-manager/drivers/types/standard-folder'; import { getStandardFolderByRegex } from 'src/modules/messaging/message-import-manager/drivers/utils/get-standard-folder-by-regex'; @@ -25,19 +25,22 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver { private readonly logger = new Logger(MicrosoftGetAllFoldersService.name); constructor( - private readonly microsoftClientProvider: MicrosoftClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, + private readonly microsoftHandleErrorService: MicrosoftHandleErrorService, ) {} async getAllMessageFolders( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'refreshToken' | 'id' | 'handle' + 'accessToken' | 'refreshToken' | 'id' | 'handle' | 'provider' >, ): Promise { try { const microsoftClient = - await this.microsoftClientProvider.getMicrosoftClient(connectedAccount); + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, + ); const response = await microsoftClient .api('/me/mailFolders') diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts index 8b22b31885..78d7aed8eb 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts @@ -10,7 +10,12 @@ export interface MessageFolderDriver { getAllMessageFolders( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'handle' + | 'connectionParameters' >, ): Promise; } diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts index f31bea3776..82dc46b6d5 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module.ts @@ -5,6 +5,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service'; import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service'; import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service'; @@ -19,6 +20,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im WorkspaceDataSourceModule, DataSourceModule, TypeOrmModule.forFeature([Workspace]), + OAuth2ClientManagerModule, MessagingGmailDriverModule, MessagingMicrosoftDriverModule, MessagingIMAPDriverModule, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts index 7b4fe879ac..c21bb482d4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts @@ -11,8 +11,6 @@ import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; -import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider'; import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service'; import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service'; import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service'; @@ -36,8 +34,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p MessageParticipantManagerModule, ], providers: [ - GmailClientProvider, - OAuth2ClientProvider, GmailGetHistoryService, GmailFetchByBatchService, GmailGetMessagesService, @@ -47,8 +43,7 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p exports: [ GmailGetMessagesService, GmailGetMessageListService, - GmailClientProvider, - OAuth2ClientProvider, + GmailHandleErrorService, ], }) diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider.ts deleted file mode 100644 index 1ad52337b3..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type gmail_v1, google } from 'googleapis'; - -import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; - -@Injectable() -export class GmailClientProvider { - constructor( - private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, - ) {} - - public async getGmailClient( - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' - >, - ): Promise { - const oAuth2Client = - await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); - - const gmailClient = google.gmail({ - version: 'v1', - auth: oAuth2Client, - }); - - return gmailClient; - } -} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider.ts deleted file mode 100644 index 9d04917909..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type oauth2_v2, google } from 'googleapis'; - -import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; - -@Injectable() -export class OAuth2ClientProvider { - constructor( - private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, - ) {} - - public async getOAuth2Client( - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' - >, - ): Promise { - const oAuth2Client = - await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); - - return google.oauth2({ - version: 'v2', - auth: oAuth2Client, - }); - } -} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.spec.ts index 1305a790f5..579916d93c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.spec.ts @@ -2,23 +2,29 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service'; import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service'; import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service'; describe('GmailGetMessageListService', () => { let service: GmailGetMessageListService; - let gmailClientProvider: GmailClientProvider; + let oAuth2ClientManagerService: OAuth2ClientManagerService; const mockConnectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'handle' + | 'connectionParameters' > = { id: 'connected-account-id', provider: ConnectedAccountProvider.GOOGLE, - refreshToken: 'refresh-token', + accessToken: 'access-token', + refreshToken: 'refresh-token', // dummy value for testing handle: 'test@gmail.com', connectionParameters: {}, }; @@ -28,9 +34,9 @@ describe('GmailGetMessageListService', () => { providers: [ GmailGetMessageListService, { - provide: GmailClientProvider, + provide: OAuth2ClientManagerService, useValue: { - getGmailClient: jest.fn(), + getGoogleOAuth2Client: jest.fn(), }, }, { @@ -53,7 +59,9 @@ describe('GmailGetMessageListService', () => { service = module.get( GmailGetMessageListService, ); - gmailClientProvider = module.get(GmailClientProvider); + oAuth2ClientManagerService = module.get( + OAuth2ClientManagerService, + ); }); describe('getMessageList', () => { @@ -71,9 +79,13 @@ describe('GmailGetMessageListService', () => { }, }; - (gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue( - mockGmailClient, - ); + const mockOAuth2Client = { + gmail: jest.fn().mockReturnValue(mockGmailClient), + }; + + ( + oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock + ).mockResolvedValue(mockOAuth2Client); const result = await service.getMessageLists({ messageChannel: { syncCursor: '', id: 'my-id' }, @@ -122,9 +134,13 @@ describe('GmailGetMessageListService', () => { }, }; - (gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue( - mockGmailClient, - ); + const mockOAuth2Client = { + gmail: jest.fn().mockReturnValue(mockGmailClient), + }; + + ( + oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock + ).mockResolvedValue(mockOAuth2Client); const result = await service.getMessageLists({ messageChannel: { syncCursor: '', id: 'my-id' }, @@ -173,9 +189,13 @@ describe('GmailGetMessageListService', () => { }, }; - (gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue( - mockGmailClient, - ); + const mockOAuth2Client = { + gmail: jest.fn().mockReturnValue(mockGmailClient), + }; + + ( + oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock + ).mockResolvedValue(mockOAuth2Client); const result = await service.getMessageLists({ messageChannel: { syncCursor: '', id: 'my-id' }, @@ -205,9 +225,13 @@ describe('GmailGetMessageListService', () => { }, }; - (gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue( - mockGmailClient, - ); + const mockOAuth2Client = { + gmail: jest.fn().mockReturnValue(mockGmailClient), + }; + + ( + oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock + ).mockResolvedValue(mockOAuth2Client); const result = await service.getMessageLists({ messageChannel: { syncCursor: '', id: 'my-id' }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts index f52f17e264..2b3e031af4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts @@ -4,6 +4,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { type gmail_v1 as gmailV1 } from 'googleapis'; import { isDefined } from 'twenty-shared/utils'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { @@ -11,7 +12,6 @@ import { MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; import { MESSAGING_GMAIL_USERS_MESSAGES_LIST_MAX_RESULT } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-list-max-result.constant'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service'; import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service'; import { computeGmailExcludeSearchFilter } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-gmail-exclude-search-filter.util'; @@ -23,23 +23,27 @@ import { assertNotNull } from 'src/utils/assert'; export class GmailGetMessageListService { private readonly logger = new Logger(GmailGetMessageListService.name); constructor( - private readonly gmailClientProvider: GmailClientProvider, private readonly gmailGetHistoryService: GmailGetHistoryService, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, + private readonly gmailHandleErrorService: GmailHandleErrorService, ) {} private async getMessageListWithoutCursor( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' + 'provider' | 'accessToken' | 'refreshToken' | 'id' | 'handle' >, messageFolders: Pick< MessageFolderWorkspaceEntity, 'name' | 'externalId' | 'isSynced' >[], ): Promise { - const gmailClient = - await this.gmailClientProvider.getGmailClient(connectedAccount); + const oAuth2Client = + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( + connectedAccount, + ); + const gmailClient = oAuth2Client.gmail({ version: 'v1' }); let pageToken: string | undefined; let hasMoreMessages = true; @@ -139,8 +143,11 @@ export class GmailGetMessageListService { connectedAccount, messageFolders, }: GetMessageListsArgs): Promise { - const gmailClient = - await this.gmailClientProvider.getGmailClient(connectedAccount); + const oAuth2Client = + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( + connectedAccount, + ); + const gmailClient = oAuth2Client.gmail({ version: 'v1' }); if (!isNonEmptyString(messageChannel.syncCursor)) { return this.getMessageListWithoutCursor(connectedAccount, messageFolders); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts index ddf8a9d76a..51eea1323d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts @@ -21,7 +21,7 @@ export class GmailGetMessagesService { messageIds: string[], connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'accessToken' | 'refreshToken' | 'id' | 'handle' | 'handleAliases' + 'accessToken' | 'id' | 'handle' | 'handleAliases' >, ): Promise { const { messageIdsByBatch, batchResponses } = diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module.ts index 24ac407b7b..41363252a9 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module.ts @@ -4,10 +4,8 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service'; import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service'; import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service'; @@ -24,17 +22,13 @@ import { MicrosoftGetMessageListService } from './services/microsoft-get-message ObjectMetadataRepositoryModule, ], providers: [ - MicrosoftClientProvider, MicrosoftGetMessageListService, MicrosoftGetMessagesService, - MicrosoftFetchByBatchService, MicrosoftHandleErrorService, - MicrosoftOAuth2ClientManagerService, ], exports: [ MicrosoftGetMessageListService, - MicrosoftClientProvider, MicrosoftGetMessagesService, MicrosoftHandleErrorService, ], diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider.ts deleted file mode 100644 index a88ded94bf..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type Client } from '@microsoft/microsoft-graph-client'; - -import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; - -@Injectable() -export class MicrosoftClientProvider { - constructor( - private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService, - ) {} - - public async getMicrosoftClient( - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - 'refreshToken' | 'id' - >, - ): Promise { - return await this.microsoftOAuth2ClientManagerService.getOAuth2Client( - connectedAccount.refreshToken, - ); - } -} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts index 41082a279a..06418dfa62 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts @@ -1,20 +1,20 @@ import { Injectable } from '@nestjs/common'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { type MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; @Injectable() export class MicrosoftFetchByBatchService { constructor( - private readonly microsoftClientProvider: MicrosoftClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, ) {} async fetchAllByBatches( messageIds: string[], connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'refreshToken' | 'id' + 'accessToken' | 'refreshToken' | 'id' | 'provider' >, ): Promise<{ messageIdsByBatch: string[][]; @@ -25,7 +25,9 @@ export class MicrosoftFetchByBatchService { const messageIdsByBatch: string[][] = []; const client = - await this.microsoftClientProvider.getMicrosoftClient(connectedAccount); + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, + ); for (let i = 0; i < messageIds.length; i += batchLimit) { const batchMessageIds = messageIds.slice(i, i + batchLimit); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.dev.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.dev.spec.ts index 90e70e72ce..120fed27c5 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.dev.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.dev.spec.ts @@ -5,25 +5,32 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { microsoftGraphWithMessagesDeltaLink } from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders'; import { MicrosoftGetMessageListService } from './microsoft-get-message-list.service'; import { MicrosoftHandleErrorService } from './microsoft-handle-error.service'; // in case you have "Please provide a valid token" it may be because you need to pass the env varible to the .env.test file +const accessToken = 'replace-with-your-access-token'; const refreshToken = 'replace-with-your-refresh-token'; const syncCursor = `replace-with-your-sync-cursor`; const mockConnectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'handle' + | 'connectionParameters' > = { id: 'connected-account-id', provider: ConnectedAccountProvider.MICROSOFT, + accessToken: accessToken, refreshToken: refreshToken, handle: 'test@gmail.com', connectionParameters: {}, @@ -45,7 +52,7 @@ xdescribe('Microsoft dev tests : get message list service', () => { imports: [TwentyConfigModule.forRoot()], providers: [ MicrosoftGetMessageListService, - MicrosoftClientProvider, + OAuth2ClientManagerService, MicrosoftHandleErrorService, MicrosoftOAuth2ClientManagerService, ConfigService, @@ -80,6 +87,7 @@ xdescribe('Microsoft dev tests : get message list service', () => { const mockConnectedAccountUnvalid = { id: 'connected-account-id', provider: ConnectedAccountProvider.MICROSOFT, + accessToken: 'invalid-token', refreshToken: 'invalid-token', handle: 'test@microsoft.com', connectionParameters: {}, @@ -195,7 +203,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => { imports: [TwentyConfigModule.forRoot()], providers: [ MicrosoftGetMessageListService, - MicrosoftClientProvider, + OAuth2ClientManagerService, MicrosoftHandleErrorService, MicrosoftOAuth2ClientManagerService, ConfigService, @@ -214,7 +222,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => { }; jest - .spyOn(MicrosoftClientProvider.prototype, 'getMicrosoftClient') + .spyOn(OAuth2ClientManagerService.prototype, 'getMicrosoftOAuth2Client') .mockResolvedValue(mockMicrosoftClient as any); }); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.ts index 6f06a9151b..37589ca807 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service.ts @@ -7,13 +7,13 @@ import { } from '@microsoft/microsoft-graph-client'; import { isNonEmptyString } from '@sniptt/guards'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; import { MessageImportDriverException, MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service'; import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils'; import { type GetMessageListsArgs } from 'src/modules/messaging/message-import-manager/types/get-message-lists-args.type'; @@ -29,7 +29,7 @@ const MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT = 999; export class MicrosoftGetMessageListService { private readonly logger = new Logger(MicrosoftGetMessageListService.name); constructor( - private readonly microsoftClientProvider: MicrosoftClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, private readonly microsoftHandleErrorService: MicrosoftHandleErrorService, ) {} @@ -62,7 +62,7 @@ export class MicrosoftGetMessageListService { public async getMessageList( connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' + 'provider' | 'accessToken' | 'id' >, messageFolder: Pick< MessageFolderWorkspaceEntity, @@ -73,7 +73,9 @@ export class MicrosoftGetMessageListService { const messageExternalIdsToDelete: string[] = []; const microsoftClient = - await this.microsoftClientProvider.getMicrosoftClient(connectedAccount); + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( + connectedAccount, + ); const folderId = messageFolder.externalId || messageFolder.name; const apiUrl = isNonEmptyString(messageFolder.syncCursor) diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.dev.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.dev.spec.ts index 381e82e0e2..1928f1ba01 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.dev.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.dev.spec.ts @@ -5,7 +5,7 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service'; import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service'; @@ -16,6 +16,7 @@ const mockMessageIds = [ 'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA', ]; +const accessToken = 'replace-with-your-access-token'; const refreshToken = 'replace-with-your-refresh-token'; xdescribe('Microsoft dev tests : get messages service', () => { @@ -27,7 +28,7 @@ xdescribe('Microsoft dev tests : get messages service', () => { providers: [ MicrosoftGetMessagesService, MicrosoftHandleErrorService, - MicrosoftClientProvider, + OAuth2ClientManagerService, MicrosoftOAuth2ClientManagerService, MicrosoftFetchByBatchService, ConfigService, @@ -44,6 +45,7 @@ xdescribe('Microsoft dev tests : get messages service', () => { provider: ConnectedAccountProvider.MICROSOFT, handle: 'John.Walker@outlook.fr', handleAliases: '', + accessToken: accessToken, refreshToken: refreshToken, }; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts index 8061b97346..c200d59dee 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts @@ -1,15 +1,17 @@ import { ConfigService } from '@nestjs/config'; +import { Logger } from '@nestjs/common'; import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service'; import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { microsoftGraphBatchWithHtmlMessagesResponse, microsoftGraphBatchWithTwoMessagesResponse, } from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service'; import { type MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service'; @@ -24,7 +26,8 @@ describe('Microsoft get messages service', () => { providers: [ MicrosoftGetMessagesService, MicrosoftHandleErrorService, - MicrosoftClientProvider, + OAuth2ClientManagerService, + GoogleOAuth2ClientManagerService, MicrosoftOAuth2ClientManagerService, MicrosoftFetchByBatchService, ConfigService, @@ -32,6 +35,10 @@ describe('Microsoft get messages service', () => { provide: TwentyConfigService, useValue: {}, }, + { + provide: Logger, + useValue: { log: jest.fn(), error: jest.fn() }, + }, ], }).compile(); @@ -54,6 +61,7 @@ describe('Microsoft get messages service', () => { const connectedAccount = { id: 'connected-account-id', provider: ConnectedAccountProvider.MICROSOFT, + accessToken: 'access-token', refreshToken: 'refresh-token', handle: 'John.l@outlook.fr', handleAliases: '', @@ -148,6 +156,7 @@ describe('Microsoft get messages service', () => { const connectedAccount = { id: 'connected-account-id', provider: ConnectedAccountProvider.MICROSOFT, + accessToken: 'access-token', refreshToken: 'refresh-token', handle: 'John.l@outlook.fr', handleAliases: '', diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts index 3207ba9be0..cfe5f36264 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts @@ -17,7 +17,12 @@ import { MicrosoftHandleErrorService } from './microsoft-handle-error.service'; type ConnectedAccountType = Pick< ConnectedAccountWorkspaceEntity, - 'refreshToken' | 'id' | 'provider' | 'handle' | 'handleAliases' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'provider' + | 'handle' + | 'handleAliases' >; @Injectable() diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index 3f1b0a8314..96fc2cbcea 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -9,6 +9,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; +import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { RefreshTokensManagerModule } from 'src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module'; @@ -47,6 +48,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess imports: [ RefreshTokensManagerModule, WorkspaceDataSourceModule, + OAuth2ClientManagerModule, MessagingGmailDriverModule, MessagingMicrosoftDriverModule, MessagingIMAPDriverModule, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-send-message-gmail.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-send-message-gmail.spec.ts index f0bd62e8fa..6b3110d638 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-send-message-gmail.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-send-message-gmail.spec.ts @@ -2,17 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; -import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider'; import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; describe('MessagingSendMessageService - Gmail HTML Support', () => { let service: MessagingSendMessageService; - let gmailClientProvider: jest.Mocked; - let oAuth2ClientProvider: jest.Mocked; + let oAuth2ClientManagerService: OAuth2ClientManagerService; beforeEach(async () => { const mockGmailClient = { @@ -24,6 +21,7 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { }; const mockOAuth2Client = { + gmail: jest.fn().mockReturnValue(mockGmailClient), userinfo: { get: jest.fn().mockResolvedValue({ data: { email: 'test@example.com', name: 'Test User' }, @@ -35,21 +33,13 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { providers: [ MessagingSendMessageService, { - provide: GmailClientProvider, + provide: OAuth2ClientManagerService, useValue: { - getGmailClient: jest.fn().mockResolvedValue(mockGmailClient), + getGoogleOAuth2Client: jest + .fn() + .mockResolvedValue(mockOAuth2Client), }, }, - { - provide: OAuth2ClientProvider, - useValue: { - getOAuth2Client: jest.fn().mockResolvedValue(mockOAuth2Client), - }, - }, - { - provide: MicrosoftClientProvider, - useValue: {}, - }, { provide: SmtpClientProvider, useValue: {}, @@ -64,8 +54,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { service = module.get( MessagingSendMessageService, ); - gmailClientProvider = module.get(GmailClientProvider); - oAuth2ClientProvider = module.get(OAuth2ClientProvider); + oAuth2ClientManagerService = module.get( + OAuth2ClientManagerService, + ); }); it('should send multipart/alternative email with both text and HTML parts via Gmail', async () => { @@ -84,8 +75,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { await service.sendMessage(sendMessageInput, connectedAccount); - const gmailClient = - await gmailClientProvider.getGmailClient(connectedAccount); + const mockOAuth2Client = + await oAuth2ClientManagerService.getGoogleOAuth2Client(connectedAccount); + const gmailClient = mockOAuth2Client.gmail({ version: 'v1' }); const sendCall = gmailClient.users.messages.send as jest.Mock; expect(sendCall).toHaveBeenCalledTimes(1); @@ -111,7 +103,16 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { }); it('should handle missing fromName gracefully', async () => { + const mockGmailClient = { + users: { + messages: { + send: jest.fn().mockResolvedValue({ data: { id: 'message-id' } }), + }, + }, + }; + const mockOAuth2ClientNoName = { + gmail: jest.fn().mockReturnValue(mockGmailClient), userinfo: { get: jest.fn().mockResolvedValue({ data: { email: 'test@example.com' }, // No name field @@ -119,9 +120,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { }, }; - (oAuth2ClientProvider.getOAuth2Client as jest.Mock).mockResolvedValueOnce( - mockOAuth2ClientNoName, - ); + ( + oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock + ).mockResolvedValueOnce(mockOAuth2ClientNoName); const sendMessageInput = { to: 'recipient@example.com', @@ -138,9 +139,10 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => { await service.sendMessage(sendMessageInput, connectedAccount); - const gmailClient = - await gmailClientProvider.getGmailClient(connectedAccount); - const sendCall = gmailClient.users.messages.send as jest.Mock; + const sendCall = mockGmailClient.users.messages.send as jest.Mock; + + expect(sendCall).toHaveBeenCalledTimes(1); + const rawMessage = Buffer.from( sendCall.mock.calls[0][0].requestBody.raw, 'base64', diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-account-authentication.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-account-authentication.service.ts index ee95266698..bbc018bdf0 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-account-authentication.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-account-authentication.service.ts @@ -49,7 +49,7 @@ export class MessagingAccountAuthenticationService { }; } - return await this.refreshAccessTokenForNonImapProvider({ + return await this.refreshAccessTokenForOAuthProvider({ connectedAccount, workspaceId, messageChannelId, @@ -79,7 +79,7 @@ export class MessagingAccountAuthenticationService { } } - private async refreshAccessTokenForNonImapProvider({ + private async refreshAccessTokenForOAuthProvider({ connectedAccount, workspaceId, messageChannelId, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-send-message.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-send-message.service.ts index 7c50aeb6ec..80335ea785 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-send-message.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-send-message.service.ts @@ -5,15 +5,13 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageImportDriverException, MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; -import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider'; import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider'; -import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils'; import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider'; import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util'; @@ -28,9 +26,7 @@ interface SendMessageInput { @Injectable() export class MessagingSendMessageService { constructor( - private readonly gmailClientProvider: GmailClientProvider, - private readonly oAuth2ClientProvider: OAuth2ClientProvider, - private readonly microsoftClientProvider: MicrosoftClientProvider, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, private readonly smtpClientProvider: SmtpClientProvider, private readonly imapClientProvider: ImapClientProvider, ) {} @@ -41,11 +37,14 @@ export class MessagingSendMessageService { ): Promise { switch (connectedAccount.provider) { case ConnectedAccountProvider.GOOGLE: { - const gmailClient = - await this.gmailClientProvider.getGmailClient(connectedAccount); - const oAuth2Client = - await this.oAuth2ClientProvider.getOAuth2Client(connectedAccount); + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( + connectedAccount, + ); + + const gmailClient = oAuth2Client.gmail({ + version: 'v1', + }); const { data } = await oAuth2Client.userinfo.get(); @@ -93,7 +92,7 @@ export class MessagingSendMessageService { } case ConnectedAccountProvider.MICROSOFT: { const microsoftClient = - await this.microsoftClientProvider.getMicrosoftClient( + await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client( connectedAccount, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/types/get-message-lists-args.type.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/types/get-message-lists-args.type.ts index aa7d77ea23..078b71a66a 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/types/get-message-lists-args.type.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/types/get-message-lists-args.type.ts @@ -6,7 +6,12 @@ export type GetMessageListsArgs = { messageChannel: Pick; connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'handle' + | 'connectionParameters' >; messageFolders: Pick< MessageFolderWorkspaceEntity,