0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 16:22:44 +00:00

Merge branch 'fixThemesDefault' into api-tests

This commit is contained in:
Charlie Humphreys
2022-12-22 20:09:49 -06:00
20 changed files with 1484 additions and 1208 deletions

View File

@@ -15,6 +15,8 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
const { DEFAULT_BREW } = require('./brewDefaults.js');
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
@@ -29,7 +31,6 @@ const splitTextStyleAndMetadata = (brew)=>{
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
};
const sanitizeBrew = (brew, accessType)=>{
@@ -77,6 +78,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
const defaultMetaTags = {
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
title : 'The Homebrewery',
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
image : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
};
//Robots.txt
app.get('/robots.txt', (req, res)=>{
return res.sendFile(`robots.txt`, { root: process.cwd() });
@@ -89,13 +98,11 @@ app.get('/', (req, res, next)=>{
renderer : 'V3'
},
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'Homepage',
description : 'Homepage',
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
description : 'Homepage'
};
splitTextStyleAndMetadata(req.brew);
return next();
});
@@ -107,13 +114,11 @@ app.get('/legacy', (req, res, next)=>{
renderer : 'legacy'
},
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'Homepage (Legacy)',
description : 'Homepage',
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
description : 'Homepage'
};
splitTextStyleAndMetadata(req.brew);
return next();
});
@@ -125,13 +130,11 @@ app.get('/migrate', (req, res, next)=>{
renderer : 'V3'
},
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'v3 Migration Guide',
description : 'A brief guide to converting Legacy documents to the v3 renderer.',
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
};
splitTextStyleAndMetadata(req.brew);
return next();
});
@@ -144,13 +147,11 @@ app.get('/changelog', async (req, res, next)=>{
renderer : 'V3'
},
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'Changelog',
description : 'Development changelog.',
thumbnail : null,
type : 'website'
description : 'Development changelog.'
};
splitTextStyleAndMetadata(req.brew);
return next();
});
@@ -163,12 +164,9 @@ app.get('/faq', async (req, res, next)=>{
renderer : 'V3'
},
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'FAQ',
description : 'Frequently Asked Questions',
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
description : 'Frequently Asked Questions'
};
splitTextStyleAndMetadata(req.brew);
@@ -194,12 +192,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
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="${fileName}.txt"`
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
});
res.status(200).send(brew.text);
});
@@ -208,12 +213,10 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username);
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : `${req.params.username}'s Collection`,
description : 'View my collection of homebrew on the Homebrewery.',
image : null,
type : 'website' // or 'profile'?
description : 'View my collection of homebrew on the Homebrewery.'
// type : could be 'profile'?
};
const fields = [
@@ -274,13 +277,13 @@ app.get('/user/:username', async (req, res, next)=>{
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || null,
image : req.brew.thumbnail || defaultMetaTags.image,
type : 'article'
};
sanitizeBrew(req.brew, 'edit');
splitTextStyleAndMetadata(req.brew);
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
@@ -292,13 +295,12 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
req.brew.title = `CLONE - ${req.brew.title}`;
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!',
image : null,
type : 'website'
description : 'Start crafting your homebrew on the Homebrewery!'
};
return next();
});
@@ -306,11 +308,10 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req;
req.ogMeta = {
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || null,
image : req.brew.thumbnail || defaultMetaTags.image,
type : 'article'
};
@@ -340,11 +341,11 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
data.title = 'Account Information Page';
let auth;
let files;
let googleCount = [];
if(req.account) {
if(req.account.googleId) {
try {
auth = await GoogleActions.authCheck(req.account, res);
auth = await GoogleActions.authCheck(req.account, res, false);
} catch (e) {
auth = undefined;
console.log('Google auth check failed!');
@@ -352,9 +353,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
}
if(auth.credentials.access_token) {
try {
files = await GoogleActions.listGoogleBrews(auth);
googleCount = await GoogleActions.listGoogleBrews(auth);
} catch (e) {
files = undefined;
googleCount = undefined;
console.log('List Google files failed!');
console.log(e);
}
@@ -362,22 +363,29 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
}
const query = { authors: req.account.username, googleId: { $exists: false } };
const brews = await HomebrewModel.find(query, 'id')
const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{
mongoCount = 0;
console.log(err);
});
data.uiItems = {
username : req.account.username,
issued : req.account.issued,
mongoCount : brews.length,
googleId : Boolean(req.account.googleId),
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
fileCount : files?.length || '-'
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();
}));
@@ -399,6 +407,7 @@ if(isLocalEnvironment){
//Render the page
const templateFn = require('./../client/template.js');
app.use(asyncHandler(async (req, res, next)=>{
const brew = _.defaults(req.brew, DEFAULT_BREW);
// Create configuration object
const configuration = {
local : isLocalEnvironment,
@@ -408,7 +417,7 @@ app.use(asyncHandler(async (req, res, next)=>{
const props = {
version : require('./../package.json').version,
url : req.originalUrl,
brew : req.brew,
brew : brew,
brews : req.brews,
googleBrews : req.googleBrews,
account : req.account,

30
server/brewDefaults.js Normal file
View File

@@ -0,0 +1,30 @@
const _ = require('lodash');
// Default brew properties in most cases
const DEFAULT_BREW = {
text : '',
editId : null,
shareId : null,
title : 'Untitled Brew',
description : '',
renderer : 'V3',
tags : [],
systems : [],
thumbnail : '',
published : false,
pageCount : 1,
theme : '5ePHB'
};
// Default brew properties for loading
const DEFAULT_BREW_LOAD = {};
_.defaults(DEFAULT_BREW_LOAD,
{
renderer : 'legacy',
published : true
},
DEFAULT_BREW);
module.exports = {
DEFAULT_BREW,
DEFAULT_BREW_LOAD
};

View File

@@ -5,24 +5,28 @@ const { nanoid } = require('nanoid');
const token = require('./token.js');
const config = require('./config.js');
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
config.get('service_account');
let serviceAuth;
try {
serviceAuth = google.auth.fromJSON(keys);
serviceAuth.scopes = [
'https://www.googleapis.com/auth/drive'
];
} catch (err) {
console.warn(err);
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
if(!config.get('service_account')){
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
} else {
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
config.get('service_account');
try {
serviceAuth = google.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
} catch (err) {
console.warn(err);
console.log('Please make sure the Google Service Account is set up properly in your config files.');
}
}
google.options({ auth: serviceAuth || config.get('google_api_key') });
const GoogleActions = {
authCheck : (account, res)=>{
authCheck : (account, res, updateTokens=true)=>{
if(!account || !account.googleId){ // If not signed into Google
const err = new Error('Not Signed In');
err.status = 401;
@@ -40,7 +44,7 @@ const GoogleActions = {
refresh_token : account.googleRefreshToken
});
oAuth2Client.on('tokens', (tokens)=>{
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
if(tokens.refresh_token) {
account.googleRefreshToken = tokens.refresh_token;
}
@@ -249,7 +253,6 @@ const GoogleActions = {
text : file.data,
description : obj.data.description,
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [],
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,

View File

@@ -9,6 +9,8 @@ const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid');
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews);
@@ -30,7 +32,7 @@ const api = {
}
return { id, googleId };
},
getBrew : (accessType, fetchGoogle = true)=>{
getBrew : (accessType, stubOnly = false)=>{
// Create middleware with the accessType passed in as part of the scope
return async (req, res, next)=>{
// Get relevant IDs for the brew
@@ -46,12 +48,9 @@ const api = {
}
});
stub = stub?.toObject();
if(accessType === 'edit' && stub?.authors?.length > 0 && !stub?.authors.includes(req.account?.username)) {
throw 'Current logged in user does not have access to this brew.';
}
// If there is a google id, try to find the google brew
if(fetchGoogle && (googleId || stub?.googleId)) {
if(!stubOnly && (googleId || stub?.googleId)) {
let googleError;
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
.catch((err)=>{
@@ -63,16 +62,30 @@ const api = {
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
}
const authorsExist = stub?.authors?.length > 0;
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
throw `The current logged in user does not have editor access to this brew.
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
}
// If after all of that we still don't have a brew, throw an exception
if(!stub && fetchGoogle) {
if(!stub && !stubOnly) {
throw 'Brew not found in Homebrewery database or Google Drive';
}
if(typeof stub?.tags === 'string') {
stub.tags = [];
}
req.brew = stub || {};
// Use _.assignWith instead of _.defaults - does this need to be replicated at all other uses of _.defaults???
_.assignWith(stub, DEFAULT_BREW_LOAD, (objValue, srcValue)=>{
if(typeof objValue === 'boolean') return objValue;
return objValue || srcValue;
});
req.brew = stub;
next();
};
@@ -108,7 +121,7 @@ const api = {
},
excludeGoogleProps : (brew)=>{
const modified = _.clone(brew);
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
for (const prop of propsToExclude) {
delete modified[prop];
}
@@ -128,6 +141,8 @@ const api = {
brew.authors = (account) ? [account.username] : [];
brew.text = api.mergeBrewText(brew);
_.defaults(brew, DEFAULT_BREW);
},
newGoogleBrew : async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, res);
@@ -179,7 +194,13 @@ const api = {
},
updateBrew : async (req, res)=>{
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
let brew = _.assign(req.brew, api.excludePropsFromUpdate(req.body));
const brewFromClient = api.excludePropsFromUpdate(req.body);
if(req.brew.version > brewFromClient.version) {
res.setHeader('Content-Type', 'application/json');
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
}
let brew = _.assign(req.brew, brewFromClient);
const { saveToGoogle, removeFromGoogle } = req.query;
const googleId = brew.googleId;
let afterSave = async ()=>true;
@@ -225,13 +246,11 @@ const api = {
brew.text = undefined;
}
brew.updatedAt = new Date();
brew.version += 1;
if(req.account) {
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
}
// we need the tag type change in both getBrew and here to handle the case where we don't have a stub on which to perform the modification
if(typeof brew.tags === 'string') {
brew.tags = [];
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
}
// define a function to catch our save errors
@@ -243,17 +262,18 @@ const api = {
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);
brew = saved?.toObject();
} else {
// if the brew does have a stub id, update it using the stub id as the key.
saved = await HomebrewModel.updateOne({ _id: brew._id }, brew).catch(saveError);
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
saved = await brew.save()
.catch(saveError);
}
if(!saved) return;
// Call and wait for afterSave to complete
const after = await afterSave();
if(!after) return;
res.status(200).send(brew);
res.status(200).send(saved);
},
deleteGoogleBrew : async (account, id, editId, res)=>{
const auth = await GoogleActions.authCheck(account, res);
@@ -322,8 +342,8 @@ const api = {
};
router.post('/api', asyncHandler(api.newBrew));
router.put('/api/:id', asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.delete('/api/:id', asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));

View File

@@ -12,13 +12,14 @@ const HomebrewSchema = mongoose.Schema({
textBin : { type: Buffer },
pageCount : { type: Number, default: 1 },
description : { type: String, default: '' },
tags : [String],
systems : [String],
renderer : { type: String, default: '' },
authors : [String],
published : { type: Boolean, default: false },
thumbnail : { type: String, default: '' },
description : { type: String, default: '' },
tags : [String],
systems : [String],
renderer : { type: String, default: '' },
authors : [String],
invitedAuthors : [String],
published : { type: Boolean, default: false },
thumbnail : { type: String, default: '' },
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now },
@@ -46,8 +47,6 @@ HomebrewSchema.statics.get = function(query, fields=null){
unzipped = zlib.inflateRawSync(brews[0].textBin);
brews[0].text = unzipped.toString();
}
if(!brews[0].renderer)
brews[0].renderer = 'legacy';
return resolve(brews[0]);
});
});