[calendar/messaging] fix connected account auth failed should skip sync (#4920)

- AuthFailedAt is set when a refreshToken is not valid and an
accessToken can't be generated, meaning it will need a manual action
from the user to provide a new refresh token.
- Calendar/messaging jobs should not be executed if authFailedAt is not
null.
This commit is contained in:
Weiko
2024-04-11 17:57:48 +02:00
committed by GitHub
parent 8853408264
commit fc56775c2a
12 changed files with 342 additions and 208 deletions

View File

@ -27,6 +27,30 @@ export class CalendarChannelRepository {
);
}
public async create(
calendarChannel: Pick<
ObjectRecord<CalendarChannelObjectMetadata>,
'id' | 'connectedAccountId' | 'handle' | 'visibility'
>,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarChannel" (id, "connectedAccountId", "handle", "visibility") VALUES ($1, $2, $3, $4)`,
[
calendarChannel.id,
calendarChannel.connectedAccountId,
calendarChannel.handle,
calendarChannel.visibility,
],
workspaceId,
transactionManager,
);
}
public async getByConnectedAccountId(
connectedAccountId: string,
workspaceId: string,

View File

@ -43,6 +43,75 @@ export class ConnectedAccountRepository {
);
}
public async getAllByHandleAndWorkspaceMemberId(
handle: string,
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[] | undefined> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const connectedAccounts =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "handle" = $1 AND "accountOwnerId" = $2 LIMIT 1`,
[handle, workspaceMemberId],
workspaceId,
transactionManager,
);
return connectedAccounts;
}
public async create(
connectedAccount: Pick<
ObjectRecord<ConnectedAccountObjectMetadata>,
| 'id'
| 'handle'
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'accountOwnerId'
>,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."connectedAccount" ("id", "handle", "provider", "accessToken", "refreshToken", "accountOwnerId") VALUES ($1, $2, $3, $4, $5, $6)`,
[
connectedAccount.id,
connectedAccount.handle,
connectedAccount.provider,
connectedAccount.accessToken,
connectedAccount.refreshToken,
connectedAccount.accountOwnerId,
],
workspaceId,
transactionManager,
);
}
public async updateAccessTokenAndRefreshToken(
accessToken: string,
refreshToken: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "accessToken" = $1, "refreshToken" = $2 WHERE "id" = $3`,
[accessToken, refreshToken, connectedAccountId],
workspaceId,
transactionManager,
);
}
public async getById(
connectedAccountId: string,
workspaceId: string,

View File

@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
@ -14,7 +13,6 @@ export class GoogleAPIRefreshAccessTokenService {
private readonly environmentService: EnvironmentService,
@InjectObjectMetadataRepository(ConnectedAccountObjectMetadata)
private readonly connectedAccountRepository: ConnectedAccountRepository,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
async refreshAndSaveAccessToken(
@ -32,6 +30,12 @@ export class GoogleAPIRefreshAccessTokenService {
);
}
if (connectedAccount.authFailedAt) {
throw new Error(
`Skipping refresh of access token for connected account ${connectedAccountId} in workspace ${workspaceId} because auth already failed, a new refresh token is needed`,
);
}
const refreshToken = connectedAccount.refreshToken;
if (!refreshToken) {
@ -82,11 +86,7 @@ export class GoogleAPIRefreshAccessTokenService {
connectedAccountId,
workspaceId,
);
this.exceptionHandlerService.captureExceptions([error], {
user: {
workspaceId,
},
});
throw new Error(`Error refreshing access token: ${error.message}`);
}
}

View File

@ -17,6 +17,10 @@ import { CalendarChannelObjectMetadata } from 'src/modules/calendar/standard-obj
import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
export enum ConnectedAccountProvider {
GOOGLE = 'google',
}
@ObjectMetadata({
standardId: standardObjectIds.connectedAccount,
namePlural: 'connectedAccounts',
@ -43,7 +47,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata {
description: 'The account provider',
icon: 'IconSettings',
})
provider: string;
provider: ConnectedAccountProvider; // field metadata should be a SELECT
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.accessToken,

View File

@ -15,6 +15,49 @@ export class MessageChannelRepository {
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async create(
messageChannel: Pick<
ObjectRecord<MessageChannelObjectMetadata>,
'id' | 'connectedAccountId' | 'type' | 'handle' | 'visibility'
>,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."messageChannel" ("id", "connectedAccountId", "type", "handle", "visibility")
VALUES ($1, $2, $3, $4, $5)`,
[
messageChannel.id,
messageChannel.connectedAccountId,
messageChannel.type,
messageChannel.handle,
messageChannel.visibility,
],
workspaceId,
transactionManager,
);
}
public async resetSync(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."messageChannel" SET "syncStatus" = NULL, "syncCursor" = '', "ongoingSyncStartedAt" = NULL
WHERE "connectedAccountId" = $1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
}
public async getAll(
workspaceId: string,
transactionManager?: EntityManager,

View File

@ -63,6 +63,14 @@ export class GmailFetchMessageContentFromCacheService {
return;
}
if (connectedAccount.authFailedAt) {
this.logger.error(
`Connected account ${connectedAccountId} in workspace ${workspaceId} is in a failed state. Skipping...`,
);
return;
}
const accessToken = connectedAccount.accessToken;
const refreshToken = connectedAccount.refreshToken;
@ -233,6 +241,14 @@ export class GmailFetchMessageContentFromCacheService {
messageIdsToFetch,
);
if (error?.message?.code === 429) {
this.logger.error(
`Error fetching messages for ${connectedAccountId} in workspace ${workspaceId}: Resource has been exhausted, locking for ${GMAIL_ONGOING_SYNC_TIMEOUT}ms...`,
);
return;
}
await this.messageChannelRepository.updateSyncStatus(
gmailMessageChannelId,
MessageChannelSyncStatus.FAILED,

View File

@ -21,6 +21,17 @@ export enum MessageChannelSyncStatus {
FAILED = 'FAILED',
}
export enum MessageChannelVisibility {
METADATA = 'metadata',
SUBJECT = 'subject',
SHARE_EVERYTHING = 'share_everything',
}
export enum MessageChannelType {
EMAIL = 'email',
SMS = 'sms',
}
@ObjectMetadata({
standardId: standardObjectIds.messageChannel,
namePlural: 'messageChannels',
@ -38,16 +49,26 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
description: 'Visibility',
icon: 'IconEyeglass',
options: [
{ value: 'metadata', label: 'Metadata', position: 0, color: 'green' },
{ value: 'subject', label: 'Subject', position: 1, color: 'blue' },
{
value: 'share_everything',
value: MessageChannelVisibility.METADATA,
label: 'Metadata',
position: 0,
color: 'green',
},
{
value: MessageChannelVisibility.SUBJECT,
label: 'Subject',
position: 1,
color: 'blue',
},
{
value: MessageChannelVisibility.SHARE_EVERYTHING,
label: 'Share Everything',
position: 2,
color: 'orange',
},
],
defaultValue: "'share_everything'",
defaultValue: MessageChannelVisibility.SHARE_EVERYTHING,
})
visibility: string;
@ -77,10 +98,20 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
description: 'Channel Type',
icon: 'IconMessage',
options: [
{ value: 'email', label: 'Email', position: 0, color: 'green' },
{ value: 'sms', label: 'SMS', position: 1, color: 'blue' },
{
value: MessageChannelType.EMAIL,
label: 'Email',
position: 0,
color: 'green',
},
{
value: MessageChannelType.SMS,
label: 'SMS',
position: 1,
color: 'blue',
},
],
defaultValue: "'email'",
defaultValue: MessageChannelType.EMAIL,
})
type: string;