From 943acec8e42b96673bcbf24511a8e1f23ce6120d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 4 Dec 2025 14:12:33 +0700 Subject: [PATCH] fix: refresh token unified, and found some bugs --- .../src/api/routes/integrations.controller.ts | 41 +++----- .../chat/tools/integration.trigger.tool.ts | 51 ++++------ .../src/database/prisma/database.module.ts | 2 + .../integrations/integration.service.ts | 96 ++++--------------- .../database/prisma/posts/posts.service.ts | 56 ++--------- .../refresh.integration.service.ts | 84 ++++++++++++++++ .../integrations/social/facebook.provider.ts | 4 +- .../src/integrations/social/gmb.provider.ts | 4 +- .../integrations/social/instagram.provider.ts | 5 +- .../social/linkedin.page.provider.ts | 4 +- .../social/social.integrations.interface.ts | 2 +- .../integrations/social/youtube.provider.ts | 4 +- 12 files changed, 154 insertions(+), 199 deletions(-) create mode 100644 libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 3a8a72e8..78179693 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -38,6 +38,7 @@ import { Sections, } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; import { uniqBy } from 'lodash'; +import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; @ApiTags('Integrations') @Controller('/integrations') @@ -45,7 +46,8 @@ export class IntegrationsController { constructor( private _integrationManager: IntegrationManager, private _integrationService: IntegrationService, - private _postService: PostsService + private _postService: PostsService, + private _refreshIntegrationService: RefreshIntegrationService ) {} @Get('/') getIntegrations() { @@ -338,37 +340,24 @@ export class IntegrationsController { return load; } catch (err) { if (err instanceof RefreshToken) { - const { accessToken, refreshToken, expiresIn, additionalSettings } = - await integrationProvider.refreshToken(getIntegration.refreshToken); + const data = await this._refreshIntegrationService.refresh( + getIntegration + ); + + if (!data) { + return; + } + + const { accessToken } = data; if (accessToken) { - await this._integrationService.createOrUpdateIntegration( - additionalSettings, - !!integrationProvider.oneTimeToken, - getIntegration.organizationId, - getIntegration.name, - getIntegration.picture!, - 'social', - getIntegration.internalId, - getIntegration.providerIdentifier, - accessToken, - refreshToken, - expiresIn - ); - - getIntegration.token = accessToken; - if (integrationProvider.refreshWait) { await timer(10000); } return this.functionIntegration(org, body); - } else { - await this._integrationService.disconnectChannel( - org.id, - getIntegration - ); - return false; } + + return false; } return false; @@ -459,7 +448,7 @@ export class IntegrationsController { refresh, auth.accessToken ); - return res(newAuth); + return res({ ...newAuth, refreshToken: body.refresh }); } return res(auth); diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts index b548d22c..9de31dc2 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts @@ -11,12 +11,14 @@ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/in import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { timer } from '@gitroom/helpers/utils/timer'; import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context'; +import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; @Injectable() export class IntegrationTriggerTool implements AgentToolInterface { constructor( private _integrationManager: IntegrationManager, - private _integrationService: IntegrationService + private _integrationService: IntegrationService, + private _refreshIntegrationService: RefreshIntegrationService ) {} name = 'triggerTool'; @@ -103,40 +105,12 @@ export class IntegrationTriggerTool implements AgentToolInterface { return { output: load }; } catch (err) { - console.log(err); if (err instanceof RefreshToken) { - const { - accessToken, - refreshToken, - expiresIn, - additionalSettings, - } = await integrationProvider.refreshToken( - getIntegration.refreshToken + const data = await this._refreshIntegrationService.refresh( + getIntegration ); - if (accessToken) { - await this._integrationService.createOrUpdateIntegration( - additionalSettings, - !!integrationProvider.oneTimeToken, - getIntegration.organizationId, - getIntegration.name, - getIntegration.picture!, - 'social', - getIntegration.internalId, - getIntegration.providerIdentifier, - accessToken, - refreshToken, - expiresIn - ); - - getIntegration.token = accessToken; - - if (integrationProvider.refreshWait) { - await timer(10000); - } - - continue; - } else { + if (!data) { await this._integrationService.disconnectChannel( organizationId, getIntegration @@ -146,6 +120,19 @@ export class IntegrationTriggerTool implements AgentToolInterface { 'We had to disconnect the channel as the token expired', }; } + + const { accessToken } = data; + + if (accessToken) { + getIntegration.token = accessToken; + + if (integrationProvider.refreshWait) { + await timer(10000); + } + + continue; + } else { + } } return { output: 'Unexpected error' }; } diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts index 17198f05..73763d0e 100644 --- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts +++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts @@ -41,6 +41,7 @@ import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/ import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.service'; import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager'; import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service'; +import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; @Global() @Module({ @@ -80,6 +81,7 @@ import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service'; ItemUserService, MessagesService, IntegrationManager, + RefreshIntegrationService, ExtractContentService, OpenaiService, FalService, diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index e55f11d8..0ce96727 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; import { @@ -19,6 +19,7 @@ import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/cl import { difference, uniq } from 'lodash'; import utc from 'dayjs/plugin/utc'; import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository'; +import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; dayjs.extend(utc); @@ -30,7 +31,9 @@ export class IntegrationService { private _autopostsRepository: AutopostRepository, private _integrationManager: IntegrationManager, private _notificationService: NotificationService, - private _workerServiceProducer: BullMqClient + private _workerServiceProducer: BullMqClient, + @Inject(forwardRef(() => RefreshIntegrationService)) + private _refreshIntegrationService: RefreshIntegrationService ) {} async changeActiveCron(orgId: string) { @@ -333,39 +336,16 @@ export class IntegrationService { dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh ) { - const { accessToken, expiresIn, refreshToken, additionalSettings } = - await new Promise((res) => { - return integrationProvider - .refreshToken(getIntegration.refreshToken!) - .then((r) => res(r)) - .catch(() => { - res({ - error: '', - accessToken: '', - id: '', - name: '', - picture: '', - username: '', - additionalSettings: undefined, - }); - }); - }); + const data = await this._refreshIntegrationService.refresh( + getIntegration + ); + if (!data) { + return []; + } + + const { accessToken } = data; if (accessToken) { - await this.createOrUpdateIntegration( - additionalSettings, - !!integrationProvider.oneTimeToken, - getIntegration.organizationId, - getIntegration.name, - getIntegration.picture!, - 'social', - getIntegration.internalId, - getIntegration.providerIdentifier, - accessToken, - refreshToken, - expiresIn - ); - getIntegration.token = accessToken; if (integrationProvider.refreshWait) { @@ -464,51 +444,13 @@ export class IntegrationService { dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh ) { - const { accessToken, expiresIn, refreshToken, additionalSettings } = - await new Promise((res) => { - getSocialIntegration - .refreshToken(getIntegration.refreshToken!) - .then((r) => res(r)) - .catch(() => - res({ - accessToken: '', - expiresIn: 0, - refreshToken: '', - id: '', - name: '', - username: '', - picture: '', - additionalSettings: undefined, - }) - ); - }); - - if (!accessToken) { - await this.refreshNeeded( - getIntegration.organizationId, - getIntegration.id - ); - - await this.informAboutRefreshError( - getIntegration.organizationId, - getIntegration - ); - return {}; - } - - await this.createOrUpdateIntegration( - additionalSettings, - !!getSocialIntegration.oneTimeToken, - getIntegration.organizationId, - getIntegration.name, - getIntegration.picture!, - 'social', - getIntegration.internalId, - getIntegration.providerIdentifier, - accessToken, - refreshToken, - expiresIn + const data = await this._refreshIntegrationService.refresh( + getIntegration ); + if (!data) { + return; + } + const { accessToken } = data; getIntegration.token = accessToken; diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 12163659..d20b7faa 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -39,6 +39,7 @@ import { validate } from 'class-validator'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; dayjs.extend(utc); import * as Sentry from '@sentry/nestjs'; +import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; type PostWithConditionals = Post & { integration?: Integration; @@ -59,7 +60,8 @@ export class PostsService { private _mediaService: MediaService, private _shortLinkService: ShortLinkService, private _webhookService: WebhooksService, - private openaiService: OpenaiService + private openaiService: OpenaiService, + private _refreshIntegrationService: RefreshIntegrationService ) {} checkPending15minutesBack() { @@ -405,7 +407,7 @@ export class PostsService { integration: Integration, posts: Post[], forceRefresh = false - ): Promise> { + ): Promise | undefined> { const getIntegration = this._integrationManager.getSocialIntegration( integration.providerIdentifier ); @@ -415,53 +417,13 @@ export class PostsService { } if (dayjs(integration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) { - const { accessToken, expiresIn, refreshToken, additionalSettings } = - await new Promise((res) => { - getIntegration - .refreshToken(integration.refreshToken!) - .then((r) => res(r)) - .catch(() => - res({ - accessToken: '', - expiresIn: 0, - refreshToken: '', - id: '', - name: '', - username: '', - picture: '', - additionalSettings: undefined, - }) - ); - }); + const data = await this._refreshIntegrationService.refresh(integration); - if (!accessToken) { - await this._integrationService.refreshNeeded( - integration.organizationId, - integration.id - ); - - await this._integrationService.informAboutRefreshError( - integration.organizationId, - integration - ); - return {}; + if (!data) { + return undefined; } - await this._integrationService.createOrUpdateIntegration( - additionalSettings, - !!getIntegration.oneTimeToken, - integration.organizationId, - integration.name, - integration.picture!, - 'social', - integration.internalId, - integration.providerIdentifier, - accessToken, - refreshToken, - expiresIn - ); - - integration.token = accessToken; + integration.token = data.accessToken; if (getIntegration.refreshWait) { await timer(10000); @@ -718,7 +680,7 @@ export class PostsService { }); } - Sentry.metrics.count("post_created", 1); + Sentry.metrics.count('post_created', 1); postList.push({ postId: posts[0].id, integration: post.integration.id, diff --git a/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts new file mode 100644 index 00000000..80f7e3d7 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts @@ -0,0 +1,84 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Integration } from '@prisma/client'; +import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; +import { + AuthTokenDetails, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; + +@Injectable() +export class RefreshIntegrationService { + constructor( + private _integrationManager: IntegrationManager, + @Inject(forwardRef(() => IntegrationService)) + private _integrationService: IntegrationService + ) {} + async refresh(integration: Integration): Promise { + const socialProvider = this._integrationManager.getSocialIntegration( + integration.providerIdentifier + ); + + const refresh = await this.refreshProcess(integration, socialProvider); + + if (!refresh) { + return false as const; + } + + await this._integrationService.createOrUpdateIntegration( + undefined, + !!socialProvider.oneTimeToken, + integration.organizationId, + integration.name, + integration.picture!, + 'social', + integration.internalId, + integration.providerIdentifier, + refresh.accessToken, + refresh.refreshToken, + refresh.expiresIn + ); + + return refresh; + } + + private async refreshProcess( + integration: Integration, + socialProvider: SocialProvider + ): Promise { + const refresh: false | AuthTokenDetails = await socialProvider + .refreshToken(integration.refreshToken) + .catch((err) => false); + + if (!refresh) { + await this._integrationService.refreshNeeded( + integration.organizationId, + integration.id + ); + + await this._integrationService.informAboutRefreshError( + integration.organizationId, + integration + ); + + await this._integrationService.disconnectChannel(integration.organizationId, integration); + + return false; + } + + if (!socialProvider.reConnect) { + return refresh; + } + + const reConnect = await socialProvider.reConnect( + integration.rootInternalId, + integration.internalId, + refresh.accessToken + ); + + return { + ...refresh, + ...reConnect, + }; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index a3dae5d8..a37e0e42 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -182,7 +182,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { id: string, requiredId: string, accessToken: string - ): Promise { + ): Promise> { const information = await this.fetchPageInformation(accessToken, { page: requiredId, }); @@ -191,8 +191,6 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { id: information.id, name: information.name, accessToken: information.access_token, - refreshToken: information.access_token, - expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: information.picture, username: information.username, }; diff --git a/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts b/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts index 2fde8307..c3e15314 100644 --- a/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts @@ -320,7 +320,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { id: string, requiredId: string, accessToken: string - ): Promise { + ): Promise> { const pages = await this.pages(accessToken); const findPage = pages.find((p) => p.id === requiredId); @@ -338,8 +338,6 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { id: information.id, name: information.name, accessToken: information.access_token, - refreshToken: information.access_token, - expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: information.picture, username: information.username, }; diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 2cc499e2..13e3701e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -292,7 +292,6 @@ export class InstagramProvider }; } - console.log('err', body); return undefined; } @@ -300,7 +299,7 @@ export class InstagramProvider id: string, requiredId: string, accessToken: string - ): Promise { + ): Promise> { const findPage = (await this.pages(accessToken)).find( (p) => p.id === requiredId ); @@ -314,8 +313,6 @@ export class InstagramProvider id: information.id, name: information.name, accessToken: information.access_token, - refreshToken: information.access_token, - expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: information.picture, username: information.username, }; diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts index feb349a5..b30ae6f5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -149,7 +149,7 @@ export class LinkedinPageProvider id: string, requiredId: string, accessToken: string - ): Promise { + ): Promise> { const information = await this.fetchPageInformation(accessToken, { page: requiredId, }); @@ -158,8 +158,6 @@ export class LinkedinPageProvider id: information.id, name: information.name, accessToken: information.access_token, - refreshToken: information.access_token, - expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: information.picture, username: information.username, }; diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index 8da48809..3653b97e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -19,7 +19,7 @@ export interface IAuthenticator { id: string, requiredId: string, accessToken: string - ): Promise; + ): Promise>; generateAuthUrl( clientInformation?: ClientInformation ): Promise; diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index 890ba1a5..d35762f5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -257,7 +257,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { id: string, requiredId: string, accessToken: string - ): Promise { + ): Promise> { const pages = await this.pages(accessToken); const findPage = pages.find((p) => p.id === requiredId); @@ -273,8 +273,6 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { id: information.id, name: information.name, accessToken: information.access_token, - refreshToken: information.access_token, - expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: information.picture, username: information.username, };