microsoft sync failed (#10381)
This PR is supposed to solve an issue with the syncrhonisation of messages, specifically with microsoft driver. Microsoft calls don't need access_Token so refreshing toekns was not implemented. However, microsoft rely on its client which calls its refresfh_token, and I might have missed some underlying dependency from microsoft impelemtation so I setup the access token process to refresh it Needs a talk before to be merged Fix : https://github.com/twentyhq/twenty/issues/10367 EDIT: it was a problem with microsoft making refreshtoken expire (contrarily to google) which needs to be handled.
This commit is contained in:
@ -1,11 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module';
|
||||
import { RefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service';
|
||||
|
||||
@Module({
|
||||
imports: [GoogleAPIRefreshAccessTokenModule],
|
||||
providers: [RefreshAccessTokenService],
|
||||
exports: [RefreshAccessTokenService],
|
||||
})
|
||||
export class RefreshAccessTokenManagerModule {}
|
||||
@ -1,83 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service';
|
||||
import {
|
||||
RefreshAccessTokenException,
|
||||
RefreshAccessTokenExceptionCode,
|
||||
} from 'src/modules/connected-account/refresh-access-token-manager/exceptions/refresh-access-token.exception';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshAccessTokenService {
|
||||
constructor(
|
||||
private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
async refreshAndSaveAccessToken(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const refreshToken = connectedAccount.refreshToken;
|
||||
let accessToken: string;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new RefreshAccessTokenException(
|
||||
`No refresh token found for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
|
||||
RefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
switch (connectedAccount.provider) {
|
||||
case 'microsoft':
|
||||
return '';
|
||||
case 'google': {
|
||||
try {
|
||||
accessToken = await this.refreshAccessToken(
|
||||
connectedAccount,
|
||||
refreshToken,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new RefreshAccessTokenException(
|
||||
`Error refreshing access token for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.message}`,
|
||||
RefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
accessToken,
|
||||
},
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
default:
|
||||
throw new Error('Provider not supported for access token refresh');
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAccessToken(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
refreshToken: string,
|
||||
): Promise<string> {
|
||||
switch (connectedAccount.provider) {
|
||||
case 'google':
|
||||
return this.googleAPIRefreshAccessTokenService.refreshAccessToken(
|
||||
refreshToken,
|
||||
);
|
||||
default:
|
||||
throw new RefreshAccessTokenException(
|
||||
`Provider ${connectedAccount.provider} is not supported`,
|
||||
RefreshAccessTokenExceptionCode.PROVIDER_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.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,
|
||||
],
|
||||
providers: [ConnectedAccountRefreshTokensService],
|
||||
exports: [ConnectedAccountRefreshTokensService],
|
||||
})
|
||||
export class RefreshTokensManagerModule {}
|
||||
@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service';
|
||||
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({
|
||||
@ -1,15 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.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 environmentService: EnvironmentService) {}
|
||||
|
||||
async refreshAccessToken(refreshToken: string): Promise<string> {
|
||||
const response = await axios.post(
|
||||
async refreshAccessToken(refreshToken: string): Promise<GoogleTokens> {
|
||||
const response = await axios.post<GoogleRefreshTokenResponse>(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
{
|
||||
client_id: this.environmentService.get('AUTH_GOOGLE_CLIENT_ID'),
|
||||
@ -24,6 +36,10 @@ export class GoogleAPIRefreshAccessTokenService {
|
||||
},
|
||||
);
|
||||
|
||||
return response.data.access_token;
|
||||
z.string().parse(response.data.access_token);
|
||||
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
|
||||
@Module({
|
||||
imports: [EnvironmentModule],
|
||||
providers: [MicrosoftAPIRefreshAccessTokenService],
|
||||
exports: [MicrosoftAPIRefreshAccessTokenService],
|
||||
})
|
||||
export class MicrosoftAPIRefreshAccessTokenModule {}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type MicrosoftTokens = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
interface MicrosoftRefreshTokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
id_token?: string;
|
||||
}
|
||||
@Injectable()
|
||||
export class MicrosoftAPIRefreshAccessTokenService {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
|
||||
async refreshTokens(refreshToken: string): Promise<MicrosoftTokens> {
|
||||
const response = await axios.post<MicrosoftRefreshTokenResponse>(
|
||||
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
new URLSearchParams({
|
||||
client_id: this.environmentService.get('AUTH_MICROSOFT_CLIENT_ID'),
|
||||
client_secret: this.environmentService.get(
|
||||
'AUTH_MICROSOFT_CLIENT_SECRET',
|
||||
),
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
}).parse(response.data);
|
||||
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class RefreshAccessTokenException extends CustomException {
|
||||
constructor(message: string, code: RefreshAccessTokenExceptionCode) {
|
||||
export class ConnectedAccountRefreshAccessTokenException extends CustomException {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ConnectedAccountRefreshAccessTokenExceptionCode,
|
||||
) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum RefreshAccessTokenExceptionCode {
|
||||
export enum ConnectedAccountRefreshAccessTokenExceptionCode {
|
||||
REFRESH_TOKEN_NOT_FOUND = 'REFRESH_TOKEN_NOT_FOUND',
|
||||
REFRESH_ACCESS_TOKEN_FAILED = 'REFRESH_ACCESS_TOKEN_FAILED',
|
||||
PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED',
|
||||
@ -0,0 +1,96 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { assertUnreachable, ConnectedAccountProvider } from 'twenty-shared';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import {
|
||||
GoogleAPIRefreshAccessTokenService,
|
||||
GoogleTokens,
|
||||
} from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-access-token.service';
|
||||
import {
|
||||
MicrosoftAPIRefreshAccessTokenService,
|
||||
MicrosoftTokens,
|
||||
} from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
import {
|
||||
ConnectedAccountRefreshAccessTokenException,
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode,
|
||||
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export type ConnectedAccountTokens = GoogleTokens | MicrosoftTokens;
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountRefreshTokensService {
|
||||
constructor(
|
||||
private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService,
|
||||
private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
async refreshAndSaveTokens(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const refreshToken = connectedAccount.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new ConnectedAccountRefreshAccessTokenException(
|
||||
`No refresh token found for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const connectedAccountTokens = await this.refreshTokens(
|
||||
connectedAccount,
|
||||
refreshToken,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
try {
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id },
|
||||
connectedAccountTokens,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error saving the new tokens for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.message} `,
|
||||
);
|
||||
}
|
||||
|
||||
return connectedAccountTokens.accessToken;
|
||||
}
|
||||
|
||||
async refreshTokens(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
refreshToken: string,
|
||||
workspaceId: string,
|
||||
): Promise<ConnectedAccountTokens> {
|
||||
try {
|
||||
switch (connectedAccount.provider) {
|
||||
case ConnectedAccountProvider.GOOGLE:
|
||||
return this.googleAPIRefreshAccessTokenService.refreshAccessToken(
|
||||
refreshToken,
|
||||
);
|
||||
case ConnectedAccountProvider.MICROSOFT:
|
||||
return this.microsoftAPIRefreshAccessTokenService.refreshTokens(
|
||||
refreshToken,
|
||||
);
|
||||
default:
|
||||
return assertUnreachable(
|
||||
connectedAccount.provider,
|
||||
`Provider ${connectedAccount.provider} not supported`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ConnectedAccountRefreshAccessTokenException(
|
||||
`Error refreshing tokens for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.message} ${error?.response?.data?.error_description}`,
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user