fix: refresh token unified, and found some bugs

This commit is contained in:
Nevo David 2025-12-04 14:12:33 +07:00
parent 71174c1b8f
commit 943acec8e4
12 changed files with 154 additions and 199 deletions

View File

@ -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);

View File

@ -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' };
}

View File

@ -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,

View File

@ -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<AuthTokenDetails>((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<AuthTokenDetails>((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;

View File

@ -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<Partial<{ postId: string; releaseURL: string }>> {
): Promise<Partial<{ postId: string; releaseURL: string }> | 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<AuthTokenDetails>((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,

View File

@ -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<false | AuthTokenDetails> {
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<AuthTokenDetails | false> {
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,
};
}
}

View File

@ -182,7 +182,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails> {
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
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,
};

View File

@ -320,7 +320,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails> {
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
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,
};

View File

@ -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<AuthTokenDetails> {
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
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,
};

View File

@ -149,7 +149,7 @@ export class LinkedinPageProvider
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails> {
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
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,
};

View File

@ -19,7 +19,7 @@ export interface IAuthenticator {
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails>;
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>>;
generateAuthUrl(
clientInformation?: ClientInformation
): Promise<GenerateAuthUrlResponse>;

View File

@ -257,7 +257,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails> {
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
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,
};