Skip to content

Commit 07b0c2e

Browse files
committed
feat: refresh token before expiration for specific platforms
1 parent 13d4bb0 commit 07b0c2e

File tree

9 files changed

+145
-31
lines changed

9 files changed

+145
-31
lines changed

apps/backend/src/api/routes/integrations.controller.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -488,30 +488,39 @@ export class IntegrationsController {
488488
throw new HttpException('', 412);
489489
}
490490

491-
return this._integrationService.createOrUpdateIntegration(
492-
additionalSettings,
493-
!!integrationProvider.oneTimeToken,
494-
org.id,
495-
validName.trim(),
496-
picture,
497-
'social',
498-
String(id),
499-
integration,
500-
accessToken,
501-
refreshToken,
502-
expiresIn,
503-
username,
504-
refresh ? false : integrationProvider.isBetweenSteps,
505-
body.refresh,
506-
+body.timezone,
507-
details
508-
? AuthService.fixedEncryption(details)
509-
: integrationProvider.customFields
510-
? AuthService.fixedEncryption(
511-
Buffer.from(body.code, 'base64').toString()
512-
)
513-
: undefined
514-
);
491+
const createUpdate =
492+
await this._integrationService.createOrUpdateIntegration(
493+
additionalSettings,
494+
!!integrationProvider.oneTimeToken,
495+
org.id,
496+
validName.trim(),
497+
picture,
498+
'social',
499+
String(id),
500+
integration,
501+
accessToken,
502+
refreshToken,
503+
expiresIn,
504+
username,
505+
refresh ? false : integrationProvider.isBetweenSteps,
506+
body.refresh,
507+
+body.timezone,
508+
details
509+
? AuthService.fixedEncryption(details)
510+
: integrationProvider.customFields
511+
? AuthService.fixedEncryption(
512+
Buffer.from(body.code, 'base64').toString()
513+
)
514+
: undefined
515+
);
516+
517+
this._refreshIntegrationService
518+
.startRefreshWorkflow(org.id, createUpdate.id, integrationProvider)
519+
.catch((err) => {
520+
console.log(err);
521+
});
522+
523+
return createUpdate;
515524
}
516525

517526
@Post('/disable')
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
3+
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
4+
import { Integration } from '@prisma/client';
5+
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
6+
7+
@Injectable()
8+
@Activity()
9+
export class IntegrationsActivity {
10+
constructor(
11+
private _integrationService: IntegrationService,
12+
private _refreshIntegrationService: RefreshIntegrationService
13+
) {}
14+
15+
@ActivityMethod()
16+
async getIntegrationsById(id: string, orgId: string) {
17+
return this._integrationService.getIntegrationById(orgId, id);
18+
}
19+
20+
async refreshToken(integration: Integration) {
21+
return this._refreshIntegrationService.refresh(integration);
22+
}
23+
}

apps/orchestrator/src/app.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { getTemporalModule } from '@gitroom/nestjs-libraries/temporal/temporal.m
44
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
55
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
66
import { EmailActivity } from '@gitroom/orchestrator/activities/email.activity';
7+
import { IntegrationsActivity } from '@gitroom/orchestrator/activities/integrations.activity';
78

8-
const activities = [PostActivity, AutopostService, EmailActivity];
9+
const activities = [
10+
PostActivity,
11+
AutopostService,
12+
EmailActivity,
13+
IntegrationsActivity,
14+
];
915
@Module({
1016
imports: [
1117
DatabaseModule,

apps/orchestrator/src/workflows/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './autopost.workflow';
33
export * from './digest.email.workflow';
44
export * from './missing.post.workflow';
55
export * from './send.email.workflow';
6+
export * from './refresh.token.workflow';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { proxyActivities, sleep } from '@temporalio/workflow';
2+
import { IntegrationsActivity } from '@gitroom/orchestrator/activities/integrations.activity';
3+
4+
const { getIntegrationsById, refreshToken } =
5+
proxyActivities<IntegrationsActivity>({
6+
startToCloseTimeout: '10 minute',
7+
retry: {
8+
maximumAttempts: 3,
9+
backoffCoefficient: 1,
10+
initialInterval: '2 minutes',
11+
},
12+
});
13+
14+
export async function refreshTokenWorkflow({
15+
organizationId,
16+
integrationId,
17+
}: {
18+
integrationId: string;
19+
organizationId: string;
20+
}) {
21+
while (true) {
22+
let integration = await getIntegrationsById(integrationId, organizationId);
23+
if (
24+
!integration ||
25+
integration.deletedAt ||
26+
integration.inBetweenSteps ||
27+
integration.refreshNeeded
28+
) {
29+
return false;
30+
}
31+
32+
const today = new Date();
33+
const endDate = new Date(integration.tokenExpiration);
34+
35+
const minMax = Math.max(0, endDate.getTime() - today.getTime());
36+
if (!minMax) {
37+
return false;
38+
}
39+
40+
await sleep(minMax as number);
41+
42+
// while we were sleeping, the integration might have been deleted
43+
integration = await getIntegrationsById(integrationId, organizationId);
44+
if (
45+
!integration ||
46+
integration.deletedAt ||
47+
integration.inBetweenSteps ||
48+
integration.refreshNeeded
49+
) {
50+
return false;
51+
}
52+
53+
await refreshToken(integration);
54+
}
55+
}

libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
AuthTokenDetails,
77
SocialProvider,
88
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
9+
import { TemporalService } from 'nestjs-temporal-core';
910

1011
@Injectable()
1112
export class RefreshIntegrationService {
1213
constructor(
1314
private _integrationManager: IntegrationManager,
1415
@Inject(forwardRef(() => IntegrationService))
15-
private _integrationService: IntegrationService
16+
private _integrationService: IntegrationService,
17+
private _temporalService: TemporalService
1618
) {}
1719
async refresh(integration: Integration): Promise<false | AuthTokenDetails> {
1820
const socialProvider = this._integrationManager.getSocialIntegration(
@@ -50,6 +52,21 @@ export class RefreshIntegrationService {
5052
);
5153
}
5254

55+
public async startRefreshWorkflow(orgId: string, id: string, integration: SocialProvider) {
56+
if (!integration.refreshCron) {
57+
return false;
58+
}
59+
60+
return this._temporalService.client
61+
.getRawClient()
62+
?.workflow.start(`refreshTokenWorkflow`, {
63+
workflowId: `refresh_${id}`,
64+
args: [{integrationId: id, organizationId: orgId}],
65+
taskQueue: 'main',
66+
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
67+
});
68+
}
69+
5370
private async refreshProcess(
5471
integration: Integration,
5572
socialProvider: SocialProvider

libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class InstagramStandaloneProvider
2424
identifier = 'instagram-standalone';
2525
name = 'Instagram\n(Standalone)';
2626
isBetweenSteps = false;
27+
refreshCron = true;
2728
scopes = [
2829
'instagram_business_basic',
2930
'instagram_business_content_publish',
@@ -69,7 +70,7 @@ export class InstagramStandaloneProvider
6970
name,
7071
accessToken: access_token,
7172
refreshToken: access_token,
72-
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
73+
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
7374
picture: profile_picture_url || '',
7475
username,
7576
};
@@ -144,7 +145,7 @@ export class InstagramStandaloneProvider
144145
name,
145146
accessToken: access_token,
146147
refreshToken: access_token,
147-
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
148+
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
148149
picture: profile_picture_url,
149150
username,
150151
};

libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface SocialProvider
130130
identifier: string;
131131
refreshWait?: boolean;
132132
convertToJPEG?: boolean;
133+
refreshCron?: boolean;
133134
dto?: any;
134135
maxLength: (additionalSettings?: any) => number;
135136
isWeb3?: boolean;

libraries/nestjs-libraries/src/integrations/social/threads.provider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
2626
// 'threads_profile_discovery',
2727
];
2828
override maxConcurrentJob = 2; // Threads has moderate rate limits
29+
refreshCron = true;
2930

3031
editor = 'normal' as const;
3132
maxLength() {
@@ -61,7 +62,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
6162
name,
6263
accessToken: access_token,
6364
refreshToken: access_token,
64-
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
65+
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
6566
picture: picture || '',
6667
username: '',
6768
};
@@ -114,7 +115,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
114115
'https://graph.threads.net/access_token' +
115116
'?grant_type=th_exchange_token' +
116117
`&client_secret=${process.env.THREADS_APP_SECRET}` +
117-
`&access_token=${getAccessToken.access_token}&fields=access_token,expires_in`
118+
`&access_token=${getAccessToken.access_token}`
118119
)
119120
).json();
120121

@@ -127,7 +128,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
127128
name,
128129
accessToken: access_token,
129130
refreshToken: access_token,
130-
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
131+
expiresIn: dayjs().add(58, 'days').unix() - dayjs().unix(),
131132
picture: picture || '',
132133
username: username,
133134
};

0 commit comments

Comments
 (0)