${text}`;
- res.status(200).send(text);
-}));
-
-//Download brew source page
-app.get('/download/:id', asyncHandler(async (req, res)=>{
- const brew = await getBrewFromId(req.params.id, 'raw');
- const prefix = 'HB - ';
-
- let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
- if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
- res.set({
- 'Cache-Control' : 'no-cache',
- 'Content-Type' : 'text/plain',
- 'Content-Disposition' : `attachment; filename="${fileName}.txt"`
+DB.connect(config).then(()=>{
+ // Ensure that we have successfully connected to the database
+ // before launching server
+ const PORT = process.env.PORT || config.get('web_port') || 8000;
+ server.app.listen(PORT, ()=>{
+ console.log(`server on port: ${PORT}`);
});
- res.status(200).send(brew.text);
-}));
-
-//User Page
-app.get('/user/:username', async (req, res, next)=>{
- const ownAccount = req.account && (req.account.username == req.params.username);
-
- let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
- .catch((err)=>{
- console.log(err);
- });
-
- if(ownAccount && req?.account?.googleId){
- const googleBrews = await GoogleActions.listGoogleBrews(req, res)
- .catch((err)=>{
- console.error(err);
- });
-
- if(googleBrews)
- brews = _.concat(brews, googleBrews);
- }
-
- req.brews = _.map(brews, (brew)=>{
- return sanitizeBrew(brew, !ownAccount);
- });
-
- return next();
-});
-
-//Edit Page
-app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
- res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
- const brew = await getBrewFromId(req.params.id, 'edit');
- req.brew = brew;
- return next();
-}));
-
-//New Page
-app.get('/new/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
- brew.title = `CLONE - ${brew.title}`;
- req.brew = brew;
- return next();
-}));
-
-//Share Page
-app.get('/share/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
-
- if(req.params.id.length > 12) {
- const googleId = req.params.id.slice(0, -12);
- const shareId = req.params.id.slice(-12);
- await GoogleActions.increaseView(googleId, shareId, 'share', brew)
- .catch((err)=>{next(err);});
- } else {
- await HomebrewModel.increaseView({ shareId: brew.shareId });
- }
-
- req.brew = brew;
- return next();
-}));
-
-//Print Page
-app.get('/print/:id', asyncHandler(async (req, res, next)=>{
- const brew = await getBrewFromId(req.params.id, 'share');
- req.brew = brew;
- return next();
-}));
-
-//Render the page
-const templateFn = require('./client/template.js');
-app.use((req, res)=>{
- const props = {
- version : require('./package.json').version,
- url : req.originalUrl,
- brew : req.brew,
- brews : req.brews,
- googleBrews : req.googleBrews,
- account : req.account,
- enable_v3 : config.get('enable_v3')
- };
- templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
- .then((page)=>{ res.send(page); })
- .catch((err)=>{
- console.log(err);
- return res.sendStatus(500);
- });
-});
-
-//v=====----- Error-Handling Middleware -----=====v//
-//Format Errors so all fields will be sent
-const replaceErrors = (key, value)=>{
- if(value instanceof Error) {
- const error = {};
- Object.getOwnPropertyNames(value).forEach(function (key) {
- error[key] = value[key];
- });
- return error;
- }
- return value;
-};
-
-const getPureError = (error)=>{
- return JSON.parse(JSON.stringify(error, replaceErrors));
-};
-
-app.use((err, req, res, next)=>{
- const status = err.status || 500;
- console.error(err);
- res.status(status).send(getPureError(err));
-});
-//^=====--------------------------------------=====^//
-
-const PORT = process.env.PORT || config.get('web_port') || 8000;
-app.listen(PORT, ()=>{
- console.log(`server on port:${PORT}`);
});
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 000000000..c36533b9b
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,298 @@
+/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
+const _ = require('lodash');
+const jwt = require('jwt-simple');
+const express = require('express');
+const yaml = require('js-yaml');
+const app = express();
+
+const homebrewApi = require('./homebrew.api.js');
+const GoogleActions = require('./googleActions.js');
+const serveCompressedStaticAssets = require('./static-assets.mv.js');
+const sanitizeFilename = require('sanitize-filename');
+const asyncHandler = require('express-async-handler');
+
+const brewAccessTypes = ['edit', 'share', 'raw'];
+
+//Get the brew object from the HB database or Google Drive
+const getBrewFromId = asyncHandler(async (id, accessType)=>{
+ if(!brewAccessTypes.includes(accessType))
+ throw ('Invalid Access Type when getting brew');
+ let brew;
+ if(id.length > 12) {
+ const googleId = id.slice(0, -12);
+ id = id.slice(-12);
+ brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
+ } else {
+ brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
+ brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
+ }
+
+ brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
+ //Split brew.text into text and style
+ //unless the Access Type is RAW, in which case return immediately
+ if(accessType == 'raw') {
+ return brew;
+ }
+ splitTextStyleAndMetadata(brew);
+ return brew;
+});
+
+const sanitizeBrew = (brew, full=false)=>{
+ delete brew._id;
+ delete brew.__v;
+ if(full){
+ delete brew.editId;
+ }
+ return brew;
+};
+
+const splitTextStyleAndMetadata = (brew)=>{
+ brew.text = brew.text.replaceAll('\r\n', '\n');
+ if(brew.text.startsWith('```metadata')) {
+ const index = brew.text.indexOf('```\n\n');
+ const metadataSection = brew.text.slice(12, index - 1);
+ const metadata = yaml.load(metadataSection);
+ Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
+ brew.text = brew.text.slice(index + 5);
+ }
+ if(brew.text.startsWith('```css')) {
+ const index = brew.text.indexOf('```\n\n');
+ brew.style = brew.text.slice(7, index - 1);
+ brew.text = brew.text.slice(index + 5);
+ }
+};
+
+app.use('/', serveCompressedStaticAssets(`${__dirname}/../build`));
+
+process.chdir(__dirname);
+
+//app.use(express.static(`${__dirname}/build`));
+app.use(require('body-parser').json({ limit: '25mb' }));
+app.use(require('cookie-parser')());
+app.use(require('./forcessl.mw.js'));
+
+// FIXME: the config should be passed as an argument for the app
+const config = require('nconf')
+ .argv()
+ .env({ lowerCase: true })
+ .file('environment', { file: `config/${process.env.NODE_ENV}.json` })
+ .file('defaults', { file: 'config/default.json' });
+
+//Account Middleware
+app.use((req, res, next)=>{
+ if(req.cookies && req.cookies.nc_session){
+ try {
+ req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
+ //console.log("Just loaded up JWT from cookie:");
+ //console.log(req.account);
+ } catch (e){}
+ }
+
+ req.config = {
+ google_client_id : config.get('google_client_id'),
+ google_client_secret : config.get('google_client_secret')
+ };
+ return next();
+});
+
+app.use(homebrewApi);
+app.use(require('./admin.api.js'));
+
+const HomebrewModel = require('./homebrew.model.js').model;
+const welcomeText = require('fs').readFileSync('./../client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
+const welcomeTextV3 = require('fs').readFileSync('./../client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
+const changelogText = require('fs').readFileSync('./../changelog.md', 'utf8');
+const faqText = require('fs').readFileSync('./../faq.md', 'utf8');
+
+String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
+
+//Robots.txt
+app.get('/robots.txt', (req, res)=>{
+ return res.sendFile(`${__dirname}/robots.txt`);
+});
+
+//Home page
+app.get('/', async (req, res, next)=>{
+ const brew = {
+ text : welcomeText
+ };
+ req.brew = brew;
+ return next();
+});
+
+//Home page v3
+app.get('/v3_preview', async (req, res, next)=>{
+ const brew = {
+ text : welcomeTextV3,
+ renderer : 'V3'
+ };
+ splitTextStyleAndMetadata(brew);
+ req.brew = brew;
+ return next();
+});
+
+//Changelog page
+app.get('/changelog', async (req, res, next)=>{
+ const brew = {
+ title : 'Changelog',
+ text : changelogText,
+ renderer : 'V3'
+ };
+ splitTextStyleAndMetadata(brew);
+ req.brew = brew;
+ return next();
+});
+
+//FAQ page
+app.get('/faq', async (req, res, next)=>{
+ const brew = {
+ title : 'FAQ',
+ text : faqText,
+ renderer : 'V3'
+ };
+ splitTextStyleAndMetadata(brew);
+ req.brew = brew;
+ return next();
+});
+
+//Source page
+app.get('/source/:id', asyncHandler(async (req, res)=>{
+ const brew = await getBrewFromId(req.params.id, 'raw');
+
+ const replaceStrings = { '&': '&', '<': '<', '>': '>' };
+ let text = brew.text;
+ for (const replaceStr in replaceStrings) {
+ text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
+ }
+ text = `${text}`;
+ res.status(200).send(text);
+}));
+
+//Download brew source page
+app.get('/download/:id', asyncHandler(async (req, res)=>{
+ const brew = await getBrewFromId(req.params.id, 'raw');
+ const prefix = 'HB - ';
+
+ let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
+ if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
+ res.set({
+ 'Cache-Control' : 'no-cache',
+ 'Content-Type' : 'text/plain',
+ 'Content-Disposition' : `attachment; filename="${fileName}.txt"`
+ });
+ res.status(200).send(brew.text);
+}));
+
+//User Page
+app.get('/user/:username', async (req, res, next)=>{
+ const ownAccount = req.account && (req.account.username == req.params.username);
+
+ let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
+ .catch((err)=>{
+ console.log(err);
+ });
+
+ if(ownAccount && req?.account?.googleId){
+ const googleBrews = await GoogleActions.listGoogleBrews(req, res)
+ .catch((err)=>{
+ console.error(err);
+ });
+
+ if(googleBrews)
+ brews = _.concat(brews, googleBrews);
+ }
+
+ req.brews = _.map(brews, (brew)=>{
+ return sanitizeBrew(brew, !ownAccount);
+ });
+
+ return next();
+});
+
+//Edit Page
+app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
+ res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
+ const brew = await getBrewFromId(req.params.id, 'edit');
+ req.brew = brew;
+ return next();
+}));
+
+//New Page
+app.get('/new/:id', asyncHandler(async (req, res, next)=>{
+ const brew = await getBrewFromId(req.params.id, 'share');
+ brew.title = `CLONE - ${brew.title}`;
+ req.brew = brew;
+ return next();
+}));
+
+//Share Page
+app.get('/share/:id', asyncHandler(async (req, res, next)=>{
+ const brew = await getBrewFromId(req.params.id, 'share');
+
+ if(req.params.id.length > 12) {
+ const googleId = req.params.id.slice(0, -12);
+ const shareId = req.params.id.slice(-12);
+ await GoogleActions.increaseView(googleId, shareId, 'share', brew)
+ .catch((err)=>{next(err);});
+ } else {
+ await HomebrewModel.increaseView({ shareId: brew.shareId });
+ }
+
+ req.brew = brew;
+ return next();
+}));
+
+//Print Page
+app.get('/print/:id', asyncHandler(async (req, res, next)=>{
+ const brew = await getBrewFromId(req.params.id, 'share');
+ req.brew = brew;
+ return next();
+}));
+
+//Render the page
+const templateFn = require('./../client/template.js');
+app.use((req, res)=>{
+ const props = {
+ version : require('./../package.json').version,
+ url : req.originalUrl,
+ brew : req.brew,
+ brews : req.brews,
+ googleBrews : req.googleBrews,
+ account : req.account,
+ enable_v3 : config.get('enable_v3')
+ };
+ templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
+ .then((page)=>{ res.send(page); })
+ .catch((err)=>{
+ console.log(err);
+ return res.sendStatus(500);
+ });
+});
+
+//v=====----- Error-Handling Middleware -----=====v//
+//Format Errors so all fields will be sent
+const replaceErrors = (key, value)=>{
+ if(value instanceof Error) {
+ const error = {};
+ Object.getOwnPropertyNames(value).forEach(function (key) {
+ error[key] = value[key];
+ });
+ return error;
+ }
+ return value;
+};
+
+const getPureError = (error)=>{
+ return JSON.parse(JSON.stringify(error, replaceErrors));
+};
+
+app.use((err, req, res, next)=>{
+ const status = err.status || 500;
+ console.error(err);
+ res.status(status).send(getPureError(err));
+});
+//^=====--------------------------------------=====^//
+
+module.exports = {
+ app : app
+};
diff --git a/server/db.js b/server/db.js
new file mode 100644
index 000000000..030d7f61b
--- /dev/null
+++ b/server/db.js
@@ -0,0 +1,37 @@
+// The main purpose of this file is to provide an interface for database
+// connection. Even though the code is quite simple and basically a tiny
+// wrapper around mongoose package, it works as single point where
+// database setup/config is performed and the interface provided here can be
+// reused by both the main application and all tests which require database
+// connection.
+
+const Mongoose = require('mongoose');
+
+const getMongoDBURL = (config)=>{
+ return config.get('mongodb_uri') ||
+ config.get('mongolab_uri') ||
+ 'mongodb://localhost/homebrewery';
+};
+
+const handleConnectionError = (error)=>{
+ if(error) {
+ console.error('Could not connect to a Mongo database: \n');
+ console.error(error);
+ console.error('\nIf you are running locally, make sure mongodb.exe is running and DB URL is configured properly');
+ process.exit(1); // non-zero exit code to indicate an error
+ }
+};
+
+const disconnect = async ()=>{
+ return await Mongoose.disconnect();
+};
+
+const connect = async (config)=>{
+ return await Mongoose.connect(getMongoDBURL(config),
+ { retryWrites: false }, handleConnectionError);
+};
+
+module.exports = {
+ connect : connect,
+ disconnect : disconnect
+};
diff --git a/server/googleActions.js b/server/googleActions.js
index f6db7f310..0050cb81d 100644
--- a/server/googleActions.js
+++ b/server/googleActions.js
@@ -96,7 +96,7 @@ GoogleActions = {
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
const obj = await drive.files.list({
- pageSize : 100,
+ pageSize : 1000,
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
})
diff --git a/shared/homebrewery/renderWarnings/renderWarnings.jsx b/shared/homebrewery/renderWarnings/renderWarnings.jsx
index 3fd290260..981fc1969 100644
--- a/shared/homebrewery/renderWarnings/renderWarnings.jsx
+++ b/shared/homebrewery/renderWarnings/renderWarnings.jsx
@@ -7,6 +7,7 @@ const cx = require('classnames');
const DISMISS_KEY = 'dismiss_render_warning';
const RenderWarnings = createClass({
+ displayName : 'RenderWarnings',
getInitialState : function() {
return {
warnings : {}
diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx
index 6d756fd6f..42076ed76 100644
--- a/shared/naturalcrit/codeEditor/codeEditor.jsx
+++ b/shared/naturalcrit/codeEditor/codeEditor.jsx
@@ -40,6 +40,7 @@ if(typeof navigator !== 'undefined'){
}
const CodeEditor = createClass({
+ displayName : 'CodeEditor',
getDefaultProps : function() {
return {
language : '',
diff --git a/shared/naturalcrit/nav/nav.jsx b/shared/naturalcrit/nav/nav.jsx
index 11e67e32c..fde42a939 100644
--- a/shared/naturalcrit/nav/nav.jsx
+++ b/shared/naturalcrit/nav/nav.jsx
@@ -8,7 +8,8 @@ const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
const Nav = {
base : createClass({
- render : function(){
+ displayName : 'Nav.base',
+ render : function(){
return