Reuse OAuth access tokens (#15089)
This commit is contained in:
@@ -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 {
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
-2
@@ -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 {}
|
||||
|
||||
-30
@@ -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<calendarV3.Calendar> {
|
||||
const oAuth2Client =
|
||||
await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount);
|
||||
|
||||
const googleCalendarClient = google.calendar({
|
||||
version: 'v3',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
return googleCalendarClient;
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -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<GetCalendarEventsResponse> {
|
||||
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[] = [];
|
||||
|
||||
+2
-4
@@ -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,
|
||||
|
||||
+5
-5
@@ -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<GetCalendarEventsResponse> {
|
||||
try {
|
||||
const microsoftClient =
|
||||
await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
|
||||
connectedAccount.refreshToken,
|
||||
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
const eventIds: string[] = [];
|
||||
|
||||
|
||||
+5
-5
@@ -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<FetchedCalendarEvent[]> {
|
||||
try {
|
||||
const microsoftClient =
|
||||
await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
|
||||
connectedAccount.refreshToken,
|
||||
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const events: Event[] = [];
|
||||
|
||||
+98
@@ -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<ConnectedAccountTokens> {
|
||||
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<void> {
|
||||
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<ConnectedAccountTokens> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
+6
-1
@@ -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<GetCalendarEventsResponse> {
|
||||
|
||||
+4
-5
@@ -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({
|
||||
|
||||
+5
-3
@@ -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')
|
||||
|
||||
-2
@@ -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],
|
||||
})
|
||||
|
||||
+4
-4
@@ -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()
|
||||
|
||||
+5
-4
@@ -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<OAuth2Client> {
|
||||
public async getOAuth2Client(refreshToken: string): Promise<GoogleApis> {
|
||||
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}`,
|
||||
|
||||
+11
@@ -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<string> {
|
||||
return this.accessToken;
|
||||
}
|
||||
}
|
||||
+7
-67
@@ -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<Client> {
|
||||
const authProvider = new MicrosoftOAuth2ClientAuthProvider(accessToken);
|
||||
|
||||
public async getOAuth2Client(refreshToken: string): Promise<Client> {
|
||||
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;
|
||||
|
||||
+19
-15
@@ -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<OAuth2Client> {
|
||||
const { refreshToken } = connectedAccount;
|
||||
): Promise<GoogleApis> {
|
||||
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<Client> {
|
||||
return this.microsoftOAuth2ClientManagerService.getOAuth2Client(
|
||||
connectedAccount.accessToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -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],
|
||||
})
|
||||
|
||||
-11
@@ -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 {}
|
||||
-51
@@ -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<ConnectedAccountTokens> {
|
||||
const response = await axios.post<GoogleRefreshTokenResponse>(
|
||||
'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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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],
|
||||
})
|
||||
|
||||
+25
@@ -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;
|
||||
}
|
||||
};
|
||||
+171
@@ -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>(
|
||||
ConnectedAccountRefreshTokensService,
|
||||
);
|
||||
microsoftAPIRefreshAccessTokenService =
|
||||
module.get<MicrosoftAPIRefreshAccessTokenService>(
|
||||
MicrosoftAPIRefreshAccessTokenService,
|
||||
);
|
||||
twentyORMManager = module.get<TwentyORMManager>(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+49
-5
@@ -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<ConnectedAccountTokens> {
|
||||
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<boolean> {
|
||||
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,
|
||||
|
||||
+9
-5
@@ -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<MessageFolder[]> {
|
||||
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' })
|
||||
|
||||
+7
-4
@@ -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<MessageFolder[]> {
|
||||
try {
|
||||
const microsoftClient =
|
||||
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);
|
||||
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const response = await microsoftClient
|
||||
.api('/me/mailFolders')
|
||||
|
||||
+6
-1
@@ -10,7 +10,12 @@ export interface MessageFolderDriver {
|
||||
getAllMessageFolders(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
|
||||
| 'provider'
|
||||
| 'accessToken'
|
||||
| 'refreshToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
>,
|
||||
): Promise<MessageFolder[]>;
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
-6
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
-30
@@ -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<gmail_v1.Gmail> {
|
||||
const oAuth2Client =
|
||||
await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount);
|
||||
|
||||
const gmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
return gmailClient;
|
||||
}
|
||||
}
|
||||
-28
@@ -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<oauth2_v2.Oauth2> {
|
||||
const oAuth2Client =
|
||||
await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount);
|
||||
|
||||
return google.oauth2({
|
||||
version: 'v2',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
}
|
||||
}
|
||||
+43
-19
@@ -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>(
|
||||
GmailGetMessageListService,
|
||||
);
|
||||
gmailClientProvider = module.get<GmailClientProvider>(GmailClientProvider);
|
||||
oAuth2ClientManagerService = module.get<OAuth2ClientManagerService>(
|
||||
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' },
|
||||
|
||||
+14
-7
@@ -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<GetMessageListsResponse> {
|
||||
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<GetMessageListsResponse> {
|
||||
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);
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export class GmailGetMessagesService {
|
||||
messageIds: string[],
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'handleAliases'
|
||||
'accessToken' | 'id' | 'handle' | 'handleAliases'
|
||||
>,
|
||||
): Promise<MessageWithParticipants[]> {
|
||||
const { messageIdsByBatch, batchResponses } =
|
||||
|
||||
-6
@@ -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,
|
||||
],
|
||||
|
||||
-24
@@ -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<Client> {
|
||||
return await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
|
||||
connectedAccount.refreshToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -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);
|
||||
|
||||
+13
-5
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+6
-4
@@ -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)
|
||||
|
||||
+4
-2
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
+11
-2
@@ -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: '',
|
||||
|
||||
+6
-1
@@ -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()
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+29
-27
@@ -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<GmailClientProvider>;
|
||||
let oAuth2ClientProvider: jest.Mocked<OAuth2ClientProvider>;
|
||||
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>(
|
||||
MessagingSendMessageService,
|
||||
);
|
||||
gmailClientProvider = module.get(GmailClientProvider);
|
||||
oAuth2ClientProvider = module.get(OAuth2ClientProvider);
|
||||
oAuth2ClientManagerService = module.get<OAuth2ClientManagerService>(
|
||||
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',
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+10
-11
@@ -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<void> {
|
||||
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,
|
||||
);
|
||||
|
||||
|
||||
+6
-1
@@ -6,7 +6,12 @@ export type GetMessageListsArgs = {
|
||||
messageChannel: Pick<MessageChannelWorkspaceEntity, 'syncCursor' | 'id'>;
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
|
||||
| 'provider'
|
||||
| 'accessToken'
|
||||
| 'refreshToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'connectionParameters'
|
||||
>;
|
||||
messageFolders: Pick<
|
||||
MessageFolderWorkspaceEntity,
|
||||
|
||||
Reference in New Issue
Block a user