Reuse OAuth access tokens (#15089)

This commit is contained in:
neo773
2025-10-21 15:08:49 +05:30
committed by GitHub
parent 793dc3d6fc
commit c66261a38b
49 changed files with 632 additions and 421 deletions
@@ -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,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 {}
@@ -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;
}
}
@@ -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[] = [];
@@ -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,
@@ -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[] = [];
@@ -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[] = [];
@@ -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;
}
}
}
}
@@ -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,
);
@@ -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> {