mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 14:12:40 +00:00
When the Homebrewery was first made, editIds and ShareIds only had 10 characters. We later increased this to 12. However this means some old, old Google Drive links (in the form of `googleId + editId`) were being split incorrectly because they assumed the newer 12-char length, accidentally cutting the last 2 chars from the googleId.
501 lines
14 KiB
JavaScript
501 lines
14 KiB
JavaScript
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
|
// Set working directory to project root
|
|
process.chdir(`${__dirname}/..`);
|
|
|
|
const _ = require('lodash');
|
|
const jwt = require('jwt-simple');
|
|
const express = require('express');
|
|
const yaml = require('js-yaml');
|
|
const app = express();
|
|
const config = require('./config.js');
|
|
|
|
const { homebrewApi, getBrew } = 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 { DEFAULT_BREW } = require('./brewDefaults.js');
|
|
|
|
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', 'theme', 'lang']));
|
|
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);
|
|
}
|
|
};
|
|
|
|
const sanitizeBrew = (brew, accessType)=>{
|
|
brew._id = undefined;
|
|
brew.__v = undefined;
|
|
if(accessType !== 'edit'){
|
|
brew.editId = undefined;
|
|
}
|
|
return brew;
|
|
};
|
|
|
|
app.use('/', serveCompressedStaticAssets(`build`));
|
|
app.use(require('./middleware/content-negotiation.js'));
|
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
|
app.use(require('cookie-parser')());
|
|
app.use(require('./forcessl.mw.js'));
|
|
|
|
//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 welcomeTextLegacy = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
|
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.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);};
|
|
|
|
const defaultMetaTags = {
|
|
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
|
title : 'The Homebrewery',
|
|
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
|
image : `${config.get('publicUrl')}/thumbnail.png`,
|
|
type : 'website'
|
|
};
|
|
|
|
//Robots.txt
|
|
app.get('/robots.txt', (req, res)=>{
|
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
|
});
|
|
|
|
//Home page
|
|
app.get('/', (req, res, next)=>{
|
|
req.brew = {
|
|
text : welcomeText,
|
|
renderer : 'V3'
|
|
},
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'Homepage',
|
|
description : 'Homepage'
|
|
};
|
|
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
});
|
|
|
|
//Home page Legacy
|
|
app.get('/legacy', (req, res, next)=>{
|
|
req.brew = {
|
|
text : welcomeTextLegacy,
|
|
renderer : 'legacy'
|
|
},
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'Homepage (Legacy)',
|
|
description : 'Homepage'
|
|
};
|
|
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
});
|
|
|
|
//Legacy/Other Document -> v3 Migration Guide
|
|
app.get('/migrate', (req, res, next)=>{
|
|
req.brew = {
|
|
text : migrateText,
|
|
renderer : 'V3'
|
|
},
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'v3 Migration Guide',
|
|
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
|
};
|
|
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
});
|
|
|
|
//Changelog page
|
|
app.get('/changelog', async (req, res, next)=>{
|
|
req.brew = {
|
|
title : 'Changelog',
|
|
text : changelogText,
|
|
renderer : 'V3'
|
|
},
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'Changelog',
|
|
description : 'Development changelog.'
|
|
};
|
|
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
});
|
|
|
|
//FAQ page
|
|
app.get('/faq', async (req, res, next)=>{
|
|
req.brew = {
|
|
title : 'FAQ',
|
|
text : faqText,
|
|
renderer : 'V3'
|
|
},
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'FAQ',
|
|
description : 'Frequently Asked Questions'
|
|
};
|
|
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
});
|
|
|
|
//Source page
|
|
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|
const { brew } = req;
|
|
|
|
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
|
let text = brew.text;
|
|
for (const replaceStr in replaceStrings) {
|
|
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
|
}
|
|
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
|
res.status(200).send(text);
|
|
});
|
|
|
|
//Download brew source page
|
|
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|
const { brew } = req;
|
|
sanitizeBrew(brew, 'share');
|
|
const prefix = 'HB - ';
|
|
|
|
const encodeRFC3986ValueChars = (str)=>{
|
|
return (
|
|
encodeURIComponent(str)
|
|
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
|
);
|
|
};
|
|
|
|
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*=UTF-8''${encodeRFC3986ValueChars(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);
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : `${req.params.username}'s Collection`,
|
|
description : 'View my collection of homebrew on the Homebrewery.'
|
|
// type : could be 'profile'?
|
|
};
|
|
|
|
const fields = [
|
|
'googleId',
|
|
'title',
|
|
'pageCount',
|
|
'description',
|
|
'authors',
|
|
'lang',
|
|
'published',
|
|
'views',
|
|
'shareId',
|
|
'editId',
|
|
'createdAt',
|
|
'updatedAt',
|
|
'lastViewed',
|
|
'thumbnail',
|
|
'tags'
|
|
];
|
|
|
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
|
.catch((err)=>{
|
|
console.log(err);
|
|
});
|
|
|
|
if(ownAccount && req?.account?.googleId){
|
|
const auth = await GoogleActions.authCheck(req.account, res);
|
|
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
|
.catch((err)=>{
|
|
console.error(err);
|
|
});
|
|
|
|
if(googleBrews && googleBrews.length > 0) {
|
|
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
|
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
|
if(match !== -1) {
|
|
brew.googleId = googleBrews[match].googleId;
|
|
brew.stubbed = true;
|
|
brew.pageCount = googleBrews[match].pageCount;
|
|
brew.renderer = googleBrews[match].renderer;
|
|
brew.version = googleBrews[match].version;
|
|
googleBrews.splice(match, 1);
|
|
}
|
|
}
|
|
|
|
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
|
brews = _.concat(brews, googleBrews);
|
|
}
|
|
}
|
|
|
|
req.brews = _.map(brews, (brew)=>{
|
|
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
|
});
|
|
|
|
return next();
|
|
});
|
|
|
|
//Edit Page
|
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : req.brew.title || 'Untitled Brew',
|
|
description : req.brew.description || 'No description.',
|
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
|
type : 'article'
|
|
};
|
|
|
|
sanitizeBrew(req.brew, 'edit');
|
|
splitTextStyleAndMetadata(req.brew);
|
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
|
return next();
|
|
});
|
|
|
|
//New Page
|
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|
sanitizeBrew(req.brew, 'share');
|
|
splitTextStyleAndMetadata(req.brew);
|
|
const brew = {
|
|
shareId : req.brew.shareId,
|
|
title : `CLONE - ${req.brew.title}`,
|
|
text : req.brew.text,
|
|
style : req.brew.style,
|
|
renderer : req.brew.renderer,
|
|
theme : req.brew.theme
|
|
};
|
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'New',
|
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
|
};
|
|
|
|
return next();
|
|
});
|
|
|
|
//Share Page
|
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
|
const { brew } = req;
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : req.brew.title || 'Untitled Brew',
|
|
description : req.brew.description || 'No description.',
|
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
|
type : 'article'
|
|
};
|
|
|
|
if(req.params.id.length > 12 && !brew._id) {
|
|
const googleId = brew.googleId;
|
|
const shareId = brew.shareId;
|
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
|
.catch((err)=>{next(err);});
|
|
} else {
|
|
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
|
}
|
|
sanitizeBrew(req.brew, 'share');
|
|
splitTextStyleAndMetadata(req.brew);
|
|
return next();
|
|
}));
|
|
|
|
//Print Page
|
|
app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|
sanitizeBrew(req.brew, 'share');
|
|
splitTextStyleAndMetadata(req.brew);
|
|
next();
|
|
});
|
|
|
|
//Account Page
|
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
|
const data = {};
|
|
data.title = 'Account Information Page';
|
|
|
|
let auth;
|
|
let googleCount = [];
|
|
if(req.account) {
|
|
if(req.account.googleId) {
|
|
try {
|
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
|
} catch (e) {
|
|
auth = undefined;
|
|
console.log('Google auth check failed!');
|
|
console.log(e);
|
|
}
|
|
if(auth.credentials.access_token) {
|
|
try {
|
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
|
} catch (e) {
|
|
googleCount = undefined;
|
|
console.log('List Google files failed!');
|
|
console.log(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
|
.catch((err)=>{
|
|
mongoCount = 0;
|
|
console.log(err);
|
|
});
|
|
|
|
data.uiItems = {
|
|
username : req.account.username,
|
|
issued : req.account.issued,
|
|
googleId : Boolean(req.account.googleId),
|
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
|
mongoCount : mongoCount,
|
|
googleCount : googleCount?.length
|
|
};
|
|
}
|
|
|
|
req.brew = data;
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : `Account Page`,
|
|
description : null
|
|
};
|
|
|
|
return next();
|
|
}));
|
|
|
|
const nodeEnv = config.get('node_env');
|
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
// Local only
|
|
if(isLocalEnvironment){
|
|
// Login
|
|
app.post('/local/login', (req, res)=>{
|
|
const username = req.body.username;
|
|
if(!username) return;
|
|
|
|
const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
|
|
return res.json(payload);
|
|
});
|
|
}
|
|
|
|
//Render the page
|
|
const templateFn = require('./../client/template.js');
|
|
const renderPage = async (req, res)=>{
|
|
// Create configuration object
|
|
const configuration = {
|
|
local : isLocalEnvironment,
|
|
publicUrl : config.get('publicUrl') ?? '',
|
|
environment : nodeEnv
|
|
};
|
|
const props = {
|
|
version : require('./../package.json').version,
|
|
url : req.customUrl || req.originalUrl,
|
|
brew : req.brew,
|
|
brews : req.brews,
|
|
googleBrews : req.googleBrews,
|
|
account : req.account,
|
|
enable_v3 : config.get('enable_v3'),
|
|
enable_themes : config.get('enable_themes'),
|
|
config : configuration,
|
|
ogMeta : req.ogMeta
|
|
};
|
|
const title = req.brew ? req.brew.title : '';
|
|
const page = await templateFn('homebrew', title, props)
|
|
.catch((err)=>{
|
|
console.log(err);
|
|
});
|
|
return page;
|
|
};
|
|
|
|
//Send rendered page
|
|
app.use(asyncHandler(async (req, res, next)=>{
|
|
const page = await renderPage(req, res);
|
|
if(!page) return;
|
|
res.send(page);
|
|
}));
|
|
|
|
//v=====----- Error-Handling Middleware -----=====v//
|
|
//Format Errors as plain objects so all fields will appear in the string sent
|
|
const formatErrors = (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, formatErrors));
|
|
};
|
|
|
|
app.use(async (err, req, res, next)=>{
|
|
const status = err.status || err.code || 500;
|
|
console.error(err);
|
|
|
|
req.ogMeta = { ...defaultMetaTags,
|
|
title : 'Error Page',
|
|
description : 'Something went wrong!'
|
|
};
|
|
req.brew = {
|
|
...err,
|
|
title : 'Error - Something went wrong!',
|
|
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
|
status : status,
|
|
HBErrorCode : err.HBErrorCode ?? '00',
|
|
pureError : getPureError(err)
|
|
};
|
|
req.customUrl= '/error';
|
|
|
|
const page = await renderPage(req, res);
|
|
if(!page) return;
|
|
res.send(page);
|
|
});
|
|
|
|
app.use((req, res)=>{
|
|
if(!res.headersSent) {
|
|
console.error('Headers have not been sent, responding with a server error.', req.url);
|
|
res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
|
|
}
|
|
});
|
|
//^=====--------------------------------------=====^//
|
|
|
|
module.exports = {
|
|
app : app
|
|
};
|