mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-09-20 16:42:58 +00:00
330 lines
10 KiB
JavaScript
330 lines
10 KiB
JavaScript
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
|
// Set working directory to project root
|
|
import { dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import packageJSON from './../package.json' with { type: 'json' };
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
process.chdir(`${__dirname}/..`);
|
|
const version = packageJSON.version;
|
|
|
|
import _ from 'lodash';
|
|
import jwt from 'jwt-simple';
|
|
import express from 'express';
|
|
import config from './config.js';
|
|
import path from 'path';
|
|
import fs from 'fs-extra';
|
|
|
|
import api from './homebrew.api.js';
|
|
import pageRoutes from './page-routes.js';
|
|
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
|
|
import adminApi from './admin.api.js';
|
|
import vaultApi from './vault.api.js';
|
|
import GoogleActions from './googleActions.js';
|
|
import serveCompressedStaticAssets from './static-assets.mv.js';
|
|
import asyncHandler from 'express-async-handler';
|
|
import { model as HomebrewModel } from './homebrew.model.js';
|
|
|
|
//==== Middleware Imports ====//
|
|
import contentNegotiation from './middleware/content-negotiation.js';
|
|
import bodyParser from 'body-parser';
|
|
import cookieParser from 'cookie-parser';
|
|
import forceSSL from './forcessl.mw.js';
|
|
import dbCheck from './middleware/dbCheck.js';
|
|
|
|
import cors from 'cors';
|
|
|
|
export default async function createApp(vite) {
|
|
const app = express();
|
|
|
|
const nodeEnv = config.get('node_env');
|
|
const isProd = nodeEnv === 'production';
|
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
|
|
const sanitizeBrew = (brew, accessType)=>{
|
|
brew._id = undefined;
|
|
brew.__v = undefined;
|
|
if(accessType !== 'edit' && accessType !== 'shareAuthor') {
|
|
brew.editId = undefined;
|
|
}
|
|
return brew;
|
|
};
|
|
|
|
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
|
|
|
if(vite) {
|
|
app.use(vite.middlewares);
|
|
}
|
|
|
|
app.use('/', serveCompressedStaticAssets('build'));
|
|
app.use(contentNegotiation);
|
|
app.use(bodyParser.json({ limit: '25mb' }));
|
|
app.use(cookieParser());
|
|
app.use(forceSSL);
|
|
|
|
|
|
const corsOptions = {
|
|
origin : (origin, callback)=>{
|
|
|
|
const allowedOrigins = [
|
|
'https://homebrewery.naturalcrit.com',
|
|
'https://www.naturalcrit.com',
|
|
'https://naturalcrit-stage.herokuapp.com',
|
|
'https://homebrewery-stage.herokuapp.com',
|
|
];
|
|
|
|
const localNetworkRegex = /^http:\/\/(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+):\d+$/;
|
|
|
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
|
|
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin) || (isLocalEnvironment && localNetworkRegex.test(origin))) {
|
|
callback(null, true);
|
|
} else {
|
|
console.log(origin, 'not allowed');
|
|
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
|
}
|
|
},
|
|
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
credentials : true,
|
|
};
|
|
|
|
app.use(cors(corsOptions));
|
|
|
|
//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){
|
|
console.log(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(adminApi(vite));
|
|
app.use(vaultApi);
|
|
|
|
const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
|
const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
|
const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
|
|
const changelogText = fs.readFileSync('changelog.md', 'utf8');
|
|
const faqText = 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'
|
|
};
|
|
|
|
app.use(pageRoutes({
|
|
defaultMetaTags,
|
|
welcomeText,
|
|
welcomeTextLegacy,
|
|
migrateText,
|
|
changelogText,
|
|
faqText,
|
|
getBrew,
|
|
getUsersBrewThemes,
|
|
HomebrewModel,
|
|
GoogleActions,
|
|
sanitizeBrew,
|
|
}));
|
|
|
|
//Robots.txt
|
|
app.get('/robots.txt', (req, res)=>{
|
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
|
});
|
|
|
|
//Serve brew metadata
|
|
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|
const { brew } = req;
|
|
sanitizeBrew(brew, 'share');
|
|
|
|
const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
|
|
'published', 'views', 'shareId', 'createdAt', 'updatedAt',
|
|
'lastViewed', 'thumbnail', 'tags'
|
|
];
|
|
|
|
const metadata = fields.reduce((acc, field)=>{
|
|
if(brew[field] !== undefined) acc[field] = brew[field];
|
|
return acc;
|
|
}, {});
|
|
res.status(200).json(metadata);
|
|
});
|
|
|
|
//Serve brew styling
|
|
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
|
|
|
//Change author name on brews
|
|
app.put('/api/user/rename', dbCheck, async (req, res)=>{
|
|
const { username, newUsername } = req.body;
|
|
const ownAccount = req.account && (req.account.username == newUsername);
|
|
|
|
if(!username || !newUsername)
|
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
|
if(!ownAccount)
|
|
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
|
try {
|
|
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
|
const renamePromises = brews.map(async (brew)=>{
|
|
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
|
);
|
|
return HomebrewModel.updateOne(
|
|
{ _id: brew._id },
|
|
{ $set: { authors: updatedAuthors } }
|
|
);
|
|
});
|
|
await Promise.all(renamePromises);
|
|
|
|
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
|
} catch (error) {
|
|
console.error('Error renaming brews:', error);
|
|
return res.status(500).json({ error: 'Failed to rename brews.' });
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
// Add Static Local Paths
|
|
app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
|
|
app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
|
|
|
|
//Send rendered page
|
|
app.use(asyncHandler(async (req, res, next)=>{
|
|
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
|
|
|
const page = await renderPage(req, res);
|
|
if(!page) return;
|
|
res.send(page);
|
|
}));
|
|
|
|
//Render the page
|
|
const renderPage = async (req, res)=>{
|
|
|
|
// Create configuration object
|
|
const configuration = {
|
|
local : isLocalEnvironment,
|
|
publicUrl : config.get('publicUrl') ?? '',
|
|
baseUrl : `${req.protocol}://${req.get('host')}`,
|
|
environment : nodeEnv,
|
|
deployment : config.get('heroku_app_name') ?? ''
|
|
};
|
|
const props = {
|
|
version : version,
|
|
url : req.customUrl || req.originalUrl,
|
|
brew : req.brew,
|
|
brews : req.brews,
|
|
googleBrews : req.googleBrews,
|
|
account : req.account,
|
|
config : configuration,
|
|
ogMeta : req.ogMeta,
|
|
userThemes : req.userThemes
|
|
};
|
|
|
|
const ogTags = [];
|
|
const ogMeta = req.ogMeta ?? {};
|
|
Object.entries(ogMeta).forEach(([key, value])=>{
|
|
if(!value) return;
|
|
const tag = `<meta property="og:${key}" content="${value}">`;
|
|
ogTags.push(tag);
|
|
});
|
|
const ogMetaTags = ogTags.join('\n');
|
|
|
|
const htmlPath = isProd ? path.resolve('build', 'index.html') : path.resolve('index.html');
|
|
let html = fs.readFileSync(htmlPath, 'utf-8');
|
|
|
|
if(!isProd && vite?.transformIndexHtml) {
|
|
html = await vite.transformIndexHtml(req.originalUrl, html);
|
|
}
|
|
|
|
html = html.replace(
|
|
'<head>',
|
|
()=>{ return `<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`; }
|
|
);
|
|
|
|
return html;
|
|
};
|
|
|
|
//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)=>{
|
|
err.originalUrl = req.originalUrl;
|
|
console.error(err);
|
|
|
|
if(err.originalUrl?.startsWith('/api')) {
|
|
// console.log('API error');
|
|
res.status(err.status || err.response?.status || 500).send(err);
|
|
return;
|
|
}
|
|
|
|
// console.log('non-API error');
|
|
const status = err.status || err.code || 500;
|
|
|
|
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.');
|
|
}
|
|
});
|
|
//^=====--------------------------------------=====^//
|
|
|
|
return app;
|
|
}
|