feat: refresh token before expiration for specific platforms
This commit is contained in:
parent
13d4bb0086
commit
07b0c2e85d
|
|
@ -488,30 +488,39 @@ export class IntegrationsController {
|
|||
throw new HttpException('', 412);
|
||||
}
|
||||
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
org.id,
|
||||
validName.trim(),
|
||||
picture,
|
||||
'social',
|
||||
String(id),
|
||||
integration,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username,
|
||||
refresh ? false : integrationProvider.isBetweenSteps,
|
||||
body.refresh,
|
||||
+body.timezone,
|
||||
details
|
||||
? AuthService.fixedEncryption(details)
|
||||
: integrationProvider.customFields
|
||||
? AuthService.fixedEncryption(
|
||||
Buffer.from(body.code, 'base64').toString()
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
const createUpdate =
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
org.id,
|
||||
validName.trim(),
|
||||
picture,
|
||||
'social',
|
||||
String(id),
|
||||
integration,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
username,
|
||||
refresh ? false : integrationProvider.isBetweenSteps,
|
||||
body.refresh,
|
||||
+body.timezone,
|
||||
details
|
||||
? AuthService.fixedEncryption(details)
|
||||
: integrationProvider.customFields
|
||||
? AuthService.fixedEncryption(
|
||||
Buffer.from(body.code, 'base64').toString()
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
|
||||
this._refreshIntegrationService
|
||||
.startRefreshWorkflow(org.id, createUpdate.id, integrationProvider)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
return createUpdate;
|
||||
}
|
||||
|
||||
@Post('/disable')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
|
||||
@Injectable()
|
||||
@Activity()
|
||||
export class IntegrationsActivity {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
) {}
|
||||
|
||||
@ActivityMethod()
|
||||
async getIntegrationsById(id: string, orgId: string) {
|
||||
return this._integrationService.getIntegrationById(orgId, id);
|
||||
}
|
||||
|
||||
async refreshToken(integration: Integration) {
|
||||
return this._refreshIntegrationService.refresh(integration);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,14 @@ import { getTemporalModule } from '@gitroom/nestjs-libraries/temporal/temporal.m
|
|||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { EmailActivity } from '@gitroom/orchestrator/activities/email.activity';
|
||||
import { IntegrationsActivity } from '@gitroom/orchestrator/activities/integrations.activity';
|
||||
|
||||
const activities = [PostActivity, AutopostService, EmailActivity];
|
||||
const activities = [
|
||||
PostActivity,
|
||||
AutopostService,
|
||||
EmailActivity,
|
||||
IntegrationsActivity,
|
||||
];
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from './autopost.workflow';
|
|||
export * from './digest.email.workflow';
|
||||
export * from './missing.post.workflow';
|
||||
export * from './send.email.workflow';
|
||||
export * from './refresh.token.workflow';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { proxyActivities, sleep } from '@temporalio/workflow';
|
||||
import { IntegrationsActivity } from '@gitroom/orchestrator/activities/integrations.activity';
|
||||
|
||||
const { getIntegrationsById, refreshToken } =
|
||||
proxyActivities<IntegrationsActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
export async function refreshTokenWorkflow({
|
||||
organizationId,
|
||||
integrationId,
|
||||
}: {
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
}) {
|
||||
while (true) {
|
||||
let integration = await getIntegrationsById(integrationId, organizationId);
|
||||
if (
|
||||
!integration ||
|
||||
integration.deletedAt ||
|
||||
integration.inBetweenSteps ||
|
||||
integration.refreshNeeded
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const endDate = new Date(integration.tokenExpiration);
|
||||
|
||||
const minMax = Math.max(0, endDate.getTime() - today.getTime());
|
||||
if (!minMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await sleep(minMax as number);
|
||||
|
||||
// while we were sleeping, the integration might have been deleted
|
||||
integration = await getIntegrationsById(integrationId, organizationId);
|
||||
if (
|
||||
!integration ||
|
||||
integration.deletedAt ||
|
||||
integration.inBetweenSteps ||
|
||||
integration.refreshNeeded
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await refreshToken(integration);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,15 @@ import {
|
|||
AuthTokenDetails,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { TemporalService } from 'nestjs-temporal-core';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshIntegrationService {
|
||||
constructor(
|
||||
private _integrationManager: IntegrationManager,
|
||||
@Inject(forwardRef(() => IntegrationService))
|
||||
private _integrationService: IntegrationService
|
||||
private _integrationService: IntegrationService,
|
||||
private _temporalService: TemporalService
|
||||
) {}
|
||||
async refresh(integration: Integration): Promise<false | AuthTokenDetails> {
|
||||
const socialProvider = this._integrationManager.getSocialIntegration(
|
||||
|
|
@ -50,6 +52,21 @@ export class RefreshIntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
public async startRefreshWorkflow(orgId: string, id: string, integration: SocialProvider) {
|
||||
if (!integration.refreshCron) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start(`refreshTokenWorkflow`, {
|
||||
workflowId: `refresh_${id}`,
|
||||
args: [{integrationId: id, organizationId: orgId}],
|
||||
taskQueue: 'main',
|
||||
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshProcess(
|
||||
integration: Integration,
|
||||
socialProvider: SocialProvider
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class InstagramStandaloneProvider
|
|||
identifier = 'instagram-standalone';
|
||||
name = 'Instagram\n(Standalone)';
|
||||
isBetweenSteps = false;
|
||||
refreshCron = true;
|
||||
scopes = [
|
||||
'instagram_business_basic',
|
||||
'instagram_business_content_publish',
|
||||
|
|
@ -69,7 +70,7 @@ export class InstagramStandaloneProvider
|
|||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
|
||||
picture: profile_picture_url || '',
|
||||
username,
|
||||
};
|
||||
|
|
@ -144,7 +145,7 @@ export class InstagramStandaloneProvider
|
|||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
|
||||
picture: profile_picture_url,
|
||||
username,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export interface SocialProvider
|
|||
identifier: string;
|
||||
refreshWait?: boolean;
|
||||
convertToJPEG?: boolean;
|
||||
refreshCron?: boolean;
|
||||
dto?: any;
|
||||
maxLength: (additionalSettings?: any) => number;
|
||||
isWeb3?: boolean;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
// 'threads_profile_discovery',
|
||||
];
|
||||
override maxConcurrentJob = 2; // Threads has moderate rate limits
|
||||
refreshCron = true;
|
||||
|
||||
editor = 'normal' as const;
|
||||
maxLength() {
|
||||
|
|
@ -61,7 +62,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
|
||||
picture: picture || '',
|
||||
username: '',
|
||||
};
|
||||
|
|
@ -114,7 +115,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
'https://graph.threads.net/access_token' +
|
||||
'?grant_type=th_exchange_token' +
|
||||
`&client_secret=${process.env.THREADS_APP_SECRET}` +
|
||||
`&access_token=${getAccessToken.access_token}&fields=access_token,expires_in`
|
||||
`&access_token=${getAccessToken.access_token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
|
|
@ -127,7 +128,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
name,
|
||||
accessToken: access_token,
|
||||
refreshToken: access_token,
|
||||
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
|
||||
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
|
||||
picture: picture || '',
|
||||
username: username,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue