feat: check for scopes
This commit is contained in:
parent
1016dedc15
commit
1e661e56cd
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
response.status(HttpStatusCode.NotAcceptable).json({ invalid: true });
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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'),
|
||||
})),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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'),
|
||||
})),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -71,4 +71,5 @@ export interface SocialProvider
|
|||
identifier: string;
|
||||
name: string;
|
||||
isBetweenSteps: boolean;
|
||||
scopes: string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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'),
|
||||
})),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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 },
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
identifier = 'x';
|
||||
name = 'X';
|
||||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<AuthTokenDetails> {
|
||||
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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue