Skip to content

Commit

Permalink
feat: addition of flow to create long lived access token using app id…
Browse files Browse the repository at this point in the history
… and app secret
  • Loading branch information
tomashco committed Jul 3, 2024
1 parent 58a6fbd commit fd499d7
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dev/pnpm-lock.yaml
# Testing
test-results

# certificates
**/certificates

# Created by https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode

### macOS ###
Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

This plugin allows you to use an instagram connected feed as content to be shown inside payload blog.

!!!important: starting from 0.0.8 the access token will be stored to db to allow to refresh it automatically. Read the updated setup guide to learn how to configure the plugin.
🔥 important: starting from 0.0.8 the access token will be stored to db to allow to refresh it automatically. Read the updated setup guide to learn how to configure the plugin.

## How to setup
1. Get an access token to connect to [Instagram Basic Display API](https://developers.facebook.com/docs/instagram-basic-display-api):
[This tutorial](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started) shows you:
- how to create a facebook developer app;
- how to add tester users (i.e. the account from where you will get the content);
- how to retrieve the access token (it's important to get the long lived token with an expiration of 60 days).
1. create a facebook developer app following [this tutorial](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started), up to point 3.

2. Add the access token in the Instagram Plugin page inside the admin panel.
🔥 Important:
- As ```redirect_uri``` set: https://yourBaseUrl/api/instagram/authorize. Change "yourBaseUrl" with your app url. If you are using this from localhost, run your payload instance with ```--experimental-https``` flagged, as https is needed for the instagram token request!
- as revoke authorization set: https://yourBaseUrl/api/instagram/unauthorize
- as request to delete data set: https://yourBaseUrl/api/instagram/delete

3. Add the plugin to the payload.config.ts, together with all your config, as follows:
2. Add the plugin to the payload.config.ts, together with all your config, as follows:

```
import { instagramPlugin } from 'instagram-payload-plugin'
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "payload-instagram-plugin",
"version": "0.0.10",
"version": "0.1.0-beta.1",
"type": "module",
"homepage:": "https://github.com/tomashco/payload-instagram-plugin",
"repository": "[email protected]:tomashco/payload-instagram-plugin.git",
Expand All @@ -16,7 +16,7 @@
"Instagram"
],
"scripts": {
"dev": "cd dev && cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DROP_DATABASE='true' next dev --turbo",
"dev": "cd dev && cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DROP_DATABASE='true' next dev --turbo --experimental-https",
"build": "tsc",
"test": "cd test && jest --config=./jest.config.js",
"test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
Expand Down
43 changes: 43 additions & 0 deletions src/api/hooks/useConfigInstagram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { QueryClient, useMutation } from '@tanstack/react-query'
import { addAppIdEndpoint } from '../../plugin'
import { useRouter } from 'next/navigation'

type useConfigInstagramType = {
appId: string
setAppId: React.Dispatch<React.SetStateAction<string>>
setAppSecret: React.Dispatch<React.SetStateAction<string>>
}

const useConfigInstagram = ({ appId, setAppId, setAppSecret }: useConfigInstagramType) => {
const router = useRouter()
const { mutate: addAppId } = useMutation({
mutationFn: (body: { appId: string; appSecret: string }) =>
fetch(addAppIdEndpoint, {
body: JSON.stringify({
...body,
redirectUri: `${window.location.origin}/api/instagram/authorize`,
}),
method: 'POST',
credentials: 'include',
}).then(res => {
if (!res.ok) throw new Error(res.status.toString())
return res.json()
}),
onSuccess: _res => {
const redirectUri = `${window.location.origin}/api/instagram/authorize`
const authorizeEndpoint = `https://api.instagram.com/oauth/authorize?client_id=${appId}&redirect_uri=${redirectUri}&scope=user_profile,user_media&response_type=code`
router.push(authorizeEndpoint)
setAppId('')
setAppSecret('')
},
onError: _res => {
alert('Status: the configuration provided is not correct, try again')
setAppId('')
setAppSecret('')
},
})

return { addAppId }
}

export default useConfigInstagram
29 changes: 20 additions & 9 deletions src/components/InstagramPostsClient/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from '@payloadcms/ui/elements/Button'
import { baseEndpoint, childrenEndpoint } from '../../plugin'
import InstagramPostsApi from '../../api/hooks/useInstagramPosts'
import InstagramCollectionApi from '../../api/hooks/useInstagramCollection'
import TokenApi from '../../api/hooks/useToken'
import ConfigInstagramApi from '../../api/hooks/useConfigInstagram'
import { PostType } from '../../types'

const queryClient = new QueryClient()
Expand All @@ -17,9 +17,12 @@ const LoadingCards = () =>

function ManagePosts() {
const [endpoint, setEndpoint] = React.useState<string>(baseEndpoint)
const [token, setToken] = React.useState<string>('')
const [appId, setAppId] = React.useState<string>('')
const [appSecret, setAppSecret] = React.useState<string>('')
const [first, setFirst] = React.useState<string>('')
const { addAccessToken } = TokenApi({ endpoint, queryClient, setToken })

// const { addAccessToken } = TokenApi({ endpoint, queryClient, setToken })
const { addAppId } = ConfigInstagramApi({ appId, setAppId, setAppSecret })
const { isPending, error, response, isFetching, isLoading } = InstagramPostsApi({ endpoint })
const { instagramCollection, mutateInstagramCollection } = InstagramCollectionApi({ queryClient })

Expand All @@ -32,24 +35,32 @@ function ManagePosts() {

const onSubmitHandler = async (evt: any) => {
evt.preventDefault()
await addAccessToken({ accessToken: token })
await addAppId({ appId, appSecret })
}

if (isLoading) return <p>Loading...</p>

if (error?.message === '403')
return (
<form onSubmit={onSubmitHandler}>
<p>Please insert a valid access token: </p>
<p>Please insert your Instagram configuration:</p>
<div className="field-type email">
<p>App ID:</p>
<input
id="field-appId"
className="field-type__wrap"
value={appId}
onChange={evt => setAppId(evt.target.value)}
/>
<p style={{ marginTop: '2rem' }}>App Secret:</p>
<input
id="field-token"
id="field-appSecret"
className="field-type__wrap"
value={token}
onChange={evt => setToken(evt.target.value)}
value={appSecret}
onChange={evt => setAppSecret(evt.target.value)}
/>
<Button id="form-token" onClick={onSubmitHandler}>
Add Access Token
Configure Instagram Plugin
</Button>
</div>
</form>
Expand Down
120 changes: 107 additions & 13 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,46 @@ import { GlobalConfig } from 'payload/types'

export const baseEndpoint = '/api/instagram/list'
export const addAccessTokenEndpoint = '/api/apikeys'
export const addAppIdEndpoint = '/api/instagram/authorize'
export const childrenEndpoint = '/api/instagram/children'
export const mediaEndpoint = '/api/instagram/media'
export const instagramCollectionEndpoint = '/api/instagram-posts'
export const getAccessToken = 'https://api.instagram.com/oauth/access_token'

export const instagramPlugin =
(pluginOptions: PluginTypes): Plugin =>
incomingConfig => {
const InstagramConfig: GlobalConfig = {
slug: 'instagramConfig',
admin: {
hidden: true,
hideAPIURL: true,
},
access: {
read: ({ req }) => !req.user,
update: ({ req }) => !req.user && !req.context.bypass,
},
fields: [
{
type: 'text',
name: 'appId',
hidden: true,
access: {
read: () => false,
update: () => false,
},
},
{
type: 'text',
name: 'appSecret',
hidden: true,
access: {
read: () => false,
update: () => false,
},
},
],
}
const ApiKeys: GlobalConfig = {
slug: 'apikeys',
admin: {
Expand Down Expand Up @@ -107,24 +140,85 @@ export const instagramPlugin =
config.endpoints = [
...(config.endpoints || []),
{
path: '/authorize',
path: '/instagram/authorize',
method: 'post',
handler: async req => {
try {
const body = req.json ? await req.json() : {}
const { appId, redirectUri } = body
const { appId, appSecret } = body

const test = await fetch(
`https://api.instagram.com/oauth/authorize?client_id=${appId}&redirect_uri=${redirectUri}&scope=user_profile,user_media&response_type=code`,
)
return new Response(
JSON.stringify({
accessToken: 'test',
}),
{
status: 200,
req.payload.updateGlobal({
slug: 'instagramConfig',
data: {
appId,
appSecret,
},
)
context: {
bypass: true,
},
})

return new Response(JSON.stringify({ message: 'Configuration added' }), {
status: 200,
})
} catch (error) {
return new Response(JSON.stringify({ message: 'Error refreshing token' }), {
status: 500,
})
}
},
},
{
path: '/instagram/authorize',
method: 'get',
handler: async req => {
const baseUrl =
req.host === 'localhost' ? `${req.origin}:${process.env.PORT || 3000}` : req.origin
try {
const { code } = req.query
const { appId, appSecret } = await req.payload.findGlobal({
slug: 'instagramConfig',
overrideAccess: true,
showHiddenFields: true,
})

const formData = new FormData()
formData.append('client_id', appId as string)
formData.append('client_secret', appSecret as string)
formData.append('grant_type', 'authorization_code')
formData.append('redirect_uri', `${baseUrl}/api/instagram/authorize`)
formData.append('code', code as string)

const response = await fetch(getAccessToken, {
method: 'POST',
body: formData,
}).then(res => {
return res.json()
})

if (!response.access_token) {
throw new Error('Invalid token')
}

const { access_token } = await fetch(
`https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=${appSecret}&access_token=${response.access_token}`,
).then(res => res.json())

if (!access_token) {
throw new Error('Error refreshing token')
}

req.payload.updateGlobal({
slug: 'apikeys',
data: {
refreshToken: access_token || '',
},
context: {
bypass: true,
},
})

return Response.redirect(`${baseUrl}/admin`, 302)
} catch (error) {
return new Response(JSON.stringify({ message: 'Error refreshing token' }), {
status: 500,
Expand Down Expand Up @@ -258,7 +352,7 @@ export const instagramPlugin =
},
]

config.globals = [...(config.globals || []), ApiKeys]
config.globals = [...(config.globals || []), ApiKeys, InstagramConfig]

config.hooks = {
...(config.hooks || {}),
Expand Down

0 comments on commit fd499d7

Please sign in to comment.