Feat: News in the client app (#34392)
This commit is contained in:
@ -1,214 +1,259 @@
|
||||
// import React from 'react';
|
||||
// import { renderToString } from 'react-dom/server';
|
||||
// // import { StaticRouter } from 'react-router-dom';
|
||||
// import { has } from 'lodash';
|
||||
import { has, pick, isEmpty } from 'lodash';
|
||||
import debug from 'debug';
|
||||
import { reportError } from '../middlewares/error-reporter';
|
||||
|
||||
// import NewsApp from '../../news/NewsApp';
|
||||
|
||||
const routerLog = debug('fcc:boot:news:router');
|
||||
const apiLog = debug('fcc:boot:news:api');
|
||||
const log = debug('fcc:boot:news');
|
||||
|
||||
export default function newsBoot(app) {
|
||||
// const router = app.loopback.Router();
|
||||
// const api = app.loopback.Router();
|
||||
const api = app.loopback.Router();
|
||||
|
||||
// router.get('/n', (req, res) => res.redirect('/news'));
|
||||
// router.get('/n/:shortId', createShortLinkHandler(app));
|
||||
api.get('/n/:shortId', createShortLinkHandler(app));
|
||||
|
||||
// router.get('/news', serveNewsApp);
|
||||
// router.get('/news/*', serveNewsApp);
|
||||
api.post('/p', createPopularityHandler(app));
|
||||
|
||||
// api.post('/p', createPopularityHandler(app));
|
||||
|
||||
// app.use(api);
|
||||
// app.use(router);
|
||||
app.use('/internal', api);
|
||||
}
|
||||
|
||||
// function serveNewsApp(req, res) {
|
||||
// const context = {};
|
||||
// const markup = renderToString(
|
||||
// <StaticRouter basename='/news' context={context} location={req.url}>
|
||||
// <NewsApp />
|
||||
// </StaticRouter>
|
||||
// );
|
||||
// if (context.url) {
|
||||
// routerLog('redirect found in `renderToString`');
|
||||
// // 'client-side' routing hit on a redirect
|
||||
// return res.redirect(context.url);
|
||||
// }
|
||||
// routerLog('news markup sending');
|
||||
// return res.render('layout-news', { title: 'News | freeCodeCamp', markup });
|
||||
// }
|
||||
function createShortLinkHandler(app) {
|
||||
const { Article } = app.models;
|
||||
|
||||
// function createShortLinkHandler(app) {
|
||||
// const { Article } = app.models;
|
||||
const referralHandler = createReferralHandler(app);
|
||||
|
||||
// const referralHandler = createRerralHandler(app);
|
||||
return function shortLinkHandler(req, res, next) {
|
||||
const { query, user } = req;
|
||||
const { shortId } = req.params;
|
||||
|
||||
// return function shortLinkHandler(req, res, next) {
|
||||
// const { query, user } = req;
|
||||
// const { shortId } = req.params;
|
||||
// We manually report the error here as it should not affect this request
|
||||
referralHandler(query, shortId, !!user).catch(err => reportError(err));
|
||||
|
||||
// referralHandler(query, shortId, !!user);
|
||||
if (!shortId) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
return Article.findOne(
|
||||
{
|
||||
where: {
|
||||
or: [{ shortId }, { slugPart: shortId }]
|
||||
}
|
||||
},
|
||||
(err, article) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
}
|
||||
if (!article) {
|
||||
return res.status(404).send('Could not find article by shortId');
|
||||
}
|
||||
const {
|
||||
slugPart,
|
||||
shortId,
|
||||
author: { username }
|
||||
} = article;
|
||||
const slug = `/news/${username}/${slugPart}--${shortId}`;
|
||||
const articleData = {
|
||||
...pick(article, [
|
||||
'author',
|
||||
'renderableContent',
|
||||
'firstPublishedDate',
|
||||
'viewCount',
|
||||
'title',
|
||||
'featureImage',
|
||||
'slugPart',
|
||||
'shortId',
|
||||
'meta'
|
||||
]),
|
||||
slug
|
||||
};
|
||||
return res.json(articleData);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// routerLog(req.origin);
|
||||
// routerLog(query.refsource);
|
||||
// if (!shortId) {
|
||||
// return res.redirect('/news');
|
||||
// }
|
||||
// routerLog('shortId', shortId);
|
||||
// return Article.findOne(
|
||||
// {
|
||||
// where: {
|
||||
// or: [{ shortId }, { slugPart: shortId }]
|
||||
// }
|
||||
// },
|
||||
// (err, article) => {
|
||||
// if (err) {
|
||||
// next(err);
|
||||
// }
|
||||
// if (!article) {
|
||||
// return res.redirect('/news');
|
||||
// }
|
||||
// const {
|
||||
// slugPart,
|
||||
// shortId,
|
||||
// author: { username }
|
||||
// } = article;
|
||||
// const slug = `/news/${username}/${slugPart}--${shortId}`;
|
||||
// return res.redirect(slug);
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
function createPopularityHandler(app) {
|
||||
const { Article, Popularity } = app.models;
|
||||
|
||||
// function createPopularityHandler(app) {
|
||||
// const { Article, Popularity } = app.models;
|
||||
function findArticleByShortId(shortId) {
|
||||
return new Promise((resolve, reject) =>
|
||||
Article.findOne({ where: { shortId } }, (err, article) => {
|
||||
if (err) {
|
||||
log('Error returned from Article.findOne(shortId)');
|
||||
return reject(err);
|
||||
}
|
||||
log('article found');
|
||||
return resolve(article);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// return function handlePopularityStats(req, res, next) {
|
||||
// const { body, user } = req;
|
||||
function findPopularityByShortId(shortId) {
|
||||
return new Promise((resolve, reject) =>
|
||||
Popularity.findOne(
|
||||
{ where: { articleId: shortId } },
|
||||
(err, popularity) => {
|
||||
if (err) {
|
||||
log('Error returned from Popularity.findOne(shortId)');
|
||||
return reject(err);
|
||||
}
|
||||
log('popularity found');
|
||||
return resolve(popularity);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// if (
|
||||
// !has(body, 'event') ||
|
||||
// !has(body, 'timestamp') ||
|
||||
// !has(body, 'shortId')
|
||||
// ) {
|
||||
// console.warn('Popularity event recieved from client is malformed');
|
||||
// console.log(JSON.stringify(body, null, 2));
|
||||
// // sending 200 because the client shouldn't care for this
|
||||
// return res.sendStatus(200);
|
||||
// }
|
||||
// res.sendStatus(200);
|
||||
// const { shortId } = body;
|
||||
// apiLog('shortId', shortId);
|
||||
// const populartiyUpdate = {
|
||||
// ...body,
|
||||
// byAuthenticatedUser: !!user
|
||||
// };
|
||||
// Popularity.findOne({ where: { articleId: shortId } }, (err, popularity) => {
|
||||
// if (err) {
|
||||
// apiLog(err);
|
||||
// return next(err);
|
||||
// }
|
||||
// if (popularity) {
|
||||
// return popularity.updateAttribute(
|
||||
// 'events',
|
||||
// [populartiyUpdate, ...popularity.events],
|
||||
// err => {
|
||||
// if (err) {
|
||||
// apiLog(err);
|
||||
// return next(err);
|
||||
// }
|
||||
// return apiLog('poplarity updated');
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// return Popularity.create(
|
||||
// {
|
||||
// events: [populartiyUpdate],
|
||||
// articleId: shortId
|
||||
// },
|
||||
// err => {
|
||||
// if (err) {
|
||||
// apiLog(err);
|
||||
// return next(err);
|
||||
// }
|
||||
// return apiLog('poulartiy created');
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// return body.event === 'view'
|
||||
// ? Article.findOne({ where: { shortId } }, (err, article) => {
|
||||
// if (err) {
|
||||
// apiLog(err);
|
||||
// next(err);
|
||||
// }
|
||||
// return article.updateAttributes(
|
||||
// { viewCount: article.viewCount + 1 },
|
||||
// err => {
|
||||
// if (err) {
|
||||
// apiLog(err);
|
||||
// return next(err);
|
||||
// }
|
||||
// return apiLog('article views updated');
|
||||
// }
|
||||
// );
|
||||
// })
|
||||
// : null;
|
||||
// };
|
||||
// }
|
||||
function createPopularity(popularityUpdate, shortId) {
|
||||
return new Promise((resolve, reject) =>
|
||||
Popularity.create(
|
||||
{
|
||||
events: [popularityUpdate],
|
||||
articleId: shortId
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
log('poulartiy created');
|
||||
return resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// function createRerralHandler(app) {
|
||||
// const { Popularity } = app.models;
|
||||
function updatePopularity(popularity, popularityUpdate) {
|
||||
return new Promise((resolve, reject) =>
|
||||
popularity.updateAttribute(
|
||||
'events',
|
||||
[popularityUpdate, ...popularity.events],
|
||||
err => {
|
||||
if (err) {
|
||||
log('Error returned from popularity.updateAttribute()');
|
||||
return reject(err);
|
||||
}
|
||||
log('poplarity updated');
|
||||
return resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// return function referralHandler(query, shortId, byAuthenticatedUser) {
|
||||
// if (!query.refsource) {
|
||||
// return null;
|
||||
// }
|
||||
// const eventUpdate = {
|
||||
// event: `referral - ${query.refsource}`,
|
||||
// timestamp: new Date(Date.now()),
|
||||
// byAuthenticatedUser
|
||||
// };
|
||||
// return Popularity.findOne(
|
||||
// { where: { articleId: shortId } },
|
||||
// (err, popularity) => {
|
||||
// if (err) {
|
||||
// console.error(
|
||||
// 'Failed finding a `Popularity` in a referral handler',
|
||||
// err
|
||||
// );
|
||||
// return null;
|
||||
// }
|
||||
function incrementArticleViews(article) {
|
||||
return new Promise((resolve, reject) =>
|
||||
article.updateAttributes({ $inc: { viewCount: 1 } }, err => {
|
||||
if (err) {
|
||||
log(err);
|
||||
return reject(err);
|
||||
}
|
||||
log('article views updated');
|
||||
return resolve();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// if (popularity) {
|
||||
// return popularity.updateAttribute(
|
||||
// 'events',
|
||||
// [eventUpdate, ...popularity.events],
|
||||
// err => {
|
||||
// if (err) {
|
||||
// console.error(
|
||||
// 'Failed in updating the `events` attribute of a `popularity`',
|
||||
// err
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// return Popularity.create(
|
||||
// {
|
||||
// events: [eventUpdate],
|
||||
// articleId: shortId
|
||||
// },
|
||||
// err => {
|
||||
// if (err) {
|
||||
// return console.error('Failed creating a new `Popularity`', err);
|
||||
// }
|
||||
// return apiLog('poulartiy created');
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
return async function handlePopularityStats(req, res, next) {
|
||||
const { body, user } = req;
|
||||
|
||||
if (
|
||||
!has(body, 'event') ||
|
||||
!has(body, 'timestamp') ||
|
||||
!has(body, 'shortId')
|
||||
) {
|
||||
console.warn('Popularity event recieved from client is malformed');
|
||||
console.log(JSON.stringify(body, null, 2));
|
||||
// sending 200 because the client shouldn't care for this
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
const { shortId } = body;
|
||||
log('shortId', shortId);
|
||||
|
||||
const articlePromise = findArticleByShortId(shortId);
|
||||
const popularityPromise = findPopularityByShortId(shortId);
|
||||
|
||||
const [article, popularity] = await Promise.all([
|
||||
articlePromise,
|
||||
popularityPromise
|
||||
]).catch(err => {
|
||||
log('find catch');
|
||||
return next(err);
|
||||
});
|
||||
if (!article || isEmpty(article)) {
|
||||
log('No article found to handle the populartity update');
|
||||
// sending 200 because the client shouldn't care for this
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
const populartiyUpdate = {
|
||||
...body,
|
||||
byAuthenticatedUser: !!user
|
||||
};
|
||||
|
||||
const populartiyUpdateOrCreatePromise = isEmpty(popularity)
|
||||
? createPopularity(populartiyUpdate, shortId)
|
||||
: updatePopularity(popularity, populartiyUpdate);
|
||||
const maybeUpdateArticlePromise =
|
||||
body.event === 'view' ? incrementArticleViews(article) : null;
|
||||
return Promise.all([
|
||||
populartiyUpdateOrCreatePromise,
|
||||
maybeUpdateArticlePromise
|
||||
])
|
||||
.then(() => res.sendStatus(200))
|
||||
.catch(err => {
|
||||
log('updates catch');
|
||||
return next(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createReferralHandler(app) {
|
||||
const { Popularity } = app.models;
|
||||
|
||||
return function referralHandler(query, shortId, byAuthenticatedUser) {
|
||||
if (!query.refsource) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const eventUpdate = {
|
||||
event: `referral - ${query.refsource}`,
|
||||
timestamp: new Date(Date.now()),
|
||||
byAuthenticatedUser
|
||||
};
|
||||
return new Promise((resolve, reject) =>
|
||||
Popularity.findOne(
|
||||
{ where: { articleId: shortId } },
|
||||
(err, popularity) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
'Failed finding a `Popularity` in a referral handler',
|
||||
err
|
||||
);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (popularity) {
|
||||
return popularity.updateAttribute(
|
||||
'events',
|
||||
[eventUpdate, ...popularity.events],
|
||||
err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
log('populartiy updated');
|
||||
return resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
return Popularity.create(
|
||||
{
|
||||
events: [eventUpdate],
|
||||
articleId: shortId
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
log('poulartiy created');
|
||||
return resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ module.exports = {
|
||||
connector: 'mongodb',
|
||||
connectionTimeout: 10000,
|
||||
url: secrets.db,
|
||||
useNewUrlParser: true
|
||||
useNewUrlParser: true,
|
||||
allowExtendedOperators: true
|
||||
},
|
||||
mail: {
|
||||
connector: 'mail',
|
||||
|
@ -8,47 +8,10 @@ import { unwrapHandledError } from '../utils/create-handled-error.js';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// const toString = Object.prototype.toString;
|
||||
// is full error or just trace
|
||||
// _.toString(new Error('foo')) => "Error: foo
|
||||
// Object.prototype.toString.call(new Error('foo')) => "[object Error]"
|
||||
// const isInspect = val => !val.stack && _.toString(val) === toString.call(val);
|
||||
// const stringifyErr = val => {
|
||||
// if (val.stack) {
|
||||
// return String(val.stack);
|
||||
// }
|
||||
|
||||
// const str = String(val);
|
||||
|
||||
// return isInspect(val) ?
|
||||
// inspect(val) :
|
||||
// str;
|
||||
// };
|
||||
|
||||
// const createStackHtml = _.flow(
|
||||
// _.cond([
|
||||
// [isInspect, err => [err]],
|
||||
// // may be stack or just err.msg
|
||||
// [_.stubTrue, _.flow(stringifyErr, _.split('\n'), _.tail) ]
|
||||
// ]),
|
||||
// _.map(_.escape),
|
||||
// _.map(line => `<li>${line}</lin>`),
|
||||
// _.join('')
|
||||
// );
|
||||
|
||||
// const createErrorTitle = _.cond([
|
||||
// [
|
||||
// _.negate(isInspect),
|
||||
// _.flow(stringifyErr, _.split('\n'), _.head, _.defaultTo('Error'))
|
||||
// ],
|
||||
// [_.stubTrue, _.constant('Error')]
|
||||
// ]);
|
||||
|
||||
export default function prodErrorHandler() {
|
||||
// error handling in production.
|
||||
// disabling eslint due to express parity rules for error handlers
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
return function(err, req, res, next) {
|
||||
// eslint-disable-line
|
||||
const handled = unwrapHandledError(err);
|
||||
// respect handled error status
|
||||
let status = handled.status || err.status || res.statusCode;
|
||||
|
@ -5,20 +5,30 @@ import {
|
||||
unwrapHandledError
|
||||
} from '../utils/create-handled-error.js';
|
||||
|
||||
const { ROLLBAR_APP_ID } = process.env;
|
||||
import { rollbar } from '../../../config/secrets';
|
||||
|
||||
const rollbar = new Rollbar(ROLLBAR_APP_ID);
|
||||
const { appId } = rollbar;
|
||||
const reporter = new Rollbar(appId);
|
||||
const log = debug('fcc:middlewares:error-reporter');
|
||||
|
||||
const errTemplate = ({message, ...restError}, req) => `
|
||||
const errTemplate = (error, req) => {
|
||||
const { message, stack } = error;
|
||||
return `
|
||||
Time: ${new Date(Date.now()).toISOString()}
|
||||
Error: ${message}
|
||||
Is authenticated user: ${!!req.user}
|
||||
Route: ${JSON.stringify(req.route, null, 2)}
|
||||
Stack: ${stack}
|
||||
|
||||
${JSON.stringify(restError, null, 2)}
|
||||
// raw
|
||||
${JSON.stringify(error, null, 2)}
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
export function reportError(err) {
|
||||
return reporter.error(err.message, err);
|
||||
}
|
||||
|
||||
export default function errrorReporter() {
|
||||
if (process.env.NODE_ENV !== 'production' && process.env.ERROR_REPORTER) {
|
||||
@ -44,6 +54,7 @@ export default function errrorReporter() {
|
||||
// logging the error provides us with more information,
|
||||
// i.e isAuthenticatedUser, req.route
|
||||
console.error(errTemplate(err, req));
|
||||
return rollbar.error(err.message, err);
|
||||
reportError(err);
|
||||
return next(err);
|
||||
};
|
||||
}
|
||||
|
@ -6,9 +6,17 @@ import { homeLocation } from '../../../config/env';
|
||||
|
||||
import { wrapHandledError } from '../utils/create-handled-error';
|
||||
|
||||
// We need to tunnel through a proxy path set up within
|
||||
// the gatsby app, at this time, that path is /internal
|
||||
const whiteListRE = new RegExp([
|
||||
'^/internal/n/',
|
||||
'^/internal/p\??'
|
||||
].join('|'));
|
||||
|
||||
|
||||
export default () => function authorizeByJWT(req, res, next) {
|
||||
const path = req.path.split('/')[1];
|
||||
if (/^external$|^internal$/.test(path)) {
|
||||
if (/^external$|^internal$/.test(path) && !whiteListRE.test(req.path)) {
|
||||
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] ||
|
||||
req.cookie && req.cookie['jwt_access_token'];
|
||||
if (!cookie) {
|
||||
|
Reference in New Issue
Block a user