From b6da5b83e3a8eca959bb9f70277c952b4bfb9f93 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 18 Oct 2024 18:34:10 +0700 Subject: [PATCH] feat: improved refresh mechanism --- .../src/api/routes/integrations.controller.ts | 45 +++++++++++---- .../launches/launches.component.tsx | 3 +- .../components/layout/continue.provider.tsx | 2 +- .../src/integrations/social.abstract.ts | 1 + .../integrations/social/bluesky.provider.ts | 2 +- .../integrations/social/discord.provider.ts | 6 +- .../integrations/social/dribbble.provider.ts | 6 +- .../integrations/social/facebook.provider.ts | 50 +++++++++------- .../integrations/social/instagram.provider.ts | 57 +++++++++++-------- .../social/linkedin.page.provider.ts | 34 ++++++++--- .../integrations/social/linkedin.provider.ts | 6 +- .../integrations/social/mastodon.provider.ts | 24 +++----- .../integrations/social/pinterest.provider.ts | 10 +--- .../src/integrations/social/slack.provider.ts | 6 +- .../social/social.integrations.interface.ts | 2 +- .../integrations/social/threads.provider.ts | 2 +- .../integrations/social/tiktok.provider.ts | 4 +- .../src/integrations/social/x.provider.ts | 5 +- .../integrations/social/youtube.provider.ts | 2 +- 19 files changed, 154 insertions(+), 113 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 7c59d784..f7c4ad74 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -28,6 +28,7 @@ import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/in import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto'; import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; @ApiTags('Integrations') @Controller('/integrations') @@ -156,7 +157,12 @@ export class IntegrationsController { : undefined; const { codeVerifier, state, url } = - await integrationProvider.generateAuthUrl(refresh, getExternalUrl); + await integrationProvider.generateAuthUrl(getExternalUrl); + + if (refresh) { + await ioRedis.set(`refresh:${state}`, refresh, 'EX', 300); + } + await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300); await ioRedis.set( `external:${state}`, @@ -311,6 +317,11 @@ export class IntegrationsController { await ioRedis.del(`external:${body.state}`); } + const refresh = await ioRedis.get(`refresh:${body.state}`); + if (refresh) { + await ioRedis.del(`refresh:${body.state}`); + } + const { accessToken, expiresIn, @@ -319,14 +330,28 @@ export class IntegrationsController { name, picture, username, - } = await integrationProvider.authenticate( - { - code: body.code, - codeVerifier: getCodeVerifier, - refresh: body.refresh, - }, - details ? JSON.parse(details) : undefined - ); + // eslint-disable-next-line no-async-promise-executor + } = await new Promise(async (res) => { + const auth = await integrationProvider.authenticate( + { + code: body.code, + codeVerifier: getCodeVerifier, + refresh: body.refresh, + }, + details ? JSON.parse(details) : undefined + ); + + if (refresh && integrationProvider.reConnect) { + const newAuth = await integrationProvider.reConnect( + auth.id, + refresh, + auth.accessToken + ); + return res(newAuth); + } + + return res(auth); + }); if (!id) { throw new Error('Invalid api key'); @@ -343,7 +368,7 @@ export class IntegrationsController { refreshToken, expiresIn, username, - integrationProvider.isBetweenSteps, + refresh ? false : integrationProvider.isBetweenSteps, body.refresh, +body.timezone, details diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 27cf01dc..8134d4e6 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -126,12 +126,13 @@ export const LaunchesComponent = () => { {sortedIntegrations.map((integration) => (
{ + const information = await this.fetchPageInformation( + accessToken, + requiredId + ); + + return { + 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, + }; + } + async authenticate(params: { code: string; codeVerifier: string; @@ -91,22 +111,6 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { .map((p: any) => p.permission); this.checkScopes(this.scopes, permissions); - if (params.refresh) { - const information = await this.fetchPageInformation( - access_token, - params.refresh - ); - return { - 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, - }; - } - const { id, name, @@ -174,7 +178,11 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { let finalId = ''; let finalUrl = ''; if ((firstPost?.media?.[0]?.url?.indexOf('mp4') || -2) > -1) { - const { id: videoId, permalink_url, ...all } = await ( + const { + id: videoId, + permalink_url, + ...all + } = await ( await this.fetch( `https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`, { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 80412537..ccf40e71 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -9,6 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { timer } from '@gitroom/helpers/utils/timer'; import dayjs from 'dayjs'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { string } from 'yup'; export class InstagramProvider extends SocialAbstract @@ -39,16 +40,39 @@ export class InstagramProvider }; } - async generateAuthUrl(refresh?: string) { + async reConnect( + id: string, + requiredId: string, + accessToken: string + ): Promise { + const findPage = (await this.pages(accessToken)).find( + (p) => p.id === requiredId + ); + + const information = await this.fetchPageInformation(accessToken, { + id: requiredId, + pageId: findPage?.pageId!, + }); + + return { + 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, + }; + } + + async generateAuthUrl() { const state = makeId(6); return { url: 'https://www.facebook.com/v20.0/dialog/oauth' + `?client_id=${process.env.FACEBOOK_APP_ID}` + `&redirect_uri=${encodeURIComponent( - `${process.env.FRONTEND_URL}/integrations/social/instagram${ - refresh ? `?refresh=${refresh}` : '' - }` + `${process.env.FRONTEND_URL}/integrations/social/instagram` )}` + `&state=${state}` + `&scope=${encodeURIComponent(this.scopes.join(','))}`, @@ -109,26 +133,6 @@ export class InstagramProvider ) ).json(); - if (params.refresh) { - const findPage = (await this.pages(access_token)).find( - (p) => p.id === params.refresh - ); - const information = await this.fetchPageInformation(access_token, { - id: params.refresh, - pageId: findPage?.pageId!, - }); - - return { - 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, - }; - } - return { id, name, @@ -187,6 +191,7 @@ export class InstagramProvider ) ).json(); + console.log(id, name, profile_picture_url, username); return { id, name, @@ -206,7 +211,9 @@ export class InstagramProvider const medias = await Promise.all( firstPost?.media?.map(async (m) => { const caption = - firstPost.media?.length === 1 ? `&caption=${encodeURIComponent(firstPost.message)}` : ``; + firstPost.media?.length === 1 + ? `&caption=${encodeURIComponent(firstPost.message)}` + : ``; const isCarousel = (firstPost?.media?.length || 0) > 1 ? `&is_carousel_item=true` : ``; const mediaType = 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 831dfc38..2bd66365 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -31,7 +31,11 @@ export class LinkedinPageProvider override async refreshToken( refresh_token: string ): Promise { - const { access_token: accessToken, expires_in, refresh_token: refreshToken } = await ( + const { + access_token: accessToken, + expires_in, + refresh_token: refreshToken, + } = await ( await fetch('https://www.linkedin.com/oauth/v2/accessToken', { method: 'POST', headers: { @@ -77,15 +81,13 @@ export class LinkedinPageProvider }; } - override async generateAuthUrl(refresh?: string) { + override async generateAuthUrl() { const state = makeId(6); const codeVerifier = makeId(30); const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${ process.env.LINKEDIN_CLIENT_ID }&redirect_uri=${encodeURIComponent( - `${process.env.FRONTEND_URL}/integrations/social/linkedin-page${ - refresh ? `?refresh=${refresh}` : '' - }` + `${process.env.FRONTEND_URL}/integrations/social/linkedin-page` )}&state=${state}&scope=${encodeURIComponent(this.scopes.join(' '))}`; return { url, @@ -117,6 +119,24 @@ export class LinkedinPageProvider })); } + async reConnect( + id: string, + requiredId: string, + accessToken: string + ): Promise { + const information = await this.fetchPageInformation(accessToken, requiredId); + + return { + 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, + }; + } + async fetchPageInformation(accessToken: string, pageId: string) { const data = await ( await fetch( @@ -149,9 +169,7 @@ export class LinkedinPageProvider body.append('code', params.code); body.append( 'redirect_uri', - `${process.env.FRONTEND_URL}/integrations/social/linkedin-page${ - params.refresh ? `?refresh=${params.refresh}` : '' - }` + `${process.env.FRONTEND_URL}/integrations/social/linkedin-page` ); body.append('client_id', process.env.LINKEDIN_CLIENT_ID!); body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!); diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 8f105c18..441e9084 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -73,15 +73,13 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(6); const codeVerifier = makeId(30); const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${ process.env.LINKEDIN_CLIENT_ID }&redirect_uri=${encodeURIComponent( - `${process.env.FRONTEND_URL}/integrations/social/linkedin${ - refresh ? `?refresh=${refresh}` : '' - }` + `${process.env.FRONTEND_URL}/integrations/social/linkedin` )}&state=${state}&scope=${encodeURIComponent(this.scopes.join(' '))}`; return { url, diff --git a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts index 1587dcec..f70678dc 100644 --- a/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts @@ -29,24 +29,20 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider { customUrl: string, state: string, clientId: string, - url: string, - refresh?: string + url: string ) { return `${customUrl}/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent( - `${url}/integrations/social/mastodon${ - refresh ? `?refresh=${refresh}` : '' - }` + `${url}/integrations/social/mastodon` )}&scope=${this.scopes.join('+')}&state=${state}`; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(6); const url = this.generateUrlDynamic( 'https://mastodon.social', state, process.env.MASTODON_CLIENT_ID!, - process.env.FRONTEND_URL!, - refresh + process.env.FRONTEND_URL! ); return { url, @@ -98,13 +94,11 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider { }; } - async authenticate( - params: { - code: string; - codeVerifier: string; - refresh?: string; - } - ) { + async authenticate(params: { + code: string; + codeVerifier: string; + refresh?: string; + }) { return this.dynamicAuthenticate( process.env.MASTODON_CLIENT_ID!, process.env.MASTODON_CLIENT_SECRET!, diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index f93668a6..73d7ad3f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -67,15 +67,13 @@ export class PinterestProvider }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(6); return { url: `https://www.pinterest.com/oauth/?client_id=${ process.env.PINTEREST_CLIENT_ID }&redirect_uri=${encodeURIComponent( - `${process.env.FRONTEND_URL}/integrations/social/pinterest${ - refresh ? `?refresh=${refresh}` : '' - }` + `${process.env.FRONTEND_URL}/integrations/social/pinterest` )}&response_type=code&scope=${encodeURIComponent( 'boards:read,boards:write,pins:read,pins:write,user_accounts:read' )}&state=${state}`, @@ -213,9 +211,7 @@ export class PinterestProvider })); try { - const { - id: pId - } = await ( + const { id: pId } = await ( await this.fetch('https://api.pinterest.com/v5/pins', { method: 'POST', headers: { diff --git a/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts index 675bee99..b13f17f2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts @@ -32,7 +32,7 @@ export class SlackProvider extends SocialAbstract implements SocialProvider { username: '', }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(6); return { @@ -43,9 +43,7 @@ export class SlackProvider extends SocialAbstract implements SocialProvider { process?.env?.FRONTEND_URL?.indexOf('https') === -1 ? 'https://redirectmeto.com/' : '' - }${process?.env?.FRONTEND_URL}/integrations/social/slack${ - refresh ? `?refresh=${refresh}` : '' - }` + }${process?.env?.FRONTEND_URL}/integrations/social/slack` )}&scope=channels:read,chat:write,users:read,groups:read,channels:join,chat:write.customize&state=${state}`, codeVerifier: makeId(10), state, 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 0b4fa59e..f791a026 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -15,8 +15,8 @@ export interface IAuthenticator { clientInformation?: ClientInformation ): Promise; refreshToken(refreshToken: string): Promise; + reConnect?(id: string, requiredId: string, accessToken: string): Promise; generateAuthUrl( - refresh?: string, clientInformation?: ClientInformation ): Promise; analytics?( diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 419a898d..e5327e0a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -34,7 +34,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(6); return { url: diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index b7786fef..6d373bd4 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -67,7 +67,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = Math.random().toString(36).substring(2); return { @@ -79,7 +79,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { process.env.NODE_ENV === 'development' || !process.env.NODE_ENV ? `https://integration.git.sn/integrations/social/tiktok` : `${process.env.FRONTEND_URL}/integrations/social/tiktok` - }${refresh ? `?refresh=${refresh}` : ''}` + }` )}` + `&state=${state}` + `&response_type=code` + diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 9dbccdd6..8bc9f0ac 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -49,15 +49,14 @@ export class XProvider extends SocialAbstract implements SocialProvider { }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const client = new TwitterApi({ appKey: process.env.X_API_KEY!, appSecret: process.env.X_API_SECRET!, }); const { url, oauth_token, oauth_token_secret } = await client.generateAuthLink( - process.env.FRONTEND_URL + - `/integrations/social/x${refresh ? `?refresh=${refresh}` : ''}`, + process.env.FRONTEND_URL + `/integrations/social/x`, { authAccessType: 'write', linkMode: 'authenticate', diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index bafa31a4..e6b966b7 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -82,7 +82,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { }; } - async generateAuthUrl(refresh?: string) { + async generateAuthUrl() { const state = makeId(7); const { client } = clientAndYoutube(); return {