diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index d6f86401..e509aba3 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -6,6 +6,7 @@ import { Param, Post, Query, + UseFilters, } from '@nestjs/common'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; @@ -23,6 +24,7 @@ import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permis import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; import { ApiTags } from '@nestjs/swagger'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; +import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes'; @ApiTags('Integrations') @Controller('/integrations') @@ -181,6 +183,7 @@ export class IntegrationsController { @Post('/social/:integration/connect') @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) + @UseFilters(new NotEnoughScopesFilter()) async connectSocialMedia( @GetOrgFromRequest() org: Organization, @Param('integration') integration: string, diff --git a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx index d2679c36..bead77f8 100644 --- a/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx +++ b/apps/frontend/src/app/(site)/integrations/social/[provider]/page.tsx @@ -1,3 +1,5 @@ +import { HttpStatusCode } from 'axios'; + export const dynamic = 'force-dynamic'; import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; @@ -19,12 +21,16 @@ export default async function Page({ }; } - const { id, inBetweenSteps } = await ( - await internalFetch(`/integrations/social/${provider}/connect`, { - method: 'POST', - body: JSON.stringify(searchParams), - }) - ).json(); + const data = await internalFetch(`/integrations/social/${provider}/connect`, { + method: 'POST', + body: JSON.stringify(searchParams), + }); + + if (data.status === HttpStatusCode.NotAcceptable) { + return redirect(`/launches?scope=missing`); + } + + const { inBetweenSteps, id } = await data.json(); if (inBetweenSteps && !searchParams.refresh) { return redirect(`/launches?added=${provider}&continue=${id}`); diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index a07643da..d115536c 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -14,13 +14,16 @@ import clsx from 'clsx'; import { useUser } from '../layout/user.context'; import { Menu } from '@gitroom/frontend/components/launches/menu/menu'; import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { Integration } from '@prisma/client'; import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback'; +import { useToaster } from '@gitroom/react/toaster/toaster'; export const LaunchesComponent = () => { const fetch = useFetch(); const router = useRouter(); + const search = useSearchParams(); + const toast = useToaster(); const [reload, setReload] = useState(false); const load = useCallback(async (path: string) => { @@ -88,7 +91,13 @@ export const LaunchesComponent = () => { ); useEffect(() => { - if (typeof window !== 'undefined' && window.opener) { + if (typeof window === 'undefined') { + return ; + } + if (search.get('scope') === 'missing') { + toast.show('You have to approve all the channel permissions', 'warning'); + } + if (window.opener) { window.close(); } }, []); diff --git a/libraries/nestjs-libraries/src/integrations/integration.missing.scopes.ts b/libraries/nestjs-libraries/src/integrations/integration.missing.scopes.ts new file mode 100644 index 00000000..f3c55ec1 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/integration.missing.scopes.ts @@ -0,0 +1,14 @@ +import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'; +import { Response } from 'express'; +import { NotEnoughScopes } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { HttpStatusCode } from 'axios'; + +@Catch(NotEnoughScopes) +export class NotEnoughScopesFilter implements ExceptionFilter { + catch(exception: NotEnoughScopes, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + response.status(HttpStatusCode.NotAcceptable).json({ invalid: true }); + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 2bc9bc29..467974e4 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -1,5 +1,6 @@ -export class RefreshToken { -} +export class RefreshToken {} + +export class NotEnoughScopes {} export abstract class SocialAbstract { async fetch(url: string, options: RequestInit = {}) { @@ -11,4 +12,25 @@ export abstract class SocialAbstract { return request; } -} \ No newline at end of file + + checkScopes(required: string[], got: string | string[]) { + console.log(required, got); + if (Array.isArray(got)) { + if (!required.every((scope) => got.includes(scope))) { + throw new NotEnoughScopes(); + } + + return true; + } + + const newGot = decodeURIComponent(got); + + const splitType = newGot.indexOf(',') > -1 ? ',' : ' '; + const gotArray = newGot.split(splitType); + if (!required.every((scope) => gotArray.includes(scope))) { + throw new NotEnoughScopes(); + } + + return true; + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts index f95fafeb..2c16c05e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts @@ -19,6 +19,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { identifier = 'dribbble'; name = 'Dribbble'; isBetweenSteps = false; + scopes = ['public', 'upload']; async refreshToken(refreshToken: string): Promise { const { access_token, expires_in } = await ( @@ -33,8 +34,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - scope: - 'boards:read,boards:write,pins:read,pins:write,user_accounts:read', + scope: `${this.scopes.join(',')}`, redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/pinterest`, }), }) @@ -87,7 +87,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { `${process.env.FRONTEND_URL}/integrations/social/dribbble${ refresh ? `?refresh=${refresh}` : '' }` - )}&response_type=code&scope=public+upload&state=${state}`, + )}&response_type=code&scope=${this.scopes.join('+')}&state=${state}`, codeVerifier: makeId(10), state, }; @@ -98,7 +98,7 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { codeVerifier: string; refresh: string; }) { - const { access_token } = await ( + const { access_token, scope } = await ( await this.fetch( `https://dribbble.com/oauth/token?client_id=${process.env.DRIBBBLE_CLIENT_ID}&client_secret=${process.env.DRIBBBLE_CLIENT_SECRET}&code=${params.code}&redirect_uri=${process.env.FRONTEND_URL}/integrations/social/dribbble`, { @@ -107,6 +107,8 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider { ) ).json(); + this.checkScopes(this.scopes, scope); + const { id, name, avatar_url, login } = await ( await this.fetch('https://api.dribbble.com/v2/user', { method: 'GET', diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index 4d3c2703..d22d5621 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -13,7 +13,14 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { identifier = 'facebook'; name = 'Facebook Page'; isBetweenSteps = true; - + scopes = [ + 'pages_show_list', + 'business_management', + 'pages_manage_posts', + 'pages_manage_engagement', + 'pages_read_engagement', + 'read_insights', + ]; async refreshToken(refresh_token: string): Promise { return { refreshToken: '', @@ -38,7 +45,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { }` )}` + `&state=${state}` + - '&scope=pages_show_list,business_management,pages_manage_posts,pages_manage_engagement,pages_read_engagement,read_insights', + `&scope=${this.scopes.join(',')}`, codeVerifier: makeId(10), state, }; @@ -73,6 +80,17 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { ) ).json(); + const { data } = await ( + await this.fetch( + `https://graph.facebook.com/v20.0/me/permissions?access_token=${access_token}` + ) + ).json(); + + const permissions = data + .filter((d: any) => d.status === 'granted') + .map((p: any) => p.permission); + this.checkScopes(this.scopes, permissions); + if (params.refresh) { const information = await this.fetchPageInformation( access_token, @@ -277,22 +295,24 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { ) ).json(); - return data?.map((d: any) => ({ - label: - d.name === 'page_impressions_unique' - ? 'Page Impressions' - : d.name === 'page_post_engagements' - ? 'Posts Engagement' - : d.name === 'page_daily_follows' - ? 'Page followers' - : d.name === 'page_video_views' - ? 'Videos views' - : 'Posts Impressions', - percentageChange: 5, - data: d?.values?.map((v: any) => ({ - total: v.value, - date: dayjs(v.end_time).format('YYYY-MM-DD'), - })), - })) || []; + return ( + data?.map((d: any) => ({ + label: + d.name === 'page_impressions_unique' + ? 'Page Impressions' + : d.name === 'page_post_engagements' + ? 'Posts Engagement' + : d.name === 'page_daily_follows' + ? 'Page followers' + : d.name === 'page_video_views' + ? 'Videos views' + : 'Posts Impressions', + percentageChange: 5, + data: d?.values?.map((v: any) => ({ + total: v.value, + date: dayjs(v.end_time).format('YYYY-MM-DD'), + })), + })) || [] + ); } } diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index f6492edf..8ae19c73 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -18,6 +18,15 @@ export class InstagramProvider identifier = 'instagram'; name = 'Instagram'; isBetweenSteps = true; + scopes = [ + 'instagram_basic', + 'pages_show_list', + 'pages_read_engagement', + 'business_management', + 'instagram_content_publish', + 'instagram_manage_comments', + 'instagram_manage_insights', + ]; async refreshToken(refresh_token: string): Promise { return { @@ -43,9 +52,7 @@ export class InstagramProvider }` )}` + `&state=${state}` + - `&scope=${encodeURIComponent( - 'instagram_basic,pages_show_list,pages_read_engagement,business_management,instagram_content_publish,instagram_manage_comments,instagram_manage_insights' - )}`, + `&scope=${encodeURIComponent(this.scopes.join(','))}`, codeVerifier: makeId(10), state, }; @@ -80,6 +87,17 @@ export class InstagramProvider ) ).json(); + const { data } = await ( + await this.fetch( + `https://graph.facebook.com/v20.0/me/permissions?access_token=${access_token}` + ) + ).json(); + + const permissions = data + .filter((d: any) => d.status === 'granted') + .map((p: any) => p.permission); + this.checkScopes(this.scopes, permissions); + const { id, name, @@ -343,13 +361,15 @@ export class InstagramProvider console.log(all); - return data?.map((d: any) => ({ - label: d.title, - percentageChange: 5, - data: d.values.map((v: any) => ({ - total: v.value, - date: dayjs(v.end_time).format('YYYY-MM-DD'), - })), - })) || []; + return ( + data?.map((d: any) => ({ + label: d.title, + percentageChange: 5, + data: d.values.map((v: any) => ({ + total: v.value, + date: dayjs(v.end_time).format('YYYY-MM-DD'), + })), + })) || [] + ); } } 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 8a732567..683114a3 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -7,9 +7,7 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider'; -import { number, string } from 'yup'; import dayjs from 'dayjs'; -import { writeFileSync } from 'fs'; export class LinkedinPageProvider extends LinkedinProvider @@ -18,6 +16,15 @@ export class LinkedinPageProvider override identifier = 'linkedin-page'; override name = 'LinkedIn Page'; override isBetweenSteps = true; + override scopes = [ + 'openid', + 'profile', + 'w_member_social', + 'r_basicprofile', + 'rw_organization_admin', + 'w_organization_social', + 'r_organization_social', + ]; override async refreshToken( refresh_token: string @@ -76,9 +83,7 @@ export class LinkedinPageProvider `${process.env.FRONTEND_URL}/integrations/social/linkedin-page${ refresh ? `?refresh=${refresh}` : '' }` - )}&state=${state}&scope=${encodeURIComponent( - 'openid profile w_member_social r_basicprofile rw_organization_admin w_organization_social r_organization_social' - )}`; + )}&state=${state}&scope=${encodeURIComponent(this.scopes.join(' '))}`; return { url, codeVerifier, @@ -152,6 +157,7 @@ export class LinkedinPageProvider access_token: accessToken, expires_in: expiresIn, refresh_token: refreshToken, + scope, } = await ( await fetch('https://www.linkedin.com/oauth/v2/accessToken', { method: 'POST', @@ -162,6 +168,8 @@ export class LinkedinPageProvider }) ).json(); + this.checkScopes(this.scopes, scope); + const { name, sub: id, diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 51b18117..7d2471fc 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -1,5 +1,9 @@ import { - AnalyticsData, AuthTokenDetails, PostDetails, PostResponse, SocialProvider + AnalyticsData, + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import sharp from 'sharp'; @@ -7,12 +11,12 @@ import { lookup } from 'mime-types'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { removeMarkdown } from '@gitroom/helpers/utils/remove.markdown'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -import { number, string } from 'yup'; export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; name = 'LinkedIn'; isBetweenSteps = false; + scopes = ['openid', 'profile', 'w_member_social', 'r_basicprofile']; async refreshToken(refresh_token: string): Promise { const { access_token: accessToken, refresh_token: refreshToken } = await ( @@ -69,9 +73,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { `${process.env.FRONTEND_URL}/integrations/social/linkedin${ refresh ? `?refresh=${refresh}` : '' }` - )}&state=${state}&scope=${encodeURIComponent( - 'openid profile w_member_social r_basicprofile' - )}`; + )}&state=${state}&scope=${encodeURIComponent(this.scopes.join(' '))}`; return { url, codeVerifier, @@ -100,6 +102,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { access_token: accessToken, expires_in: expiresIn, refresh_token: refreshToken, + scope, } = await ( await this.fetch('https://www.linkedin.com/oauth/v2/accessToken', { method: 'POST', @@ -110,6 +113,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }) ).json(); + this.checkScopes(this.scopes, scope); + const { name, sub: id, @@ -380,7 +385,10 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - actor: type === 'personal' ? `urn:li:person:${id}` : `urn:li:organization:${id}`, + actor: + type === 'personal' + ? `urn:li:person:${id}` + : `urn:li:organization:${id}`, object: topPostId, message: { text: removeMarkdown({ diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index b09bc655..25c64e18 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -20,6 +20,13 @@ export class PinterestProvider identifier = 'pinterest'; name = 'Pinterest'; isBetweenSteps = false; + scopes = [ + 'boards:read', + 'boards:write', + 'pins:read', + 'pins:write', + 'user_accounts:read', + ]; async refreshToken(refreshToken: string): Promise { const { access_token, expires_in } = await ( @@ -34,8 +41,7 @@ export class PinterestProvider body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - scope: - 'boards:read,boards:write,pins:read,pins:write,user_accounts:read', + scope: this.scopes.join(','), redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/pinterest`, }), }) @@ -83,7 +89,7 @@ export class PinterestProvider codeVerifier: string; refresh: string; }) { - const { access_token, refresh_token, expires_in } = await ( + const { access_token, refresh_token, expires_in, scope } = await ( await this.fetch('https://api.pinterest.com/v5/oauth/token', { method: 'POST', headers: { @@ -100,6 +106,8 @@ export class PinterestProvider }) ).json(); + this.checkScopes(this.scopes, scope); + const { id, profile_image, username } = await ( await this.fetch('https://api.pinterest.com/v5/user_account', { method: 'GET', diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts index db4ffbf2..7cd21e6c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts @@ -14,6 +14,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { identifier = 'reddit'; name = 'Reddit'; isBetweenSteps = false; + scopes = ['read', 'identity', 'submit', 'flair']; async refreshToken(refreshToken: string): Promise { const { @@ -62,9 +63,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { process.env.REDDIT_CLIENT_ID }&response_type=code&state=${state}&redirect_uri=${encodeURIComponent( `${process.env.FRONTEND_URL}/integrations/social/reddit` - )}&duration=permanent&scope=${encodeURIComponent( - 'read identity submit flair' - )}`; + )}&duration=permanent&scope=${encodeURIComponent(this.scopes.join(' '))}`; return { url, codeVerifier, @@ -77,6 +76,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, + scope } = await ( await this.fetch('https://www.reddit.com/api/v1/access_token', { method: 'POST', @@ -94,6 +94,8 @@ export class RedditProvider extends SocialAbstract implements SocialProvider { }) ).json(); + this.checkScopes(this.scopes, scope); + const { name, id, icon_img } = await ( await this.fetch('https://oauth.reddit.com/api/v1/me', { headers: { 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 7449ceb0..80c06876 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -71,4 +71,5 @@ export interface SocialProvider identifier: string; name: string; isBetweenSteps: boolean; + scopes: string[]; } diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 8f02cdfa..37fe1500 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -15,6 +15,12 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { identifier = 'threads'; name = 'Threads'; isBetweenSteps = false; + scopes = [ + 'threads_basic', + 'threads_content_publish', + 'threads_manage_replies', + 'threads_manage_insights', + ]; async refreshToken(refresh_token: string): Promise { return { @@ -42,9 +48,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { }` )}` + `&state=${state}` + - `&scope=${encodeURIComponent( - 'threads_basic,threads_content_publish,threads_manage_replies,threads_manage_insights' - )}`, + `&scope=${encodeURIComponent(this.scopes.join(','))}`, codeVerifier: makeId(10), state, }; @@ -214,7 +218,9 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { const { id: containerId } = await ( await this.fetch( - `https://graph.threads.net/v1.0/${id}/threads?text=${firstPost?.message}&media_type=CAROUSEL&children=${medias.join( + `https://graph.threads.net/v1.0/${id}/threads?text=${ + firstPost?.message + }&media_type=CAROUSEL&children=${medias.join( ',' )}&access_token=${accessToken}`, { @@ -304,10 +310,12 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { data?.map((d: any) => ({ label: capitalize(d.name), percentageChange: 5, - data: d.total_value ? [{total: d.total_value.value, date: dayjs().format('YYYY-MM-DD')}] : d.values.map((v: any) => ({ - total: v.value, - date: dayjs(v.end_time).format('YYYY-MM-DD'), - })), + data: d.total_value + ? [{ total: d.total_value.value, date: dayjs().format('YYYY-MM-DD') }] + : d.values.map((v: any) => ({ + total: v.value, + date: dayjs(v.end_time).format('YYYY-MM-DD'), + })), })) || [] ); } diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 26ea3d6d..15d3b766 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -12,6 +12,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { identifier = 'tiktok'; name = 'Tiktok'; isBetweenSteps = false; + scopes = ['user.info.basic', 'video.publish', 'video.upload']; async refreshToken(refreshToken: string): Promise { const value = { @@ -74,9 +75,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { )}` + `&state=${state}` + `&response_type=code` + - `&scope=${encodeURIComponent( - 'user.info.basic,video.publish,video.upload' - )}`, + `&scope=${encodeURIComponent(this.scopes.join(','))}`, codeVerifier: state, state, }; @@ -99,7 +98,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { : `${process.env.FRONTEND_URL}/integrations/social/tiktok`, }; - const { access_token, refresh_token } = await ( + const { access_token, refresh_token, scope } = await ( await this.fetch('https://open.tiktokapis.com/v2/oauth/token/', { headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -109,6 +108,8 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { }) ).json(); + this.checkScopes(this.scopes, scope); + const { data: { user: { avatar_url, display_name, open_id }, diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 2470a3f9..7852ee6c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -15,6 +15,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { identifier = 'x'; name = 'X'; isBetweenSteps = false; + scopes = []; async refreshToken(refreshToken: string): Promise { const startingClient = new TwitterApi({ @@ -55,9 +56,8 @@ export class XProvider extends SocialAbstract implements SocialProvider { }); 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${refresh ? `?refresh=${refresh}` : ''}`, { 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 d275f4e7..f7117431 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -46,6 +46,17 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { identifier = 'youtube'; name = 'YouTube'; isBetweenSteps = false; + scopes = [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtube.force-ssl', + 'https://www.googleapis.com/auth/youtube.readonly', + 'https://www.googleapis.com/auth/youtube.upload', + 'https://www.googleapis.com/auth/youtubepartner', + 'https://www.googleapis.com/auth/youtubepartner', + 'https://www.googleapis.com/auth/yt-analytics.readonly', + ]; async refreshToken(refresh_token: string): Promise { const { client, oauth2 } = clientAndYoutube(); @@ -79,17 +90,7 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { prompt: 'consent', state, redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/youtube`, - scope: [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/youtube', - 'https://www.googleapis.com/auth/youtube.force-ssl', - 'https://www.googleapis.com/auth/youtube.readonly', - 'https://www.googleapis.com/auth/youtube.upload', - 'https://www.googleapis.com/auth/youtubepartner', - 'https://www.googleapis.com/auth/youtubepartner', - 'https://www.googleapis.com/auth/yt-analytics.readonly', - ], + scope: this.scopes.slice(0), }), codeVerifier: makeId(11), state, @@ -104,6 +105,9 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { const { client, oauth2 } = clientAndYoutube(); const { tokens } = await client.getToken(params.code); client.setCredentials(tokens); + const { scopes } = await client.getTokenInfo(tokens.access_token!); + this.checkScopes(this.scopes, scopes); + const user = oauth2(client); const { data } = await user.userinfo.get();