diff --git a/client/src/actions/show.js b/client/src/actions/show.js index ee1d0b7e..b4ed7719 100644 --- a/client/src/actions/show.js +++ b/client/src/actions/show.js @@ -2,10 +2,13 @@ import * as actions from '../constants/show_action_types'; import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from '../constants/show_request_types'; // basic request parsing -export function onHandleShowPageUri (params) { +export function onHandleShowPageUri (params, url) { return { type: actions.HANDLE_SHOW_URI, - data: params, + data: { + ...params, + url, + } }; } diff --git a/package-lock.json b/package-lock.json index 87374bf1..d625325d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1259,6 +1259,14 @@ "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", "dev": true }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "1.3.7" + } + }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", @@ -2690,6 +2698,16 @@ "shimmer": "1.2.0" } }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "1.7.6", + "emitter-listener": "1.1.1", + "semver": "5.5.1" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3640,6 +3658,14 @@ "minimalistic-crypto-utils": "1.0.1" } }, + "emitter-listener": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.1.tgz", + "integrity": "sha1-6Lu+gkS8jg0LTvcc0UKUx/JBx+w=", + "requires": { + "shimmer": "1.2.0" + } + }, "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", @@ -4365,6 +4391,14 @@ "promise": "7.3.1" } }, + "express-http-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/express-http-context/-/express-http-context-1.1.0.tgz", + "integrity": "sha512-LS47HseitRIxzBHDEQrlVwZkEkMaViM+nhRCrlWYxPNIu7W8KUZyNUOxiD93OghHesl7y+DhBYuz3yfaNHDvVA==", + "requires": { + "cls-hooked": "4.2.2" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -12893,6 +12927,11 @@ "tweetnacl": "0.14.5" } }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", diff --git a/package.json b/package.json index 3d1cf596..23f23980 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "cookie-session": "^2.0.0-beta.3", "express": "^4.15.2", "express-handlebars": "^3.0.0", + "express-http-context": "^1.1.0", "get-video-dimensions": "^1.0.0", "helmet": "^3.13.0", "image-size": "^0.6.3", diff --git a/server/index.js b/server/index.js index 85222a71..097fd9ce 100644 --- a/server/index.js +++ b/server/index.js @@ -7,6 +7,7 @@ const cookieSession = require('cookie-session'); const http = require('http'); const logger = require('winston'); const Path = require('path'); +const httpContext = require('express-http-context'); // load local modules const db = require('./models'); @@ -26,6 +27,14 @@ const { }, } = require('@config/siteConfig'); +function setRouteDataInContextMiddleware(routePath, routeData) { + return function (req, res, next) { + httpContext.set('routePath', routePath); + httpContext.set('routeData', routeData); + next(); + }; +} + function Server () { this.initialize = () => { // configure logging @@ -43,6 +52,9 @@ function Server () { // set HTTP headers to protect against well-known web vulnerabilties app.use(helmet()); + // Support per-request http-context + app.use(httpContext.middleware); + // 'express.static' to serve static files from public directory const publicPath = Path.resolve(process.cwd(), 'public'); app.use(express.static(publicPath)); @@ -78,11 +90,15 @@ function Server () { app.set('view engine', 'handlebars'); // set the routes on the app - require('./routes/auth/index')(app); - require('./routes/api/index')(app); - require('./routes/pages/index')(app); - require('./routes/assets/index')(app); - require('./routes/fallback/index')(app); + const routes = require('./routes'); + + Object.keys(routes).map((routePath) => { + let routeData = routes[routePath]; + let routeMethod = routeData.hasOwnProperty('method') ? routeData.method : 'get'; + let controllers = Array.isArray(routeData.controller) ? routeData.controller : [routeData.controller]; + + app[routeMethod](routePath, setRouteDataInContextMiddleware(routePath, routeData), ...controllers); + }); this.app = app; }; diff --git a/server/render/build/handleShowRender.js b/server/render/build/handleShowRender.js index 98f433ca..2236d1bb 100644 --- a/server/render/build/handleShowRender.js +++ b/server/render/build/handleShowRender.js @@ -18,6 +18,8 @@ var _effects = require("redux-saga/effects"); var _reactHelmet = _interopRequireDefault(require("react-helmet")); +var httpContext = _interopRequireWildcard(require("express-http-context")); + var _reducers = _interopRequireDefault(require("@reducers")); var _GAListener = _interopRequireDefault(require("@components/GAListener")); @@ -28,6 +30,8 @@ var _sagas = _interopRequireDefault(require("@sagas")); var _actions = _interopRequireDefault(require("@actions")); +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var returnSagaWithParams = function returnSagaWithParams(saga, params) { @@ -52,19 +56,17 @@ var returnSagaWithParams = function returnSagaWithParams(saga, params) { }; module.exports = function (req, res) { - var context = {}; // create and apply middleware + var context = {}; - var sagaMiddleware = (0, _reduxSaga.default)(); - var middleware = (0, _redux.applyMiddleware)(sagaMiddleware); // create a new Redux store instance + var _httpContext$get = httpContext.get('routeData'), + _httpContext$get$acti = _httpContext$get.action, + action = _httpContext$get$acti === void 0 ? false : _httpContext$get$acti, + _httpContext$get$saga = _httpContext$get.saga, + saga = _httpContext$get$saga === void 0 ? false : _httpContext$get$saga; - var store = (0, _redux.createStore)(_reducers.default, middleware); // create an action to handle the given url, - // and create a the saga needed to handle that action + var runSaga = action !== false && saga !== false; - var action = _actions.default.onHandleShowPageUri(req.params); - - var saga = returnSagaWithParams(_sagas.default.handleShowPageUri, action); // run the saga middleware with the saga call - - sagaMiddleware.run(saga).done.then(function () { + var renderPage = function renderPage(store) { // render component to a string var html = (0, _server.renderToString)(_react.default.createElement(_reactRedux.Provider, { store: store @@ -84,5 +86,27 @@ module.exports = function (req, res) { var preloadedState = store.getState(); // send the rendered page back to the client res.send((0, _renderFullPage.default)(helmet, html, preloadedState)); - }); + }; + + console.log(httpContext.get('routePath'), runSaga, httpContext.get('routeData'), action, saga); + + if (runSaga) { + // create and apply middleware + var sagaMiddleware = (0, _reduxSaga.default)(); + var middleware = (0, _redux.applyMiddleware)(sagaMiddleware); // create a new Redux store instance + + var store = (0, _redux.createStore)(_reducers.default, middleware); // create an action to handle the given url, + // and create a the saga needed to handle that action + + var boundAction = action(req.params, req.url); + var boundSaga = returnSagaWithParams(saga, boundAction); // run the saga middleware with the saga call + + sagaMiddleware.run(boundSaga).done.then(function () { + return renderPage(store); + }); + } else { + var _store = (0, _redux.createStore)(_reducers.default); + + renderPage(_store); + } }; \ No newline at end of file diff --git a/server/render/src/handleShowRender.jsx b/server/render/src/handleShowRender.jsx index 2a8809f2..f96d8015 100644 --- a/server/render/src/handleShowRender.jsx +++ b/server/render/src/handleShowRender.jsx @@ -7,6 +7,7 @@ import renderFullPage from '../renderFullPage'; import createSagaMiddleware from 'redux-saga'; import { call } from 'redux-saga/effects'; import Helmet from 'react-helmet'; +import * as httpContext from 'express-http-context'; import Reducers from '@reducers'; import GAListener from '@components/GAListener'; @@ -23,46 +24,60 @@ const returnSagaWithParams = (saga, params) => { module.exports = (req, res) => { let context = {}; - // create and apply middleware - const sagaMiddleware = createSagaMiddleware(); - const middleware = applyMiddleware(sagaMiddleware); + const { + action = false, + saga = false, + } = httpContext.get('routeData'); - // create a new Redux store instance - const store = createStore(Reducers, middleware); + const runSaga = (action !== false && saga !== false); - // create an action to handle the given url, - // and create a the saga needed to handle that action - const action = Actions.onHandleShowPageUri(req.params); - const saga = returnSagaWithParams(Sagas.handleShowPageUri, action); + const renderPage = (store) => { + // render component to a string + const html = renderToString( + + + + + + + + ); - // run the saga middleware with the saga call - sagaMiddleware - .run(saga) - .done - .then(() => { - // render component to a string - const html = renderToString( - - - - - - - - ); + // get head tags from helmet + const helmet = Helmet.renderStatic(); - // get head tags from helmet - const helmet = Helmet.renderStatic(); + // check for a redirect + if (context.url) { + return res.redirect(301, context.url); + } - // check for a redirect - if (context.url) { - return res.redirect(301, context.url); - } + // get the initial state from our Redux store + const preloadedState = store.getState(); - // get the initial state from our Redux store - const preloadedState = store.getState(); + // send the rendered page back to the client + res.send(renderFullPage(helmet, html, preloadedState)); + }; - // send the rendered page back to the client - res.send(renderFullPage(helmet, html, preloadedState)); - }); + if (runSaga) { + // create and apply middleware + const sagaMiddleware = createSagaMiddleware(); + const middleware = applyMiddleware(sagaMiddleware); + + // create a new Redux store instance + const store = createStore(Reducers, middleware); + + // create an action to handle the given url, + // and create a the saga needed to handle that action + const boundAction = action(req.params, req.url); + const boundSaga = returnSagaWithParams(saga, boundAction); + + // run the saga middleware with the saga call + sagaMiddleware + .run(boundSaga) + .done + .then(() => renderPage(store) ); + } else { + const store = createStore(Reducers); + renderPage(store); + } }; diff --git a/server/routes/api/index.js b/server/routes/api/index.js index b5a0c466..f4220179 100644 --- a/server/routes/api/index.js +++ b/server/routes/api/index.js @@ -21,31 +21,31 @@ const getTorList = require('../../controllers/api/tor'); const getBlockedList = require('../../controllers/api/blocked'); const getOEmbedData = require('../../controllers/api/oEmbed'); -module.exports = (app) => { +module.exports = { // channel routes - app.get('/api/channel/availability/:name', torCheckMiddleware, channelAvailability); - app.get('/api/channel/short-id/:longId/:name', torCheckMiddleware, channelShortId); - app.get('/api/channel/data/:channelName/:channelClaimId', torCheckMiddleware, channelData); - app.get('/api/channel/claims/:channelName/:channelClaimId/:page', torCheckMiddleware, channelClaims); + '/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] }, + '/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] }, + '/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] }, + '/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] }, // claim routes - app.get('/api/claim/availability/:name', torCheckMiddleware, claimAvailability); - app.get('/api/claim/data/:claimName/:claimId', torCheckMiddleware, claimData); - app.get('/api/claim/get/:name/:claimId', torCheckMiddleware, claimGet); - app.get('/api/claim/list/:name', torCheckMiddleware, claimList); - app.post('/api/claim/long-id', torCheckMiddleware, claimLongId); // note: should be a 'get' - app.post('/api/claim/publish', torCheckMiddleware, multipartMiddleware, claimPublish); - app.get('/api/claim/resolve/:name/:claimId', torCheckMiddleware, claimResolve); - app.get('/api/claim/short-id/:longId/:name', torCheckMiddleware, claimShortId); + '/api/claim/availability/:name': { controller: [ torCheckMiddleware, claimAvailability ] }, + '/api/claim/data/:claimName/:claimId': { controller: [ torCheckMiddleware, claimData ] }, + '/api/claim/get/:name/:claimId': { controller: [ torCheckMiddleware, claimGet ] }, + '/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] }, + '/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get' + '/api/claim/publish': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimPublish ] }, + '/api/claim/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] }, + '/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] }, // file routes - app.get('/api/file/availability/:name/:claimId', torCheckMiddleware, fileAvailability); + '/api/file/availability/:name/:claimId': { controller: [ torCheckMiddleware, fileAvailability ] }, // user routes - app.put('/api/user/password/', torCheckMiddleware, userPassword); + '/api/user/password/': { method: 'put', controller: [ torCheckMiddleware, userPassword ] }, // configs - app.get('/api/config/site/publishing', torCheckMiddleware, publishingConfig); + '/api/config/site/publishing': { controller: [ torCheckMiddleware, publishingConfig ] }, // tor - app.get('/api/tor', torCheckMiddleware, getTorList); + '/api/tor': { controller: [ torCheckMiddleware, getTorList ] }, // blocked - app.get('/api/blocked', torCheckMiddleware, getBlockedList); + '/api/blocked': { controller: [ torCheckMiddleware, getBlockedList ] }, // open embed - app.get('/api/oembed', torCheckMiddleware, getOEmbedData); + '/api/oembed': { controller: [ torCheckMiddleware, getOEmbedData ] }, }; diff --git a/server/routes/assets/index.js b/server/routes/assets/index.js index ef70ea3f..411a4f64 100644 --- a/server/routes/assets/index.js +++ b/server/routes/assets/index.js @@ -1,7 +1,11 @@ const serveByClaim = require('../../controllers/assets/serveByClaim'); const serveByIdentifierAndClaim = require('../../controllers/assets/serveByIdentifierAndClaim'); -module.exports = (app) => { - app.get('/:identifier/:claim', serveByIdentifierAndClaim); - app.get('/:claim', serveByClaim); +// TODO: Adjust build & sources to use import/export everywhere +const Actions = require('@actions').default; +const Sagas = require('@sagas').default; + +module.exports = { + '/:identifier/:claim': { controller: serveByIdentifierAndClaim, action: Actions.onHandleShowPageUri, saga: Sagas.handleShowPageUri }, + '/:claim': { controller: serveByClaim, action: Actions.onHandleShowPageUri, saga: Sagas.handleShowPageUri }, }; diff --git a/server/routes/auth/index.js b/server/routes/auth/index.js index 9754f5b2..8bdbe1b6 100644 --- a/server/routes/auth/index.js +++ b/server/routes/auth/index.js @@ -4,9 +4,9 @@ const handleLoginRequest = require('../../controllers/auth/login'); const handleLogoutRequest = require('../../controllers/auth/logout'); const handleUserRequest = require('../../controllers/auth/user'); -module.exports = (app) => { - app.post('/signup', speechPassport.authenticate('local-signup'), handleSignupRequest); - app.post('/login', handleLoginRequest); - app.get('/logout', handleLogoutRequest); - app.get('/user', handleUserRequest); +module.exports = { + '/signup': { method: 'post', controller: [ speechPassport.authenticate('local-signup'), handleSignupRequest ] }, + '/login': { method: 'post', controller: handleLoginRequest }, + '/logout': { controller: handleLogoutRequest }, + '/user': { controller: handleUserRequest }, }; diff --git a/server/routes/fallback/index.js b/server/routes/fallback/index.js index cc453b81..d46b3ae8 100644 --- a/server/routes/fallback/index.js +++ b/server/routes/fallback/index.js @@ -1,5 +1,5 @@ const handlePageRequest = require('../../controllers/pages/sendReactApp'); -module.exports = (app) => { - app.get('*', handlePageRequest); +module.exports = { + '*': { controller: handlePageRequest, action: 'fallback' }, }; diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 00000000..73756955 --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,7 @@ +module.exports = { + ...require('./pages'), + ...require('./api'), + ...require('./assets'), + ...require('./auth'), + ...require('./fallback'), +}; diff --git a/server/routes/pages/index.js b/server/routes/pages/index.js index 2dff606b..0d097ddf 100644 --- a/server/routes/pages/index.js +++ b/server/routes/pages/index.js @@ -2,13 +2,13 @@ const handlePageRequest = require('../../controllers/pages/sendReactApp'); const handleVideoEmbedRequest = require('../../controllers/pages/sendVideoEmbedPage'); const redirect = require('../../controllers/utils/redirect'); -module.exports = (app) => { - app.get('/', handlePageRequest); - app.get('/login', handlePageRequest); - app.get('/about', handlePageRequest); - app.get('/trending', redirect('/popular')); - app.get('/popular', handlePageRequest); - app.get('/new', handlePageRequest); - app.get('/multisite', handlePageRequest); - app.get('/video-embed/:name/:claimId', handleVideoEmbedRequest); // for twitter +module.exports = { + '/': { controller: handlePageRequest }, + '/login': { controller: handlePageRequest }, + '/about': { controller: handlePageRequest }, + '/trending': { controller: redirect('/popular') }, + '/popular': { controller: handlePageRequest }, + '/new': { controller: handlePageRequest }, + '/multisite': { controller: handlePageRequest }, + '/video-embed/:name/:claimId': { controller: handleVideoEmbedRequest }, // for twitter };