Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/app/admin/instagram/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ interface InstagramPost {
post_url: string
}

const META_GRAPH_VERSION = 'v26.0'
const META_INSTAGRAM_SCOPES = [
'public_profile',
'email',
'pages_show_list',
'pages_read_engagement',
'instagram_basic',
].join(',')

export default function AdminInstagramPage() {
const [posts, setPosts] = useState<InstagramPost[]>([])
const [loading, setLoading] = useState(true)
Expand All @@ -31,7 +40,7 @@ export default function AdminInstagramPage() {
const [showSetupGuide, setShowSetupGuide] = useState(false)

// Meta Devtools MCP Remote Server Config
const [mcpServerUrl, setMcpServerUrl] = useState('https://graph.facebook.com/v19.0/mcp')
const [mcpServerUrl, setMcpServerUrl] = useState(`https://graph.facebook.com/${META_GRAPH_VERSION}/mcp`)
const [mcpStatus, setMcpStatus] = useState<'DISCOVERED' | 'AUTHENTICATING' | 'OFFLINE'>('DISCOVERED')

// Manual New Post Form
Expand Down Expand Up @@ -108,7 +117,7 @@ export default function AdminInstagramPage() {
appId: appId || '1679398459977278',
cookie: true,
xfbml: true,
version: 'v19.0',
version: META_GRAPH_VERSION,
})
}
}
Expand Down Expand Up @@ -145,7 +154,7 @@ export default function AdminInstagramPage() {
setMessage('⚠️ Facebook SDK Login cancelled or not authorized.')
}
},
{ scope: 'public_profile,email,user_profile,user_media' }
{ scope: META_INSTAGRAM_SCOPES, return_scopes: true }
)
} else {
handleConnectAccount()
Expand Down Expand Up @@ -182,7 +191,7 @@ export default function AdminInstagramPage() {
let authUrl = ''

if (authType === 'meta_business') {
authUrl = `https://www.facebook.com/v19.0/dialog/oauth?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=public_profile,email,user_profile,user_media&response_type=code`
authUrl = `https://www.facebook.com/${META_GRAPH_VERSION}/dialog/oauth?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(META_INSTAGRAM_SCOPES)}&response_type=code&state=facebook`
} else {
authUrl = `https://api.instagram.com/oauth/authorize?client_id=${cleanAppId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=user_profile,user_media&response_type=code`
}
Expand Down Expand Up @@ -284,7 +293,7 @@ export default function AdminInstagramPage() {
</span>
</div>
<p className="text-xs text-white/60 font-mono mt-0.5">
Instagram Graph API v19.0 • Followers: <strong className="text-white">11,355</strong> • Following: <strong className="text-white">459</strong>
Instagram Graph API {META_GRAPH_VERSION} • Followers: <strong className="text-white">11,355</strong> • Following: <strong className="text-white">459</strong>
</p>
</div>
</div>
Expand Down Expand Up @@ -462,10 +471,10 @@ export default function AdminInstagramPage() {
<div className="space-y-1">
<label className="text-white/70">Discovered MCP Tools &amp; Scopes</label>
<div className="rounded-xl border border-white/15 bg-black/60 p-2.5 flex flex-wrap gap-2">
<span className="rounded-md bg-blue-500/20 px-2 py-1 text-[10px] text-blue-300 border border-blue-500/30">user_profile</span>
<span className="rounded-md bg-purple-500/20 px-2 py-1 text-[10px] text-purple-300 border border-purple-500/30">user_media</span>
<span className="rounded-md bg-pink-500/20 px-2 py-1 text-[10px] text-pink-300 border border-pink-500/30">instagram_basic</span>
<span className="rounded-md bg-emerald-500/20 px-2 py-1 text-[10px] text-emerald-300 border border-emerald-500/30">pages_show_list</span>
<span className="rounded-md bg-blue-500/20 px-2 py-1 text-[10px] text-blue-300 border border-blue-500/30">public_profile</span>
<span className="rounded-md bg-purple-500/20 px-2 py-1 text-[10px] text-purple-300 border border-purple-500/30">pages_show_list</span>
<span className="rounded-md bg-pink-500/20 px-2 py-1 text-[10px] text-pink-300 border border-pink-500/30">pages_read_engagement</span>
<span className="rounded-md bg-emerald-500/20 px-2 py-1 text-[10px] text-emerald-300 border border-emerald-500/30">instagram_basic</span>
</div>
</div>
</div>
Expand Down
60 changes: 41 additions & 19 deletions src/app/api/instagram-callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { supabase } from '@/lib/supabase'

export const revalidate = 0

const META_GRAPH_VERSION = 'v26.0'

// Official Instagram OAuth Callback Endpoint
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const error = searchParams.get('error')
const errorReason = searchParams.get('error_reason')
const provider = searchParams.get('state') === 'facebook' ? 'facebook' : 'instagram'

if (error || !code) {
console.error('Instagram OAuth Error:', error || 'No code returned')
Expand All @@ -18,24 +21,38 @@ export async function GET(request: Request) {
// Remove trailing #_ appended by Instagram OAuth redirect
const cleanCode = code.replace(/#_$/, '')

const clientId = process.env.INSTAGRAM_CLIENT_ID || '1679398459977278'
const clientSecret = process.env.INSTAGRAM_CLIENT_SECRET || ''
const clientId = process.env.META_APP_ID || process.env.INSTAGRAM_CLIENT_ID || '1679398459977278'
const clientSecret =
process.env.META_APP_SECRET ||
process.env.FACEBOOK_APP_SECRET ||
process.env.INSTAGRAM_CLIENT_SECRET ||
''
const redirectUri = `${origin}/api/instagram-callback`

try {
// 1. Exchange Authorization Code for Short-Lived Access Token
const formData = new URLSearchParams()
formData.append('client_id', clientId)
formData.append('client_secret', clientSecret)
formData.append('grant_type', 'authorization_code')
formData.append('redirect_uri', redirectUri)
formData.append('code', cleanCode)

const tokenRes = await fetch('https://api.instagram.com/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
})
// Exchange the code with the same provider that issued it. Facebook Login
// codes cannot be exchanged at Instagram's OAuth endpoint.
let tokenRes: Response
if (provider === 'facebook') {
const tokenUrl = new URL(`https://graph.facebook.com/${META_GRAPH_VERSION}/oauth/access_token`)
tokenUrl.searchParams.set('client_id', clientId)
tokenUrl.searchParams.set('client_secret', clientSecret)
tokenUrl.searchParams.set('redirect_uri', redirectUri)
tokenUrl.searchParams.set('code', cleanCode)
tokenRes = await fetch(tokenUrl)
} else {
const formData = new URLSearchParams()
formData.append('client_id', clientId)
formData.append('client_secret', clientSecret)
formData.append('grant_type', 'authorization_code')
formData.append('redirect_uri', redirectUri)
formData.append('code', cleanCode)
tokenRes = await fetch('https://api.instagram.com/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
})
}

const tokenData = await tokenRes.json()

Expand All @@ -48,7 +65,7 @@ export async function GET(request: Request) {
let finalAccessToken = tokenData.access_token

// 2. Exchange for Long-Lived Access Token (60 days validity) if client secret present
if (clientSecret) {
if (clientSecret && provider === 'instagram') {
try {
const longLivedRes = await fetch(
`https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=${clientSecret}&access_token=${tokenData.access_token}`
Expand All @@ -68,11 +85,16 @@ export async function GET(request: Request) {
value: finalAccessToken,
})

// 4. Trigger Feed Fetcher to auto-populate Instagram Posts
const feedRes = await fetch(`https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${finalAccessToken}`)
// Facebook user tokens must first resolve the connected Page's Instagram
// professional account; the feed endpoint already handles that lookup.
const feedUrl =
provider === 'facebook'
? `${origin}/api/instagram-feed?token=${encodeURIComponent(finalAccessToken)}`
: `https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${finalAccessToken}`
const feedRes = await fetch(feedUrl)
const feedData = await feedRes.json()

if (feedData.data && Array.isArray(feedData.data)) {
if (provider === 'instagram' && feedData.data && Array.isArray(feedData.data)) {
const posts = feedData.data.map((item: any) => ({
image_url: item.media_url || item.thumbnail_url || '/hero-cyber-portrait.jpg',
caption: item.caption || 'Live Instagram Post @sahad_____sha',
Expand Down
10 changes: 6 additions & 4 deletions src/app/api/instagram-feed/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getToken, UserAuthorizationRequiredError } from '@vercel/connect'

export const revalidate = 0

const META_GRAPH_VERSION = 'v26.0'

// Official Instagram API with Facebook Login & Graph API Reader
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
Expand Down Expand Up @@ -53,12 +55,12 @@ export async function GET(request: Request) {
} catch {}
}

// 3. Try Facebook Graph API for Instagram Professional Accounts (/v19.0/me/accounts & /v19.0/{ig-user-id}/media)
// 3. Try Facebook Graph API for Instagram Professional Accounts.
if (token.trim()) {
try {
// Step A: Check if Token is a Facebook Professional User Token
const fbRes = await fetch(
`https://graph.facebook.com/v19.0/me/accounts?fields=instagram_business_account{id,username,media_count},name&access_token=${token.trim()}`
`https://graph.facebook.com/${META_GRAPH_VERSION}/me/accounts?fields=instagram_business_account{id,username,media_count},name&access_token=${token.trim()}`
)
const fbData = await fbRes.json()

Expand All @@ -72,7 +74,7 @@ export async function GET(request: Request) {

// Step B: Query Instagram Business Account Media
const mediaEndpoint = igUserId
? `https://graph.facebook.com/v19.0/${igUserId}/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}`
? `https://graph.facebook.com/${META_GRAPH_VERSION}/${igUserId}/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}`
: `https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,like_count,comments_count&access_token=${token.trim()}`

const graphRes = await fetch(mediaEndpoint)
Expand All @@ -86,7 +88,7 @@ export async function GET(request: Request) {
comments_count: item.comments_count || Math.floor(Math.random() * 30 + 10),
post_url: item.permalink || `https://www.instagram.com/${targetUsername}/`,
}))
apiSource = igUserId ? 'FACEBOOK_LOGIN_GRAPH_API_V19' : 'INSTAGRAM_BASIC_DISPLAY_API'
apiSource = igUserId ? 'FACEBOOK_LOGIN_GRAPH_API_V26' : 'INSTAGRAM_BASIC_DISPLAY_API'
}
} catch (e) {
console.error('Facebook / Instagram API Exception:', e)
Expand Down