mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-14 12:38:44 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into search-by-tags
This commit is contained in:
+281
-229
@@ -4,64 +4,82 @@ import { model as NotificationModel } from './notifications.model.js';
|
||||
import express from 'express';
|
||||
import Moment from 'moment';
|
||||
import zlib from 'zlib';
|
||||
import templateFn from '../client/template.js';
|
||||
import config from './config.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isProd = nodeEnv === 'production';
|
||||
|
||||
import HomebrewAPI from './homebrew.api.js';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||
|
||||
const mw = {
|
||||
adminOnly : (req, res, next)=>{
|
||||
if(!req.get('authorization')){
|
||||
return res
|
||||
export default function createAdminApi(vite) {
|
||||
const router = express.Router();
|
||||
|
||||
const mw = {
|
||||
adminOnly : (req, res, next)=>{
|
||||
if(!req.get('authorization')){
|
||||
return res
|
||||
.set('WWW-Authenticate', 'Basic realm="Authorization Required"')
|
||||
.status(401)
|
||||
.send('Authorization Required');
|
||||
}
|
||||
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
|
||||
}
|
||||
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
|
||||
.toString('ascii')
|
||||
.split(':');
|
||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||
return next();
|
||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||
return next();
|
||||
}
|
||||
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
||||
}
|
||||
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const junkBrewPipeline = [
|
||||
{ $match : {
|
||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||
} },
|
||||
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
||||
{ $match: { textBinSize: { $lt: 140 } } },
|
||||
{ $limit: 100 }
|
||||
];
|
||||
// Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||
const junkBrewsPipeline = [
|
||||
{ $match : {
|
||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||
} },
|
||||
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1 } },
|
||||
{ $match: { textBinSize: { $lt: 140 } } },
|
||||
{ $limit: 300 }
|
||||
];
|
||||
|
||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||
const uncompressedBrewQuery = HomebrewModel.find({
|
||||
'text' : { '$exists': true }
|
||||
}).lean().limit(10000).select('_id');
|
||||
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
|
||||
const lostBrewsPipeline = [
|
||||
{
|
||||
$match : {
|
||||
authors : [],
|
||||
updatedAt : { $lt: Moment().subtract(2, 'years').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(2, 'years').toDate() }
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit : 500
|
||||
}
|
||||
];
|
||||
|
||||
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((objs)=>res.json({ count: objs.length }))
|
||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||
const uncompressedBrewQuery = HomebrewModel.find({
|
||||
'text' : { '$exists': true }
|
||||
}).lean().limit(10000).select('_id');
|
||||
|
||||
router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
|
||||
.catch((error)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
||||
// Delete result of junkBrewsPipeline
|
||||
router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((docs)=>{
|
||||
const ids = docs.map((doc)=>doc._id);
|
||||
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||
@@ -71,18 +89,41 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* Searches for matching edit or share id, also attempts to partial match */
|
||||
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
|
||||
return res.json(req.brew);
|
||||
});
|
||||
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
|
||||
.catch((error)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
});
|
||||
|
||||
/* Find 50 brews that aren't compressed yet */
|
||||
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||
const query = uncompressedBrewQuery.clone();
|
||||
// Delete result of lostBrewsPipeline
|
||||
router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((docs)=>{
|
||||
const ids = docs.map((doc)=>doc._id);
|
||||
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||
}).then((result)=>{
|
||||
res.json({ count: result.deletedCount });
|
||||
}).catch((error)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
});
|
||||
|
||||
query.exec()
|
||||
/* Searches for matching edit or share id, also attempts to partial match */
|
||||
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
|
||||
return res.json(req.brew);
|
||||
});
|
||||
|
||||
/* Find 50 brews that aren't compressed yet */
|
||||
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||
const query = uncompressedBrewQuery.clone();
|
||||
|
||||
query.exec()
|
||||
.then((objs)=>{
|
||||
const ids = objs.map((obj)=>obj._id);
|
||||
res.json({ count: ids.length, ids });
|
||||
@@ -91,46 +132,44 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||
console.error(err);
|
||||
res.status(500).send(err.message || 'Internal Server Error');
|
||||
});
|
||||
});
|
||||
|
||||
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
||||
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
||||
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
|
||||
|
||||
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
||||
|
||||
const brew = req.brew;
|
||||
|
||||
const properties = ['text', 'description', 'title'];
|
||||
properties.forEach((property)=>{
|
||||
brew[property] = cleanText(brew[property]);
|
||||
});
|
||||
|
||||
splitTextStyleAndMetadata(brew);
|
||||
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
||||
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
||||
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
|
||||
|
||||
req.body = brew;
|
||||
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
||||
|
||||
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||
req.account = undefined;
|
||||
const brew = req.brew;
|
||||
|
||||
return await HomebrewAPI.updateBrew(req, res);
|
||||
});
|
||||
const properties = ['text', 'description', 'title'];
|
||||
properties.forEach((property)=>{
|
||||
brew[property] = cleanText(brew[property]);
|
||||
});
|
||||
|
||||
/* Get list of a user's documents */
|
||||
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
|
||||
const username = req.params.user;
|
||||
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
|
||||
req.body = brew;
|
||||
|
||||
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
|
||||
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||
req.account = undefined;
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields);
|
||||
return await HomebrewAPI.updateBrew(req, res);
|
||||
});
|
||||
|
||||
return res.json(brews);
|
||||
});
|
||||
/* Get list of a user's documents */
|
||||
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
|
||||
const username = req.params.user;
|
||||
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
|
||||
|
||||
/* Compresses the "text" field of a brew to binary */
|
||||
router.put('/admin/compress/:id', (req, res)=>{
|
||||
HomebrewModel.findOne({ _id: req.params.id })
|
||||
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields);
|
||||
|
||||
return res.json(brews);
|
||||
});
|
||||
|
||||
/* Compresses the "text" field of a brew to binary */
|
||||
router.put('/admin/compress/:id', (req, res)=>{
|
||||
HomebrewModel.findOne({ _id: req.params.id })
|
||||
.then((brew)=>{
|
||||
if(!brew)
|
||||
return res.status(404).send('Brew not found');
|
||||
@@ -147,239 +186,252 @@ router.put('/admin/compress/:id', (req, res)=>{
|
||||
console.error(err);
|
||||
res.status(500).send('Error while saving');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||
try {
|
||||
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
|
||||
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||
try {
|
||||
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
|
||||
|
||||
return res.json({
|
||||
totalBrews : totalBrewsCount,
|
||||
totalPublishedBrews : publishedBrewsCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
return res.json({
|
||||
totalBrews : totalBrewsCount,
|
||||
totalPublishedBrews : publishedBrewsCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
// ####################### LOCKS
|
||||
// ####################### LOCKS
|
||||
|
||||
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const countLocksQuery = {
|
||||
lock : { $exists: true }
|
||||
};
|
||||
const count = await HomebrewModel.countDocuments(countLocksQuery)
|
||||
const countLocksQuery = {
|
||||
lock : { $exists: true }
|
||||
};
|
||||
const count = await HomebrewModel.countDocuments(countLocksQuery)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error };
|
||||
});
|
||||
|
||||
return res.json({ count });
|
||||
return res.json({ count });
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countLocksPipeline = [
|
||||
{
|
||||
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countLocksPipeline = [
|
||||
{
|
||||
$match :
|
||||
{
|
||||
'lock' : { '$exists': 1 }
|
||||
},
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
|
||||
];
|
||||
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error };
|
||||
});
|
||||
return res.json({
|
||||
lockedDocuments
|
||||
});
|
||||
return res.json({
|
||||
lockedDocuments
|
||||
});
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const lock = req.body;
|
||||
const lock = req.body;
|
||||
|
||||
lock.applied = new Date;
|
||||
lock.applied = new Date;
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
|
||||
|
||||
if(brew.lock && !lock.overwrite) {
|
||||
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
|
||||
}
|
||||
if(brew.lock && !lock.overwrite) {
|
||||
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
|
||||
}
|
||||
|
||||
lock.overwrite = undefined;
|
||||
lock.overwrite = undefined;
|
||||
|
||||
brew.lock = lock;
|
||||
brew.markModified('lock');
|
||||
brew.lock = lock;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
|
||||
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
const filter = {
|
||||
shareId : req.params.id
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
|
||||
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
|
||||
|
||||
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
|
||||
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
|
||||
|
||||
brew.lock = undefined;
|
||||
brew.markModified('lock');
|
||||
brew.lock = undefined;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
|
||||
}));
|
||||
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
|
||||
}));
|
||||
|
||||
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countReviewsPipeline = [
|
||||
{
|
||||
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const countReviewsPipeline = [
|
||||
{
|
||||
$match :
|
||||
{
|
||||
'lock.reviewRequested' : { '$exists': 1 }
|
||||
},
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
},
|
||||
{
|
||||
$project : {
|
||||
shareId : 1,
|
||||
editId : 1,
|
||||
title : 1,
|
||||
lock : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
|
||||
];
|
||||
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error };
|
||||
});
|
||||
return res.json({
|
||||
reviewDocuments
|
||||
});
|
||||
return res.json({
|
||||
reviewDocuments
|
||||
});
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
|
||||
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
|
||||
// === This route is NOT Admin only ===
|
||||
// Any user can request a review of their document
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
lock : { $exists: 1 }
|
||||
};
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
lock : { $exists: 1 }
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
|
||||
|
||||
if(brew.lock.reviewRequested){
|
||||
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
|
||||
};
|
||||
if(brew.lock.reviewRequested){
|
||||
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
|
||||
};
|
||||
|
||||
brew.lock.reviewRequested = new Date();
|
||||
brew.markModified('lock');
|
||||
brew.lock.reviewRequested = new Date();
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
|
||||
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
'lock.reviewRequested' : { $exists: 1 }
|
||||
};
|
||||
const filter = {
|
||||
shareId : req.params.id,
|
||||
'lock.reviewRequested' : { $exists: 1 }
|
||||
};
|
||||
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
|
||||
const brew = await HomebrewModel.findOne(filter);
|
||||
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
|
||||
|
||||
brew.lock.reviewRequested = undefined;
|
||||
brew.markModified('lock');
|
||||
brew.lock.reviewRequested = undefined;
|
||||
brew.markModified('lock');
|
||||
|
||||
await brew.save()
|
||||
await brew.save()
|
||||
.catch((error)=>{
|
||||
throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error };
|
||||
});
|
||||
|
||||
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
|
||||
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
// ####################### NOTIFICATIONS
|
||||
// ####################### NOTIFICATIONS
|
||||
|
||||
router.get('/admin/notification/all', async (req, res, next)=>{
|
||||
try {
|
||||
const notifications = await NotificationModel.getAll();
|
||||
return res.json(notifications);
|
||||
router.get('/admin/notification/all', async (req, res, next)=>{
|
||||
try {
|
||||
const notifications = await NotificationModel.getAll();
|
||||
return res.json(notifications);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error getting all notifications: ', error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
||||
try {
|
||||
const notification = await NotificationModel.addNotification(req.body);
|
||||
return res.status(201).json(notification);
|
||||
} catch (error) {
|
||||
console.log('Error adding notification: ', error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
|
||||
try {
|
||||
const notification = await NotificationModel.deleteNotification(req.params.id);
|
||||
return res.json(notification);
|
||||
} catch (error) {
|
||||
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||
templateFn('admin', {
|
||||
url : req.originalUrl
|
||||
})
|
||||
.then((page)=>res.send(page))
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
res.sendStatus(500);
|
||||
} catch (error) {
|
||||
console.log('Error getting all notifications: ', error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
||||
try {
|
||||
const notification = await NotificationModel.addNotification(req.body);
|
||||
return res.status(201).json(notification);
|
||||
} catch (error) {
|
||||
console.log('Error adding notification: ', error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
|
||||
try {
|
||||
const notification = await NotificationModel.deleteNotification(req.params.id);
|
||||
return res.json(notification);
|
||||
} catch (error) {
|
||||
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/admin', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||
const props = {
|
||||
url : req.originalUrl
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
res.send(html.replace(
|
||||
'<head>',
|
||||
`<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>`
|
||||
));
|
||||
}));
|
||||
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
+167
-223
@@ -1,30 +1,45 @@
|
||||
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
|
||||
import mongoose from 'mongoose';
|
||||
import supertest from 'supertest';
|
||||
import HBApp from './app.js';
|
||||
import createApp from './app.js';
|
||||
import { model as NotificationModel } from './notifications.model.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
|
||||
let app;
|
||||
let request;
|
||||
let dbState;
|
||||
|
||||
// Mimic https responses to avoid being redirected all the time
|
||||
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
||||
beforeAll(async ()=>{
|
||||
app = await createApp();
|
||||
request = supertest.agent(app).set('X-Forwarded-Proto', 'https');
|
||||
});
|
||||
|
||||
describe('Tests for admin api', ()=>{
|
||||
beforeEach(()=>{
|
||||
dbState = mongoose.connection.readyState;
|
||||
mongoose.connection.readyState = 1;
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
mongoose.connection.readyState = dbState;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async ()=>{
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe('Notifications', ()=>{
|
||||
it('should return list of all notifications', async ()=>{
|
||||
const testNotifications = ['a', 'b'];
|
||||
|
||||
jest.spyOn(NotificationModel, 'find')
|
||||
.mockImplementationOnce(()=>{
|
||||
jest.spyOn(NotificationModel, 'find').mockImplementationOnce(()=>{
|
||||
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.get('/admin/notification/all')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
const response = await request
|
||||
.get('/admin/notification/all')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(testNotifications);
|
||||
@@ -44,18 +59,17 @@ describe('Tests for admin api', ()=>{
|
||||
_id : expect.any(String),
|
||||
createdAt : expect.any(String),
|
||||
startAt : inputNotification.startAt,
|
||||
stopAt : inputNotification.stopAt,
|
||||
stopAt : inputNotification.stopAt
|
||||
};
|
||||
|
||||
jest.spyOn(NotificationModel.prototype, 'save')
|
||||
.mockImplementationOnce(function() {
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.post('/admin/notification/add')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(inputNotification);
|
||||
const response = await request
|
||||
.post('/admin/notification/add')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(inputNotification);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual(savedNotification);
|
||||
@@ -69,16 +83,14 @@ describe('Tests for admin api', ()=>{
|
||||
stopAt : new Date().toISOString()
|
||||
};
|
||||
|
||||
//Change 'save' function to just return itself instead of actually interacting with the database
|
||||
jest.spyOn(NotificationModel.prototype, 'save')
|
||||
.mockImplementationOnce(function() {
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.post('/admin/notification/add')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(inputNotification);
|
||||
const response = await request
|
||||
.post('/admin/notification/add')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(inputNotification);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ message: 'Dismiss key is required!' });
|
||||
@@ -87,15 +99,15 @@ describe('Tests for admin api', ()=>{
|
||||
it('should delete a notification based on its dismiss key', async ()=>{
|
||||
const dismissKey = 'testKey';
|
||||
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||
.mockImplementationOnce((key)=>{
|
||||
return { exec: jest.fn().mockResolvedValue(key) };
|
||||
});
|
||||
const response = await app
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce((key)=>{
|
||||
return { exec: jest.fn().mockResolvedValue(key) };
|
||||
});
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||
const response = await request
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
||||
});
|
||||
@@ -103,15 +115,15 @@ describe('Tests for admin api', ()=>{
|
||||
it('should handle error deleting a notification that doesnt exist', async ()=>{
|
||||
const dismissKey = 'testKey';
|
||||
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||
.mockImplementationOnce(()=>{
|
||||
return { exec: jest.fn().mockResolvedValue() };
|
||||
});
|
||||
const response = await app
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce(()=>{
|
||||
return { exec: jest.fn().mockResolvedValue() };
|
||||
});
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||
const response = await request
|
||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({ message: 'Notification not found' });
|
||||
});
|
||||
@@ -120,30 +132,24 @@ describe('Tests for admin api', ()=>{
|
||||
describe('Locks', ()=>{
|
||||
describe('Count', ()=>{
|
||||
it('Count of all locked documents', async ()=>{
|
||||
const testNumber = 16777216; // 8^8, because why not
|
||||
const testNumber = 16777216;
|
||||
|
||||
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testNumber);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.resolve(testNumber));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/count');
|
||||
const response = await request
|
||||
.get('/api/lock/count')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ count: testNumber });
|
||||
});
|
||||
|
||||
it('Handle error while fetching count of locked documents', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.reject());
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/count');
|
||||
const response = await request
|
||||
.get('/api/lock/count')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -151,7 +157,7 @@ describe('Tests for admin api', ()=>{
|
||||
message : 'Unable to get lock count',
|
||||
name : 'Lock Count Error',
|
||||
originalUrl : '/api/lock/count',
|
||||
status : 500,
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -160,28 +166,22 @@ describe('Tests for admin api', ()=>{
|
||||
it('Get list of all locked documents', async ()=>{
|
||||
const testLocks = ['a', 'b'];
|
||||
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testLocks);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/locks');
|
||||
const response = await request
|
||||
.get('/api/locks')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ lockedDocuments: testLocks });
|
||||
});
|
||||
|
||||
it('Handle error while fetching list of all locked documents', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/locks');
|
||||
const response = await request
|
||||
.get('/api/locks')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -196,28 +196,22 @@ describe('Tests for admin api', ()=>{
|
||||
it('Get list of all locked documents with pending review requests', async ()=>{
|
||||
const testLocks = ['a', 'b'];
|
||||
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testLocks);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/reviews');
|
||||
const response = await request
|
||||
.get('/api/lock/reviews')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ reviewDocuments: testLocks });
|
||||
});
|
||||
|
||||
it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{
|
||||
jest.spyOn(HomebrewModel, 'aggregate')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.reject();
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.get('/api/lock/reviews');
|
||||
const response = await request
|
||||
.get('/api/lock/reviews')
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -235,8 +229,8 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); }
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve()
|
||||
};
|
||||
|
||||
const testLock = {
|
||||
@@ -245,15 +239,12 @@ describe('Tests for admin api', ()=>{
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
const response = await request
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
@@ -277,24 +268,21 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve(),
|
||||
lock : {
|
||||
code : 1,
|
||||
editMessage : 'oldEdit',
|
||||
shareMessage : 'oldShare',
|
||||
shareMessage : 'oldShare'
|
||||
}
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
const response = await request
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
@@ -317,24 +305,21 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve(),
|
||||
lock : {
|
||||
code : 1,
|
||||
editMessage : 'oldEdit',
|
||||
shareMessage : 'oldShare',
|
||||
shareMessage : 'oldShare'
|
||||
}
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
const response = await request
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -352,8 +337,8 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); }
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.reject()
|
||||
};
|
||||
|
||||
const testLock = {
|
||||
@@ -362,15 +347,12 @@ describe('Tests for admin api', ()=>{
|
||||
shareMessage : 'share'
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.send(testLock);
|
||||
const response = await request
|
||||
.post(`/api/lock/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.send(testLock);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -396,19 +378,17 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
|
||||
'Authorization',
|
||||
`Basic ${Buffer.from('admin:password3').toString('base64')}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
@@ -421,18 +401,16 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve()
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
|
||||
'Authorization',
|
||||
`Basic ${Buffer.from('admin:password3').toString('base64')}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -441,7 +419,7 @@ describe('Tests for admin api', ()=>{
|
||||
name : 'Not Locked',
|
||||
originalUrl : `/api/unlock/${testBrew.shareId}`,
|
||||
shareId : testBrew.shareId,
|
||||
status : 500,
|
||||
status : 500
|
||||
});
|
||||
});
|
||||
|
||||
@@ -456,19 +434,17 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.reject(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/unlock/${testBrew.shareId}`);
|
||||
const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
|
||||
'Authorization',
|
||||
`Basic ${Buffer.from('admin:password3').toString('base64')}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -494,40 +470,28 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`,
|
||||
name : 'Review Requested',
|
||||
name : 'Review Requested'
|
||||
});
|
||||
});
|
||||
|
||||
it('Error when cannot find a locked brew', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId'
|
||||
};
|
||||
const testBrew = { shareId: 'shareId' };
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
|
||||
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||
.catch((err)=>{return err;});
|
||||
const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -557,25 +521,20 @@ describe('Tests for admin api', ()=>{
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
.mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||
.catch((err)=>{return err;});
|
||||
const response = await request
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
HBErrorCode : '70',
|
||||
HBErrorCode : '71',
|
||||
code : 500,
|
||||
message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
|
||||
name : 'Brew Not Found',
|
||||
message : `Review already requested for brew ${testBrew.shareId} - ${testBrew.title}`,
|
||||
name : 'Review Already Requested',
|
||||
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||
});
|
||||
});
|
||||
|
||||
it('Handle error while adding review request to a locked brew', async ()=>{
|
||||
const testLock = {
|
||||
applied : 'YES',
|
||||
@@ -587,18 +546,14 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.reject(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -622,19 +577,16 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.resolve(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.resolve(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
const response = await request
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
@@ -644,18 +596,13 @@ describe('Tests for admin api', ()=>{
|
||||
});
|
||||
|
||||
it('Error when clearing review request from a brew with no review request', async ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
};
|
||||
const testBrew = { shareId: 'shareId' };
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
const response = await request
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
@@ -678,19 +625,16 @@ describe('Tests for admin api', ()=>{
|
||||
const testBrew = {
|
||||
shareId : 'shareId',
|
||||
title : 'title',
|
||||
markModified : ()=>{ return true; },
|
||||
save : ()=>{ return Promise.reject(); },
|
||||
markModified : ()=>true,
|
||||
save : ()=>Promise.reject(),
|
||||
lock : testLock
|
||||
};
|
||||
|
||||
jest.spyOn(HomebrewModel, 'findOne')
|
||||
.mockImplementationOnce(()=>{
|
||||
return Promise.resolve(testBrew);
|
||||
});
|
||||
jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
|
||||
|
||||
const response = await app
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||
const response = await request
|
||||
.put(`/api/lock/review/remove/${testBrew.shareId}`)
|
||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
|
||||
+269
-567
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -12,624 +12,326 @@ 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';
|
||||
|
||||
const app = express();
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
import api from './homebrew.api.js';
|
||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
|
||||
const { homebrewApi, getBrew, 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 sanitizeFilename from 'sanitize-filename';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import templateFn from '../client/template.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import streamApi from './stream.api.js';
|
||||
import pageRoutes from './page-routes.js';
|
||||
|
||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.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';
|
||||
|
||||
|
||||
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 */);
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`build`));
|
||||
app.use(contentNegotiation);
|
||||
app.use(bodyParser.json({ limit: '25mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(forceSSL);
|
||||
|
||||
import cors from 'cors';
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
export default async function createApp(vite) {
|
||||
const app = express();
|
||||
|
||||
const corsOptions = {
|
||||
origin : (origin, callback)=>{
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isProd = nodeEnv === 'production';
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
|
||||
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'));
|
||||
const sanitizeBrew = (brew, accessType)=>{
|
||||
brew._id = undefined;
|
||||
brew.__v = undefined;
|
||||
if(accessType !== 'edit' && accessType !== 'shareAuthor') {
|
||||
brew.editId = undefined;
|
||||
}
|
||||
},
|
||||
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
credentials : true,
|
||||
};
|
||||
return brew;
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
||||
|
||||
//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'));
|
||||
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);
|
||||
} 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);
|
||||
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'
|
||||
};
|
||||
|
||||
//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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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`
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
});
|
||||
|
||||
//Serve brew metadata
|
||||
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
const { brew } = req;
|
||||
sanitizeBrew(brew, 'share');
|
||||
app.use(homebrewApi);
|
||||
app.use(adminApi(vite));
|
||||
app.use(vaultApi);
|
||||
app.use(streamApi);
|
||||
|
||||
const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
|
||||
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,
|
||||
HomebrewModel,
|
||||
sanitizeBrew,
|
||||
}));
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||
});
|
||||
//serve brew for sharepage rerender
|
||||
app.get('/api/fetch/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res)=>{
|
||||
const { brew } = req;
|
||||
brew.authors.includes(req.account?.username)
|
||||
? sanitizeBrew(brew, 'shareAuthor')
|
||||
: sanitizeBrew(brew, 'share');
|
||||
splitTextStyleAndMetadata(brew);
|
||||
res.json({ brew });
|
||||
}));
|
||||
|
||||
//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)=>{
|
||||
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);});
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', dbCheck, 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);
|
||||
}, {});
|
||||
res.status(200).json(metadata);
|
||||
});
|
||||
|
||||
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
//Serve brew styling
|
||||
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const auth = await GoogleActions.authCheck(req.account, res);
|
||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
//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);
|
||||
|
||||
// If stub matches file from Google, use Google metadata over stub metadata
|
||||
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.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Remaining unstubbed google brews display current user as author
|
||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||
brews = _.concat(brews, googleBrews);
|
||||
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.' });
|
||||
}
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
// Clean up brew data
|
||||
brew.title = brew.title?.trim();
|
||||
brew.description = brew.description?.trim();
|
||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//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.' });
|
||||
}
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
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 from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(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,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
// increase visitor view count, do not include visits by author(s)
|
||||
if(!brew.authors.includes(req.account?.username)){
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Account Page
|
||||
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
if(!req.account) {
|
||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||
const error = new Error('No valid account');
|
||||
error.status = 401;
|
||||
error.HBErrorCode = '50';
|
||||
error.page = data.title;
|
||||
return next(error);
|
||||
};
|
||||
|
||||
let auth;
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return 0;
|
||||
});
|
||||
|
||||
data.accountDetails = {
|
||||
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();
|
||||
}));
|
||||
|
||||
// Local only
|
||||
if(isLocalEnvironment){
|
||||
// Local only
|
||||
if(isLocalEnvironment){
|
||||
// Login
|
||||
app.post('/local/login', (req, res)=>{
|
||||
const username = req.body.username;
|
||||
if(!username) return;
|
||||
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'));
|
||||
|
||||
//Vault Page
|
||||
app.get('/vault', asyncHandler(async(req, res, next)=>{
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'The Vault',
|
||||
description : 'Search for Brews'
|
||||
};
|
||||
return next();
|
||||
}));
|
||||
|
||||
//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 title = req.brew ? req.brew.title : '';
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
|
||||
return res.json(payload);
|
||||
});
|
||||
return 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));
|
||||
};
|
||||
// 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'));
|
||||
|
||||
app.use(async (err, req, res, next)=>{
|
||||
err.originalUrl = req.originalUrl;
|
||||
console.error(err);
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||
|
||||
if(err.originalUrl?.startsWith('/api')) {
|
||||
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') ?? '',
|
||||
developmentStyle : config.get('development_style')
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
const safeProps = JSON.stringify(props).replace(/<(?=\/?script)/ig, '\\u003c');
|
||||
html = html.replace(
|
||||
'<head>',
|
||||
()=>{ //Replace must use a function instead of string, else brews with `$` in contents are treated as replacement patterns
|
||||
return `<head>\n`
|
||||
+ `<script id="props">`
|
||||
+ `window.__INITIAL_PROPS__ = ${safeProps
|
||||
}</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;
|
||||
}
|
||||
res.status(err.status || err.response?.status || 500).send(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('non-API error');
|
||||
const status = err.status || err.code || 500;
|
||||
// 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';
|
||||
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);
|
||||
});
|
||||
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.');
|
||||
}
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
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.');
|
||||
}
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
export default app;
|
||||
return app;
|
||||
}
|
||||
|
||||
+26
-21
@@ -2,27 +2,32 @@ import _ from 'lodash';
|
||||
|
||||
// Default properties for newly-created brews
|
||||
const DEFAULT_BREW = {
|
||||
title : '',
|
||||
text : '',
|
||||
style : undefined,
|
||||
description : '',
|
||||
editId : undefined,
|
||||
shareId : undefined,
|
||||
createdAt : undefined,
|
||||
updatedAt : undefined,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB',
|
||||
authors : [],
|
||||
tags : [],
|
||||
systems : [],
|
||||
lang : 'en',
|
||||
thumbnail : '',
|
||||
views : 0,
|
||||
published : false,
|
||||
pageCount : 1,
|
||||
gDrive : false,
|
||||
trashed : false
|
||||
|
||||
title : '',
|
||||
text : '',
|
||||
style : undefined,
|
||||
description : '',
|
||||
editId : undefined,
|
||||
shareId : undefined,
|
||||
createdAt : undefined,
|
||||
updatedAt : undefined,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB',
|
||||
authors : [],
|
||||
tags : [],
|
||||
lang : 'en',
|
||||
thumbnail : '',
|
||||
views : 0,
|
||||
published : false,
|
||||
pageCount : 1,
|
||||
gDrive : false,
|
||||
trashed : false,
|
||||
bleedSize : { top: '.125in', bottom: '.125in', inner: '.125in', outer: '.125in' },
|
||||
safetySpace : { top: '.25in', bottom: '.25in', outer: '.25in', inner: '.5in' },
|
||||
trimSize : { width: '8.5in', height: '11in' },
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
license : 'None',
|
||||
legalAuthors : ''
|
||||
};
|
||||
// Default values for older brews with missing properties
|
||||
// e.g., missing "renderer" is assumed to be "legacy"
|
||||
|
||||
@@ -4,4 +4,5 @@ export default nconf
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('development', { file: 'config/dev.json' })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
@@ -0,0 +1,94 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import _ from 'lodash';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import config from './config.js';
|
||||
|
||||
// DEBUG
|
||||
const DEBUG = {
|
||||
showEvents : config.get('showStreamEvents'),
|
||||
showSubscriberCount : config.get('showStreamSubscriberCount')
|
||||
};
|
||||
|
||||
// Delays
|
||||
const REPORT_DELAY = config.get('REPORT_DELAY') || 30 * 60 * 1000; //ms
|
||||
const HEARTBEAT_DELAY = config.get('HEARTBEAT_DELAY') || 20 * 1000; //ms
|
||||
|
||||
|
||||
const Stream = new EventEmitter;
|
||||
|
||||
// Create array of stream subscribers
|
||||
let subscribers = [];
|
||||
|
||||
// Listener functions
|
||||
const subscribe = function(shareId, res){
|
||||
const id = nanoid(24); // NanoID is assumed globally unique;
|
||||
const subscriber = {
|
||||
id,
|
||||
shareId,
|
||||
stream : res
|
||||
};
|
||||
|
||||
subscribers.push(subscriber);
|
||||
|
||||
Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId });
|
||||
|
||||
return subscriber.id;
|
||||
};
|
||||
|
||||
const unsubscribe = function(id){
|
||||
subscribers
|
||||
.filter((sub)=>{return sub.id == id;})
|
||||
.forEach((sub)=>{
|
||||
// Unsubscribe event - not currently used
|
||||
// Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId });
|
||||
sub?.stream?.status(200).end();
|
||||
});
|
||||
|
||||
subscribers = subscribers.filter((sub)=>{ return sub.id != id; });
|
||||
};
|
||||
|
||||
// Create heartbeat
|
||||
const heartbeatId = config.get('node_env') != 'test' ?
|
||||
setInterval(()=>{
|
||||
if(DEBUG.showEvents){ console.log('STREAM HEARTBEAT @ ', new Date); };
|
||||
subscribers
|
||||
.forEach((sub)=>{
|
||||
sub?.stream?.write(`:\n\n`);
|
||||
});
|
||||
}, HEARTBEAT_DELAY)
|
||||
:
|
||||
undefined;
|
||||
|
||||
Stream.on('close', ()=>{
|
||||
console.log('STREAM CLOSE');
|
||||
clearInterval(heartbeatId);
|
||||
});
|
||||
|
||||
// Create global sendUpdate listener
|
||||
Stream.on('sendUpdate', (event, data)=>{
|
||||
if(DEBUG.showEvents) console.log('Event:', event, '\nData:', data);
|
||||
subscribers
|
||||
.filter((sub)=>{
|
||||
if(data?.id) return data.id == sub.id;
|
||||
return data.shareId == sub.shareId;
|
||||
})
|
||||
.forEach((sub)=>{
|
||||
sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
});
|
||||
});
|
||||
|
||||
// DEBUG: Report subscriber count periodically
|
||||
if(DEBUG.showSubscriberCount){
|
||||
const reportIntervalId = setInterval(()=>{
|
||||
console.log('Subscriber count:', subscribers.length, ' @ ', new Date);
|
||||
}, REPORT_DELAY);
|
||||
}
|
||||
|
||||
export default {
|
||||
emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions
|
||||
on : (event, listener)=>{return Stream.on(event, listener);},
|
||||
off : (event, listener)=>{return Stream.off(event, listener);},
|
||||
subscribe,
|
||||
unsubscribe
|
||||
};
|
||||
@@ -151,7 +151,6 @@ const GoogleActions = {
|
||||
description : file.description,
|
||||
views : parseInt(file.properties.views),
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : [],
|
||||
lang : file.properties.lang,
|
||||
thumbnail : file.properties.thumbnail,
|
||||
webViewLink : file.webViewLink
|
||||
@@ -298,7 +297,6 @@ const GoogleActions = {
|
||||
text : file.data,
|
||||
|
||||
description : obj.data.description,
|
||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||
authors : [],
|
||||
lang : obj.data.properties.lang,
|
||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||
|
||||
+118
-135
@@ -2,10 +2,9 @@
|
||||
import _ from 'lodash';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import express from 'express';
|
||||
import zlib from 'zlib';
|
||||
import GoogleActions from './googleActions.js';
|
||||
import Markdown from '../shared/markdown.js';
|
||||
import yaml from 'js-yaml';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import * as yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
|
||||
@@ -21,15 +20,29 @@ const router = express.Router();
|
||||
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
|
||||
import Themes from '../themes/themes.json' with { type: 'json' };
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
|
||||
const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
const migrateSystemsToTags = (brew)=>{
|
||||
if(!('systems' in brew) || !Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
}
|
||||
const systemMap = {
|
||||
'5e' : 'system:D&D 5e',
|
||||
'4e' : 'system:D&D 4e',
|
||||
'3.5e' : 'system:D&D 3.5e',
|
||||
'Pathfinder' : 'system:Pathfinder 2e'
|
||||
};
|
||||
const systemTags = brew.systems.map((s)=>systemMap[s]);
|
||||
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
||||
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
};
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
@@ -85,19 +98,17 @@ const api = {
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||
|
||||
if(brews) {
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
|
||||
return userThemes;
|
||||
@@ -147,8 +158,7 @@ const api = {
|
||||
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||
.catch((googleError)=>{
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound')
|
||||
if(googleError.code === 404 || googleError.status === 404)
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
else
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
@@ -167,16 +177,14 @@ const api = {
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
req.brew = stub;
|
||||
req.brew = migrateSystemsToTags(stub);
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
getCSS : async (req, res)=>{
|
||||
const { brew } = req;
|
||||
if(!brew) return res.status(404).send('');
|
||||
splitTextStyleAndMetadata(brew);
|
||||
if(!brew.style) return res.status(404).send('');
|
||||
if(!brew?.style) return res.status(404).send('');
|
||||
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
@@ -184,7 +192,6 @@ const api = {
|
||||
});
|
||||
return res.status(200).send(brew.style);
|
||||
},
|
||||
|
||||
mergeBrewText : (brew)=>{
|
||||
let text = brew.text;
|
||||
if(brew.style !== undefined) {
|
||||
@@ -193,43 +200,32 @@ const api = {
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme', 'columns', 'columnGutter', 'license', 'legalAuthors']);
|
||||
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
|
||||
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
|
||||
metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
|
||||
metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
|
||||
metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
|
||||
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
`${yaml.dump(metadata)}` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
return text;
|
||||
},
|
||||
|
||||
getGoodBrewTitle : (text)=>{
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
const tokens = hbfm.marked.lexer(text);
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
},
|
||||
excludePropsFromUpdate : (brew)=>{
|
||||
// Remove undesired properties
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludePropsFromUpdate : (brew)=>{ //Remove props that should not update/overwrite when editing
|
||||
return _.omit(brew, ['_id', 'views', 'lastViewed']);
|
||||
},
|
||||
excludeGoogleProps : (brew)=>{
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludeGoogleProps : (brew)=>{ //Remove props that should not be saved to Google (only stored to Homebrewery stub)
|
||||
return _.omit(brew, ['version', 'tags', 'systems', 'published', 'authors', 'invitedAuthors', 'owner', 'views', 'thumbnail']);
|
||||
},
|
||||
excludeStubProps : (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
excludeStubProps : (brew)=>{ //Remove props that should not be saved to stub-only brews
|
||||
brew.text = brew.textBin = undefined; // Can't use _.omit because this brew is a Mongoose object
|
||||
return brew;
|
||||
},
|
||||
beforeNewSave : (account, brew)=>{
|
||||
@@ -248,9 +244,7 @@ const api = {
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const newBrew = api.excludeGoogleProps(brew);
|
||||
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, api.excludeGoogleProps(brew));
|
||||
},
|
||||
newBrew : async (req, res)=>{
|
||||
const brew = req.body;
|
||||
@@ -262,7 +256,7 @@ const api = {
|
||||
|
||||
api.beforeNewSave(req.account, brew);
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
let newHomebrew = new HomebrewModel(brew);
|
||||
newHomebrew.editId = nanoid(12);
|
||||
newHomebrew.shareId = nanoid(12);
|
||||
|
||||
@@ -271,15 +265,10 @@ const api = {
|
||||
googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
|
||||
|
||||
if(!googleId) return;
|
||||
api.excludeStubProps(newHomebrew);
|
||||
newHomebrew = api.excludeStubProps(newHomebrew);
|
||||
newHomebrew.googleId = googleId;
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
}
|
||||
|
||||
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err, err.toString(), err.stack);
|
||||
@@ -315,7 +304,6 @@ const api = {
|
||||
});
|
||||
|
||||
currentTheme = req.brew;
|
||||
splitTextStyleAndMetadata(currentTheme);
|
||||
if(!currentTheme.tags.some((tag)=>tag === 'meta:theme' || tag === 'meta:Theme'))
|
||||
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
|
||||
themeName ??= currentTheme.title;
|
||||
@@ -351,10 +339,8 @@ const api = {
|
||||
return res.status(200).send(returnObj);
|
||||
},
|
||||
updateBrew : async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
splitTextStyleAndMetadata(brewFromServer);
|
||||
|
||||
if(brewFromServer?.version !== brewFromClient?.version){
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
@@ -368,22 +354,30 @@ const api = {
|
||||
|
||||
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
let result = [];
|
||||
try {
|
||||
const patches = parsePatch(brewFromClient.patches);
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
||||
if(patchedResult != brewFromClient.text)
|
||||
result = applyPatches(patches, encodeURI(brewFromServer.text));
|
||||
const failedPatchCount = result[1].filter((patchApplied)=>!patchApplied).length; //Count how many patches failed to apply
|
||||
if(failedPatchCount){
|
||||
throw (`Patch failure: ${failedPatchCount}/${result[1].length} did not apply`);
|
||||
}
|
||||
const patchedText = decodeURI(result[0]);
|
||||
if(patchedText !== brewFromClient.text){
|
||||
debugTextMismatch(brewFromClient.text, patchedText, `edit/${brewFromClient.editId}`);
|
||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||
}
|
||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||
} catch (err) {
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
//patches : brewFromClient.patches,
|
||||
// patches : brewFromClient.patches,
|
||||
// result : result,
|
||||
brewId : brewFromClient.editId || 'unknown',
|
||||
error : err
|
||||
});
|
||||
@@ -391,77 +385,68 @@ const api = {
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
brew = migrateSystemsToTags(brew);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
const googleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
};
|
||||
|
||||
brew.googleId = undefined;
|
||||
} else if(!brew.googleId && saveToGoogle) {
|
||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
|
||||
|
||||
if(!brew.googleId) return;
|
||||
} else if(brew.googleId) {
|
||||
// If the google id exists and no other actions are being performed, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
||||
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
if(brew.googleId) {
|
||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||
api.excludeStubProps(brew);
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.updatedAt = new Date();
|
||||
brew.version = (brew.version || 1) + 1;
|
||||
|
||||
brew.updatedAt = new Date();
|
||||
brew.version = (brew.version || 1) + 1;
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||
}
|
||||
|
||||
// define a function to catch our save errors
|
||||
const saveError = (err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||
};
|
||||
let saved;
|
||||
if(!brew._id) {
|
||||
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||
} else {
|
||||
// if the brew does have a stub id, update it using the stub id as the key.
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
saved = await brew.save()
|
||||
.catch(saveError);
|
||||
//=====----- Update to or remove from Google Drive -----=========//
|
||||
const originalGoogleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
|
||||
if(brew.googleId && removeFromGoogle) { // If removing from google, remove the brew's googleId
|
||||
brew.googleId = undefined;
|
||||
}
|
||||
else if(!brew.googleId && saveToGoogle) { // If adding to google, create new google document and record the googleId
|
||||
brew.googleId = await api.newGoogleBrew(req.account, brew, res);
|
||||
if(!brew.googleId) return;
|
||||
}
|
||||
else if(brew.googleId) { // If already on google, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
//=====----- Update to Mongo -----=====//
|
||||
if(brew.googleId) {
|
||||
// If google id exists after above actions, Mongo will store only a stub; exclude non-stub properties from update
|
||||
brew = api.excludeStubProps(brew);
|
||||
}
|
||||
|
||||
if(brew._id) // if the brew has a stub id, fetch and merge into Mongo stub document
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
else // if not, create a new Mongo document
|
||||
brew = new HomebrewModel(brew);
|
||||
|
||||
const saved = await brew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||
});
|
||||
|
||||
if(!saved) return;
|
||||
// Call and wait for afterSave to complete
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
if(originalGoogleId && removeFromGoogle) {
|
||||
const deleted = await api.deleteGoogleBrew(req.account, originalGoogleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
|
||||
if(!deleted) return;
|
||||
}
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
Stream.emit('sendUpdate', 'brewUpdated', { time: new Date, shareId: brew.shareId, version: brew.version });
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
@@ -481,7 +466,7 @@ const api = {
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
}
|
||||
throw(err);
|
||||
throw (err);
|
||||
}
|
||||
|
||||
let brew = req.brew;
|
||||
@@ -509,8 +494,6 @@ const api = {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||
brew.googleId = undefined;
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||
await brew.save()
|
||||
@@ -535,9 +518,9 @@ const api = {
|
||||
router.use(dbCheck);
|
||||
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); //alt endpoint, unused
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); //alt endpoint, unused
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
|
||||
+408
-228
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
import dedent from 'dedent';
|
||||
|
||||
describe('Tests for api', ()=>{
|
||||
let api;
|
||||
@@ -63,7 +64,6 @@ describe('Tests for api', ()=>{
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
lang : 'en',
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
@@ -204,7 +204,6 @@ describe('Tests for api', ()=>{
|
||||
expect(id).toEqual('abcdefghij');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrew', ()=>{
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||
@@ -351,7 +350,6 @@ describe('Tests for api', ()=>{
|
||||
renderer : 'legacy',
|
||||
lang : 'en',
|
||||
shareId : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
@@ -362,7 +360,27 @@ describe('Tests for api', ()=>{
|
||||
style : undefined,
|
||||
trashed : false,
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
views : 0,
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
@@ -382,7 +400,68 @@ describe('Tests for api', ()=>{
|
||||
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||
});
|
||||
});
|
||||
describe('Get CSS', ()=>{
|
||||
it('should return brew style content as CSS text', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
|
||||
expect(res.set).toHaveBeenCalledWith({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/css'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when brew has no style content', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should return 404 when brew does not exist', async ()=>{
|
||||
const testBrew = { };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
describe('mergeBrewText', ()=>{
|
||||
it('should set metadata and no style if it is not present', ()=>{
|
||||
const result = api.mergeBrewText({
|
||||
@@ -390,26 +469,60 @@ describe('Tests for api', ()=>{
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
googleId : '12345',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
expect(result).toEqual(dedent`
|
||||
\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
\`\`\`
|
||||
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
brew`);
|
||||
});
|
||||
|
||||
it('should set metadata and style', ()=>{
|
||||
@@ -419,33 +532,66 @@ brew`);
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
googleId : '12345',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
expect(result).toEqual(dedent`
|
||||
\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
renderer: v3
|
||||
theme: phb
|
||||
columns: '2'
|
||||
columnGutter: .125in
|
||||
license: None
|
||||
legalAuthors: ''
|
||||
bleedSize:
|
||||
top: .125in
|
||||
bottom: .125in
|
||||
inner: .125in
|
||||
outer: .125in
|
||||
safetySpace:
|
||||
top: .25in
|
||||
bottom: .25in
|
||||
outer: .25in
|
||||
inner: .5in
|
||||
trimSize:
|
||||
width: 8.5in
|
||||
height: 11in
|
||||
\`\`\`
|
||||
|
||||
\`\`\`
|
||||
\`\`\`css
|
||||
hello yes i am css
|
||||
\`\`\`
|
||||
|
||||
\`\`\`css
|
||||
hello yes i am css
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
brew`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclusion methods', ()=>{
|
||||
it('excludePropsFromUpdate removes the correct keys', ()=>{
|
||||
const sent = Object.assign({}, googleBrew);
|
||||
@@ -463,7 +609,6 @@ brew`);
|
||||
|
||||
expect(sent).toEqual(googleBrew);
|
||||
expect(result.tags).toBeUndefined();
|
||||
expect(result.systems).toBeUndefined();
|
||||
expect(result.published).toBeUndefined();
|
||||
expect(result.authors).toBeUndefined();
|
||||
expect(result.owner).toBeUndefined();
|
||||
@@ -483,7 +628,6 @@ brew`);
|
||||
expect(result.pageCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeNewSave', ()=>{
|
||||
it('sets the title if none', ()=>{
|
||||
const brew = {
|
||||
@@ -525,7 +669,6 @@ brew`);
|
||||
expect(hbBrew.text).toEqual('merged');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newGoogleBrew', ()=>{
|
||||
it('should call the correct methods', ()=>{
|
||||
api.excludeGoogleProps = jest.fn(()=>'newBrew');
|
||||
@@ -539,7 +682,6 @@ brew`);
|
||||
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newBrew', ()=>{
|
||||
it('should set up a default brew via Homebrew model', async ()=>{
|
||||
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
|
||||
@@ -558,16 +700,42 @@ brew`);
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : expect.objectContaining({}),
|
||||
text : '```metadata\n' +
|
||||
'title: asdf\n' +
|
||||
'bleedSize: {}\n' +
|
||||
'safetySpace: {}\n' +
|
||||
'trimSize: {}' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'asdf',
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
views : 0,
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -605,43 +773,51 @@ brew`);
|
||||
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteGoogleBrew', ()=>{
|
||||
it('should check auth and delete brew', async ()=>{
|
||||
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme bundle', ()=>{
|
||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||
const brews = {
|
||||
@@ -785,7 +961,94 @@ brew`);
|
||||
status : 422 });
|
||||
});
|
||||
});
|
||||
describe('updateBrew', ()=>{
|
||||
it('should return error on version mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1 };
|
||||
const brewFromServer = { version: 1000, text: '' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
it('should return error on hash mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '1234' };
|
||||
const brewFromServer = { version: 1, text: 'test' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
// Commenting this one out for now, since we are no longer throwing this error while we monitor
|
||||
// it('should return error on applying patches', async ()=>{
|
||||
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
|
||||
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
// const req = {
|
||||
// brew : brewFromServer,
|
||||
// body : brewFromClient,
|
||||
// };
|
||||
|
||||
// let err;
|
||||
// try {
|
||||
// await api.updateBrew(req, res);
|
||||
// } catch (e) {
|
||||
// err = e;
|
||||
// }
|
||||
|
||||
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
|
||||
// });
|
||||
|
||||
it('should save brew, no ID', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
|
||||
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
model.save = jest.fn((brew)=>{return brew;});
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient,
|
||||
query : { saveToGoogle: false, removeFromGoogle: false }
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id : '1',
|
||||
description : 'Test Description',
|
||||
hash : '098f6bcd4621d373cade4e832627b4f6',
|
||||
title : 'Test Title',
|
||||
version : 2
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteGoogleBrew', ()=>{
|
||||
it('should check auth and delete brew', async ()=>{
|
||||
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||
});
|
||||
});
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||
@@ -978,8 +1241,7 @@ brew`);
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
expect(saved.googleId).toEqual(undefined);
|
||||
expect(saved.text).toEqual(undefined);
|
||||
expect(saved.textBin).not.toEqual(undefined);
|
||||
expect(saved.text).not.toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||
@@ -1006,68 +1268,7 @@ brew`);
|
||||
expect(saved.googleId).toEqual(brew.googleId);
|
||||
});
|
||||
});
|
||||
describe('Get CSS', ()=>{
|
||||
it('should return brew style content as CSS text', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
|
||||
expect(res.set).toHaveBeenCalledWith({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/css'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when brew has no style content', async ()=>{
|
||||
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should return 404 when brew does not exist', async ()=>{
|
||||
const testBrew = { };
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
await api.getCSS(req, res);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style');
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
describe('Split Text, Style, and Metadata', ()=>{
|
||||
|
||||
it('basic splitting', async ()=>{
|
||||
@@ -1076,7 +1277,6 @@ brew`);
|
||||
'title: title\n' +
|
||||
'description: description\n' +
|
||||
'tags: [ \'tag a\' , \'tag b\' ]\n' +
|
||||
'systems: [ test system ]\n' +
|
||||
'renderer: legacy\n' +
|
||||
'theme: 5ePHB\n' +
|
||||
'lang: en\n' +
|
||||
@@ -1097,8 +1297,6 @@ brew`);
|
||||
// Metadata
|
||||
expect(testBrew.title).toEqual('title');
|
||||
expect(testBrew.description).toEqual('description');
|
||||
expect(testBrew.tags).toEqual(['tag a', 'tag b']);
|
||||
expect(testBrew.systems).toEqual(['test system']);
|
||||
expect(testBrew.renderer).toEqual('legacy');
|
||||
expect(testBrew.theme).toEqual('5ePHB');
|
||||
expect(testBrew.lang).toEqual('en');
|
||||
@@ -1108,96 +1306,78 @@ brew`);
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
|
||||
it('convert tags string to array', async ()=>{
|
||||
it('extended metadata', async ()=>{
|
||||
const testBrew = {
|
||||
text : '```metadata\n' +
|
||||
'tags: tag a\n' +
|
||||
'```\n\n'
|
||||
'title: title\n' +
|
||||
'description: description\n' +
|
||||
'tags: [ \'tag a\' , \'tag b\' ]\n' +
|
||||
'renderer: legacy\n' +
|
||||
'theme: 5ePHB\n' +
|
||||
'lang: en\n' +
|
||||
'bleedSize:\n' +
|
||||
' top: 1.5in\n' +
|
||||
' bottom: 1.5in\n' +
|
||||
' outer: 1.5in\n' +
|
||||
' inner: 1.5in\n' +
|
||||
'safetySpace:\n' +
|
||||
' top: 1.25in\n' +
|
||||
' bottom: 1.25in\n' +
|
||||
' outer: 1.25in\n' +
|
||||
' inner: 1.5in\n' +
|
||||
'trimSize:\n' +
|
||||
' width: 18.5in\n' +
|
||||
' height: 111in\n' +
|
||||
'columns: 12\n' +
|
||||
'columnGutter: 1.125in\n' +
|
||||
'license: AELF\n' +
|
||||
'legalAuthors: Tom Bombadil\n' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'```css\n' +
|
||||
'style\n' +
|
||||
'style\n' +
|
||||
'style\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'text\n'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(testBrew);
|
||||
|
||||
// Metadata
|
||||
expect(testBrew.tags).toEqual(['tag a']);
|
||||
expect(testBrew.title).toEqual('title');
|
||||
expect(testBrew.description).toEqual('description');
|
||||
expect(testBrew.renderer).toEqual('legacy');
|
||||
expect(testBrew.theme).toEqual('5ePHB');
|
||||
expect(testBrew.lang).toEqual('en');
|
||||
// Paper Specfications
|
||||
expect(testBrew.bleedSize.top).toEqual('1.5in');
|
||||
expect(testBrew.bleedSize.bottom).toEqual('1.5in');
|
||||
expect(testBrew.bleedSize.inner).toEqual('1.5in');
|
||||
expect(testBrew.bleedSize.outer).toEqual('1.5in');
|
||||
|
||||
expect(testBrew.safetySpace.top).toEqual('1.25in');
|
||||
expect(testBrew.safetySpace.bottom).toEqual('1.25in');
|
||||
expect(testBrew.safetySpace.inner).toEqual('1.5in');
|
||||
expect(testBrew.safetySpace.outer).toEqual('1.25in');
|
||||
|
||||
expect(testBrew.trimSize.width).toEqual('18.5in');
|
||||
expect(testBrew.trimSize.height).toEqual('111in');
|
||||
|
||||
expect(testBrew.columns).toEqual(12);
|
||||
expect(testBrew.columnGutter).toEqual('1.125in');
|
||||
|
||||
// Extended Metadata
|
||||
expect(testBrew.license).toEqual('AELF');
|
||||
expect(testBrew.legalAuthors).toEqual('Tom Bombadil');
|
||||
|
||||
// Style
|
||||
expect(testBrew.style).toEqual('style\nstyle\nstyle\n');
|
||||
// Text
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBrew', ()=>{
|
||||
it('should return error on version mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1 };
|
||||
const brewFromServer = { version: 1000, text: '' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
it('should return error on hash mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '1234' };
|
||||
const brewFromServer = { version: 1, text: 'test' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
// Commenting this one out for now, since we are no longer throwing this error while we monitor
|
||||
// it('should return error on applying patches', async ()=>{
|
||||
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
|
||||
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
// const req = {
|
||||
// brew : brewFromServer,
|
||||
// body : brewFromClient,
|
||||
// };
|
||||
|
||||
// let err;
|
||||
// try {
|
||||
// await api.updateBrew(req, res);
|
||||
// } catch (e) {
|
||||
// err = e;
|
||||
// }
|
||||
|
||||
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
|
||||
// });
|
||||
|
||||
it('should save brew, no ID', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
|
||||
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
model.save = jest.fn((brew)=>{return brew;});
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient,
|
||||
query : { saveToGoogle: false, removeFromGoogle: false }
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id : '1',
|
||||
description : 'Test Description',
|
||||
hash : '098f6bcd4621d373cade4e832627b4f6',
|
||||
title : 'Test Title',
|
||||
version : 2
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ const HomebrewSchema = mongoose.Schema({
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : { type: [String], index: true },
|
||||
systems : [String],
|
||||
systems : { type: [String], default: undefined },
|
||||
lang : { type: String, default: 'en', index: true },
|
||||
renderer : { type: String, default: '', index: true },
|
||||
authors : { type: [String], index: true },
|
||||
@@ -32,14 +32,21 @@ const HomebrewSchema = mongoose.Schema({
|
||||
lock : { type: Object, index: true }
|
||||
}, { versionKey: false });
|
||||
|
||||
HomebrewSchema.pre('save', function() {
|
||||
if(this.text !== undefined) {
|
||||
this.textBin = zlib.deflateRawSync(this.text);
|
||||
this.text = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
HomebrewSchema.statics.increaseView = async function(query) {
|
||||
const brew = await Homebrew.findOne(query).exec();
|
||||
brew.lastViewed = new Date();
|
||||
brew.views = brew.views + 1;
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
return err;
|
||||
});
|
||||
.catch((err)=>{
|
||||
return err;
|
||||
});
|
||||
return brew;
|
||||
};
|
||||
|
||||
@@ -51,6 +58,7 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
||||
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||
const unzipped = zlib.inflateRawSync(brew.textBin);
|
||||
brew.text = unzipped.toString();
|
||||
brew.textBin = undefined;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// page-routes.js
|
||||
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
import _ from 'lodash';
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import fs from 'fs';
|
||||
|
||||
//==== Middleware Imports ====//
|
||||
import dbCheck from './middleware/dbCheck.js';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
import GoogleActions from './googleActions.js';
|
||||
|
||||
import api from './homebrew.api.js';
|
||||
const { getBrew, getUsersBrewThemes } = api;
|
||||
|
||||
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');
|
||||
|
||||
export default function pageRoutes({
|
||||
defaultMetaTags,
|
||||
HomebrewModel,
|
||||
sanitizeBrew,
|
||||
}) {
|
||||
const app = express.Router();
|
||||
|
||||
//Home page
|
||||
app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
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', dbCheck, 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);
|
||||
});
|
||||
|
||||
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
|
||||
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 stub matches file from Google, use Google metadata over stub metadata
|
||||
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.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Remaining unstubbed google brews display current user as author
|
||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
// Clean up brew data
|
||||
brew.title = brew.title?.trim();
|
||||
brew.description = brew.description?.trim();
|
||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
sanitizeBrew(req.brew, 'edit');
|
||||
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 from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
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,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
// increase visitor view count, do not include visits by author(s)
|
||||
if(!brew.authors.includes(req.account?.username)){
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Account Page
|
||||
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
if(!req.account) {
|
||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||
const error = new Error('No valid account');
|
||||
error.status = 401;
|
||||
error.HBErrorCode = '50';
|
||||
error.page = data.title;
|
||||
return next(error);
|
||||
};
|
||||
|
||||
let auth;
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return 0;
|
||||
});
|
||||
|
||||
data.accountDetails = {
|
||||
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();
|
||||
}));
|
||||
|
||||
//Vault Page
|
||||
app.get('/vault', asyncHandler(async(req, res, next)=>{
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'The Vault',
|
||||
description : 'Search for Brews'
|
||||
};
|
||||
return next();
|
||||
}));
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import express from 'express';
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
|
||||
const app = express.Router();
|
||||
|
||||
// Create Event Stream source for pages to listen to
|
||||
app.get('/api/stream/:id', (req, res)=>{
|
||||
res.writeHead(200, {
|
||||
'Content-Type' : 'text/event-stream',
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Connection' : 'keep-alive',
|
||||
'Content-Encoding' : 'none'
|
||||
});
|
||||
|
||||
const subId = Stream.subscribe(req.params.id, res);
|
||||
|
||||
res.on('close', ()=>{
|
||||
Stream.unsubscribe(subId);
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user