Skip to content
Open
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
257 changes: 257 additions & 0 deletions src/controllers/socialMediaControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
const fs = require('fs');
const axios = require('axios');
const pinterestSchedule = require('../models/pinterestSchedule');

const OAUTH_URL = 'https://api.pinterest.com/v5/oauth';
const ACCESS_TOKEN_FILE = 'access_token.txt';
const PINTEREST_ENDPOINT = process.env.PINTEREST_SANDBOX_API
? 'https://api-sandbox.pinterest.com/v5'
: 'https://api.pinterest.com/v5';
const ONE_COMMUNITY_BOARD_NAME = 'OneCommunity';

// get access token from pinterest and store it
async function getPinterestAccessToken(authorizationCode) {
const auth = `${process.env.PINTEREST_APP_ID}:${process.env.PINTEREST_APP_SECRET}`;
const b64auth = Buffer.from(auth).toString('base64');

const authHeaders = {
Authorization: `Basic ${b64auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
};

const postData = new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: process.env.PINTEREST_REDIRECT_URI,
});

const response = await axios.post(`${OAUTH_URL}/token`, postData.toString(), {
headers: authHeaders,
});

const { access_token, refresh_token, expires_in } = response.data;
const expireTime = new Date().getTime() + expires_in * 1000;

const jsonToken = { accessToken: access_token, refreshToken: refresh_token, expireTime };
fs.writeFileSync(ACCESS_TOKEN_FILE, JSON.stringify(jsonToken));
return jsonToken;
}
async function refreshPinterestAccessToken() {
const tokenData = JSON.parse(fs.readFileSync(ACCESS_TOKEN_FILE, 'utf8'));
const auth = `${process.env.PINTEREST_APP_ID}:${process.env.PINTEREST_APP_SECRET}`;
const b64auth = Buffer.from(auth).toString('base64');

const postData = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: tokenData.refreshToken,
});

const response = await axios.post(`${OAUTH_URL}/token`, postData.toString(), {
headers: {
Authorization: `Basic ${b64auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});

const { access_token, expires_in } = response.data;
const expireTime = new Date().getTime() + expires_in * 1000;

const updated = { ...tokenData, accessToken: access_token, expireTime };
fs.writeFileSync(ACCESS_TOKEN_FILE, JSON.stringify(updated));
return updated;
}
function getValidAccessTokenOrExcepts() {
if (!fs.existsSync(ACCESS_TOKEN_FILE)) {
throw new Error('Access token file not found');
}
const tokenData = fs.readFileSync(ACCESS_TOKEN_FILE, 'utf8');
const tokenObject = JSON.parse(tokenData);
const expireTime = new Date(tokenObject.expireTime);
if (new Date() > expireTime) {
throw new Error('Access token expired');
}
return tokenObject;
}

// fetch access token from local file
async function fetchAccessToken() {
if (process.env.PINTEREST_SANDBOX_API) {
return process.env.PINTEREST_SANDBOX_API_TOKEN;
}

let tokenObject;
try {
tokenObject = getValidAccessTokenOrExcepts();
} catch {
// Token missing or expired — try refresh first
try {
tokenObject = await refreshPinterestAccessToken();
} catch {
throw new Error('Pinterest token expired. Please re-authenticate via /api/pinterest/auth');
}
}

return tokenObject.accessToken;
}

async function getPinterestRequestHeaders() {
const accessToken = await fetchAccessToken();
return { Authorization: `Bearer ${accessToken}` };
}

async function fetchBoardList() {
const requestUrl = `${PINTEREST_ENDPOINT}/boards`;
const response = await axios.get(requestUrl, {
headers: await getPinterestRequestHeaders(),
responseType: 'json',
});
const boardList = response.data.items;
// console.log(boardList);
// res.status(200).json(boardList);
return boardList;
}
function initiatePinterestAuth(_req, res) {
const scopes = ['pins:read', 'pins:write', 'boards:read', 'boards:write', 'users:read'];
const authUrl = new URL('https://www.pinterest.com/oauth/');
authUrl.searchParams.set('client_id', process.env.PINTEREST_APP_ID);
authUrl.searchParams.set('redirect_uri', process.env.PINTEREST_REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', scopes.join(','));
res.redirect(authUrl.toString());
}

async function handlePinterestCallback(req, res) {
const { code } = req.query;
if (!code) return res.status(400).json({ error: 'No authorization code received.' });
try {
await getPinterestAccessToken(code);
res.status(200).json({ message: 'Pinterest authenticated successfully.' });
} catch (err) {
res.status(500).json({ error: err.message });
}
}

async function createBoard(title, details) {
const requestUrl = `${PINTEREST_ENDPOINT}/boards`;
const postData = { name: title, description: details, privacy: 'PUBLIC' };
// const createPinHeaders = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
const createPinHeaders = await getPinterestRequestHeaders();

const response = await axios.post(requestUrl, postData, {
headers: createPinHeaders,
responseType: 'json',
});

return response.data;
}

async function getPostData(requestBody) {
const boardList = await fetchBoardList();

// If One Community board not exist, create one
let OneCommBoard = boardList.find((board) => board.name === ONE_COMMUNITY_BOARD_NAME);
if (!OneCommBoard) {
const boardDetails = 'Updates from One Community';
OneCommBoard = await createBoard(ONE_COMMUNITY_BOARD_NAME, boardDetails);
}

const boardId = OneCommBoard.id;

// Process content
let sourceType;
const { imgType } = requestBody;
let mediaSourceItems;
const { mediaItems } = requestBody;
let mediaSource;

if (imgType === 'FILE') {
// Process upload image file
const contentType = mediaItems.split(';')[0].split(':')[1].trim();
const data = mediaItems.split(',')[1].trim();

mediaSourceItems = { content_type: contentType, data };
sourceType = 'image_base64';
mediaSource = { source_type: sourceType, ...mediaSourceItems };
} else {
// Process url image source
mediaSourceItems = mediaItems;
sourceType = 'image_url';
mediaSource = { source_type: sourceType, ...mediaSourceItems };
}
const { description } = requestBody;
const { title } = requestBody;

const postData = { board_id: boardId, description, title, media_source: mediaSource };

return postData;
}

// Send post pin request to Pinterest API
async function postPinImmediately(postData) {
const requestUrl = `${PINTEREST_ENDPOINT}/pins`;
return await axios.post(requestUrl, postData, {
headers: await getPinterestRequestHeaders(),
responseType: 'json',
});
}

async function createPin(req, res) {
try {
const postData = await getPostData(req.body);
const response = await postPinImmediately(postData);
res.status(200).json(response.data);
} catch (error) {
if (error.response) {
console.error('Error creating Pinterest pin:', error.response.data);
res.status(error.response.status).json({ error: error.response.data.message });
} else {
console.error('Error creating Pinterest pin:', error.message);
res.status(500).json({ error: error.message || 'Failed to create Pinterest pin' });
}
}
}

async function schedulePin(req, res) {
try {
const postDataObj = await getPostData(req.body);
const postData = JSON.stringify(postDataObj);
const { scheduledTime } = req.body;

const scheduledPin = new pinterestSchedule({ postData, scheduledTime });
await scheduledPin.save();
res.status(200).send();
} catch (err) {
res.status(500).send();
}
}

async function fetchScheduledPin(_req, res) {
try {
// TODO: add pagination
const scheduledPinList = await pinterestSchedule.find();
res.status(200).json(scheduledPinList);
} catch (err) {
res.status(500).send('Failed to fetch scheduled pins');
}
}

async function deletedScheduledPin(req, res) {
try {
await pinterestSchedule.deleteOne({ _id: req.params.id });
res.status(200).send('Scheduled pin post deleted successfully!');
} catch (err) {
res.status(500).send('Failed to deleted scheduled pin post!');
}
}

module.exports = {
createPin,
fetchBoardList,
createBoard,
schedulePin,
fetchScheduledPin,
deletedScheduledPin,
postPinImmediately,
initiatePinterestAuth,
handlePinterestCallback,
};
26 changes: 26 additions & 0 deletions src/cronjobs/pinterestScheduleJob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { CronJob } = require('cron');
const moment = require('moment-timezone');
const pinterestSchedule = require('../models/pinterestSchedule');
const { postPinImmediately } = require('../controllers/socialMediaControllers');

async function pinterestScheduleJob() {
new CronJob('* * * * *', async () => {
try {
const now = new Date().toISOString();
const scheduledPinList = await pinterestSchedule.find({ scheduledTime: { $lte: now } });

for (const scheduledPin of scheduledPinList) {
const postData = JSON.parse(scheduledPin.postData);
// console.log(`Creating pin: ${scheduledPin}`)
await postPinImmediately(postData);

// Delete database record after posting to pinterest
await pinterestSchedule.deleteOne(scheduledPin);
}
} catch (err) {
console.log(err);
}
}).start();
}

module.exports = pinterestScheduleJob;
10 changes: 10 additions & 0 deletions src/models/pinterestSchedule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const mongoose = require('mongoose');

const { Schema } = mongoose;

const pinterestSchedule = new Schema({
postData: { type: String, required: true },
scheduledTime: { type: Date, required: true },
});

module.exports = mongoose.model('pinterestSchedule', pinterestSchedule);
26 changes: 26 additions & 0 deletions src/routes/socialMediaRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const express = require('express');
const {
createPin,
schedulePin,
fetchScheduledPin,
deletedScheduledPin,
} = require('../controllers/socialMediaControllers');
const {
initiatePinterestAuth,
handlePinterestCallback,
} = require('../controllers/socialMediaControllers');

const socialMediaRouter = express.Router();

// social media routes
socialMediaRouter.route('/pinterest/createPin').post(createPin);
// Step 1 — redirects user to Pinterest login
socialMediaRouter.route('/pinterest/auth').get(initiatePinterestAuth);

// Step 2 — Pinterest redirects back here with ?code=
socialMediaRouter.route('/pinterest/auth/callback').get(handlePinterestCallback);
socialMediaRouter.route('/pinterest/schedule').post(schedulePin);
socialMediaRouter.route('/pinterest/schedule').get(fetchScheduledPin);
socialMediaRouter.route('/pinterest/schedule/:id').delete(deletedScheduledPin);

module.exports = socialMediaRouter;
1 change: 1 addition & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require('./cronjobs/userProfileJobs')();
require('./cronjobs/pullRequestReviewJobs')();
require('./jobs/analyticsAggregation').scheduleDaily();
require('./cronjobs/bidWinnerJobs')();
require('./cronjobs/pinterestScheduleJob')();

// Process pending and stuck emails on startup (only after DB is connected)
mongoose.connection.once('connected', () => {
Expand Down
16 changes: 15 additions & 1 deletion src/startup/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ module.exports = function (app) {
if (req.originalUrl.startsWith('/api/mastodon')) {
return next();
}
const openPaths = ['/api/lb/myWebhooks'];
// const openPaths = ['/api/lb/myWebhooks'];
const openPaths = [
'/paypal/webhook', // existing
'/pinterest/auth', // add this
'/pinterest/auth/callback', // add this
];

if (req.originalUrl === '/') {
res.status(200).send('This is the homepage for rest services');
Expand Down Expand Up @@ -119,6 +124,15 @@ module.exports = function (app) {
}

// Skip auth check for PayPal webhook route
// Skip auth check for Pinterest OAuth routes
if (req.originalUrl.startsWith('/api/social/pinterest/auth')) {
return next();
}

// Skip auth check for PayPal webhook route
if (openPaths.includes(req.path)) {
return next();
}

if (openPaths.includes(req.path)) {
return next(); // Allow PayPal requests through
Expand Down
6 changes: 5 additions & 1 deletion src/startup/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const jobsRouter = require('../routes/jobsRouter');
const laborCostRouter = require('../routes/laborCostRouter');
const jobAnalyticsRouter = require('../routes/jobAnalyticsRouter');
const liveJournalPost = require('../models/liveJournalPost');

const socialMediaRouter = require('../routes/socialMediaRouter');
// const actionItemRouter = require('../routes/actionItemRouter')(actionItem);
// const actionItemRouter = require('../routes/actionItemRouter')(actionItem);
// const actionItemRouter = require('../routes/actionItemRouter')(actionItem);
Expand Down Expand Up @@ -666,4 +666,8 @@ module.exports = function (app) {
app.use('/api/kitchenandinventory/recipes', recipeRouter);

app.use('/api/analytics', analyticsRouter);

//social media routes
app.use('/api/social', socialMediaRouter); // keep existing
app.use('/social', socialMediaRouter); // add this for Vite proxy
};
Loading