const axios = require('axios') const AUTHORIZE_TOKEN_URL = 'https://api.instagram.com/oauth/access_token' const GRAPH_BASE_URL = 'https://graph.instagram.com' /** * Exchange the short-lived OAuth `code` (from the Instagram Login redirect) * for a short-lived (1hr) user access token. */ async function exchangeCodeForToken (code) { const clientId = process.env.INSTAGRAM_CLIENT_ID const clientSecret = process.env.INSTAGRAM_APP_SECRET const redirectUri = process.env.INSTAGRAM_REDIRECT_URI if (!clientId || !clientSecret || !redirectUri) { throw new Error('Instagram OAuth is not configured (INSTAGRAM_CLIENT_ID/APP_SECRET/REDIRECT_URI)') } const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: 'authorization_code', redirect_uri: redirectUri, code, }) const { data } = await axios.post(AUTHORIZE_TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15000, }) return { accessToken: data?.access_token, userId: data?.user_id, } } /** * Exchange a short-lived token for a long-lived one (~60 days). */ async function exchangeForLongLivedToken (shortLivedToken) { const clientSecret = process.env.INSTAGRAM_APP_SECRET if (!clientSecret) { throw new Error('INSTAGRAM_APP_SECRET is not configured') } const { data } = await axios.get(`${GRAPH_BASE_URL}/access_token`, { params: { grant_type: 'ig_exchange_token', client_secret: clientSecret, access_token: shortLivedToken, }, timeout: 15000, }) const expiresInSeconds = Number(data?.expires_in) || 60 * 24 * 60 * 60 return { accessToken: data?.access_token, expiresAt: new Date(Date.now() + expiresInSeconds * 1000), } } /** * Refresh a long-lived token before it expires (must be at least 24h old, valid). */ async function refreshLongLivedToken (longLivedToken) { const { data } = await axios.get(`${GRAPH_BASE_URL}/refresh_access_token`, { params: { grant_type: 'ig_refresh_token', access_token: longLivedToken, }, timeout: 15000, }) const expiresInSeconds = Number(data?.expires_in) || 60 * 24 * 60 * 60 return { accessToken: data?.access_token, expiresAt: new Date(Date.now() + expiresInSeconds * 1000), } } /** * Basic profile info for the connected Instagram Business/Creator account. */ async function getProfile (accessToken) { const { data } = await axios.get(`${GRAPH_BASE_URL}/me`, { params: { fields: 'id,username,account_type,name,biography,profile_picture_url', access_token: accessToken, }, timeout: 15000, }) return { id: data?.id, username: data?.username, accountType: data?.account_type || null, name: data?.name || null, biography: data?.biography || null, profilePictureUrl: data?.profile_picture_url || null, } } /** * Paginated media list (posts + reels). `after` is a Graph API paging cursor. */ async function getMedia (accessToken, { after } = {}) { const { data } = await axios.get(`${GRAPH_BASE_URL}/me/media`, { params: { fields: 'id,caption,media_type,media_product_type,media_url,permalink,timestamp', limit: 25, access_token: accessToken, ...(after ? { after } : {}), }, timeout: 15000, }) return { items: Array.isArray(data?.data) ? data.data : [], nextCursor: data?.paging?.cursors?.after || null, hasNext: Boolean(data?.paging?.next), } } module.exports = { exchangeCodeForToken, exchangeForLongLivedToken, refreshLongLivedToken, getProfile, getMedia, }