0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-07 20:42:44 +00:00

Update homebrew.api.js

This commit is contained in:
Trevor Buckner
2024-10-11 23:23:56 -04:00
committed by GitHub
parent 1c47d743d6
commit 4859756ef8

View File

@@ -9,30 +9,24 @@ const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const { splitTextStyleAndMetadata } = require('../shared/helpers.js'); const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const rateLimit = require('express-rate-limit');
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js'); const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
const Themes = require('../themes/themes.json'); const Themes = require('../themes/themes.json');
const isStaticTheme = (renderer, themeName)=>{ const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined; return Themes[renderer]?.[themeName] !== undefined;
}; };
// const getTopBrews = (cb) => { // const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews); // cb(brews);
// }); // });
// }; // };
const MAX_TITLE_LENGTH = 100; const MAX_TITLE_LENGTH = 100;
const api = { const api = {
homebrewApi : router, homebrewApi : router,
getId : (req)=>{ getId : (req)=>{
// Set the id and initial potential google id, where the google id is present on the existing brew. // Set the id and initial potential google id, where the google id is present on the existing brew.
let id = req.params.id, googleId = req.body?.googleId; let id = req.params.id, googleId = req.body?.googleId;
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up. // If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
if(id.length > 12) { if(id.length > 12) {
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase) if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
@@ -49,7 +43,6 @@ const api = {
getUsersBrewThemes : async (username)=>{ getUsersBrewThemes : async (username)=>{
if(!username) if(!username)
return {}; return {};
const fields = [ const fields = [
'title', 'title',
'tags', 'tags',
@@ -60,11 +53,8 @@ const api = {
'authors', 'authors',
'renderer' 'renderer'
]; ];
const userThemes = {}; const userThemes = {};
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } }); const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
if(brews) { if(brews) {
for (const brew of brews) { for (const brew of brews) {
userThemes[brew.renderer] ??= {}; userThemes[brew.renderer] ??= {};
@@ -79,7 +69,6 @@ const api = {
}; };
} }
} }
return userThemes; return userThemes;
}, },
getBrew : (accessType, stubOnly = false)=>{ getBrew : (accessType, stubOnly = false)=>{
@@ -87,7 +76,6 @@ const api = {
return async (req, res, next)=>{ return async (req, res, next)=>{
// Get relevant IDs for the brew // Get relevant IDs for the brew
const { id, googleId } = api.getId(req); const { id, googleId } = api.getId(req);
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id }) let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
.catch((err)=>{ .catch((err)=>{
@@ -98,11 +86,9 @@ const api = {
} }
}); });
stub = stub?.toObject(); stub = stub?.toObject();
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// If there is a google id, try to find the google brew // If there is a google id, try to find the google brew
if(!stubOnly && (googleId || stub?.googleId)) { if(!stubOnly && (googleId || stub?.googleId)) {
let googleError; let googleError;
@@ -132,37 +118,31 @@ const api = {
} }
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId }; throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
} }
// If after all of that we still don't have a brew, throw an exception // If after all of that we still don't have a brew, throw an exception
if(!stub && !stubOnly) { if(!stub && !stubOnly) {
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id }; throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
} }
// Clean up brew: fill in missing fields with defaults / fix old invalid values // Clean up brew: fill in missing fields with defaults / fix old invalid values
if(stub) { if(stub) {
stub.tags = stub.tags || undefined; // Clear empty strings stub.tags = stub.tags || undefined; // Clear empty strings
stub.renderer = stub.renderer || undefined; // Clear empty strings stub.renderer = stub.renderer || undefined; // Clear empty strings
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
} }
req.brew = stub ?? {}; req.brew = stub ?? {};
next(); next();
}; };
}, },
getCSS : async (req, res)=>{ getCSS : async (req, res)=>{
const { brew } = req; const { brew } = req;
if(!brew) return res.status(404).send(''); if(!brew) return res.status(404).send('');
splitTextStyleAndMetadata(brew); splitTextStyleAndMetadata(brew);
if(!brew.style) return res.status(404).send(''); if(!brew.style) return res.status(404).send('');
res.set({ res.set({
'Cache-Control' : 'no-cache', 'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css' 'Content-Type' : 'text/css'
}); });
return res.status(200).send(brew.style); return res.status(200).send(brew.style);
}, },
mergeBrewText : (brew)=>{ mergeBrewText : (brew)=>{
let text = brew.text; let text = brew.text;
if(brew.style !== undefined) { if(brew.style !== undefined) {
@@ -211,40 +191,30 @@ const api = {
if(!brew.title) { if(!brew.title) {
brew.title = api.getGoodBrewTitle(brew.text); brew.title = api.getGoodBrewTitle(brew.text);
} }
brew.authors = (account) ? [account.username] : []; brew.authors = (account) ? [account.username] : [];
brew.text = api.mergeBrewText(brew); brew.text = api.mergeBrewText(brew);
_.defaults(brew, DEFAULT_BREW); _.defaults(brew, DEFAULT_BREW);
brew.title = brew.title.trim(); brew.title = brew.title.trim();
brew.description = brew.description.trim(); brew.description = brew.description.trim();
}, },
newGoogleBrew : async (account, brew, res)=>{ newGoogleBrew : async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, res); const oAuth2Client = GoogleActions.authCheck(account, res);
const newBrew = api.excludeGoogleProps(brew); const newBrew = api.excludeGoogleProps(brew);
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew); return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
}, },
newBrew : async (req, res)=>{ newBrew : async (req, res)=>{
const brew = req.body; const brew = req.body;
const { saveToGoogle } = req.query; const { saveToGoogle } = req.query;
delete brew.editId; delete brew.editId;
delete brew.shareId; delete brew.shareId;
delete brew.googleId; delete brew.googleId;
api.beforeNewSave(req.account, brew); api.beforeNewSave(req.account, brew);
const newHomebrew = new HomebrewModel(brew); const newHomebrew = new HomebrewModel(brew);
newHomebrew.editId = nanoid(12); newHomebrew.editId = nanoid(12);
newHomebrew.shareId = nanoid(12); newHomebrew.shareId = nanoid(12);
let googleId, saved; let googleId, saved;
if(saveToGoogle) { if(saveToGoogle) {
googleId = await api.newGoogleBrew(req.account, newHomebrew, res); googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
if(!googleId) return; if(!googleId) return;
api.excludeStubProps(newHomebrew); api.excludeStubProps(newHomebrew);
newHomebrew.googleId = googleId; newHomebrew.googleId = googleId;
@@ -254,7 +224,6 @@ const api = {
// Delete the non-binary text field since it's not needed anymore // Delete the non-binary text field since it's not needed anymore
newHomebrew.text = undefined; newHomebrew.text = undefined;
} }
saved = await newHomebrew.save() saved = await newHomebrew.save()
.catch((err)=>{ .catch((err)=>{
console.error(err, err.toString(), err.stack); console.error(err, err.toString(), err.stack);
@@ -262,21 +231,17 @@ const api = {
}); });
if(!saved) return; if(!saved) return;
saved = saved.toObject(); saved = saved.toObject();
res.status(200).send(saved); res.status(200).send(saved);
}, },
getThemeBundle : async(req, res)=>{ getThemeBundle : async(req, res)=>{
/* getThemeBundle: Collects the theme and all parent themes /* getThemeBundle: Collects the theme and all parent themes
returns an object containing an array of css, and an array of snippets, in render order returns an object containing an array of css, and an array of snippets, in render order
req.params.id : The shareId ( User theme ) or name ( static theme ) req.params.id : The shareId ( User theme ) or name ( static theme )
req.params.renderer : The Markdown renderer used for this theme */ req.params.renderer : The Markdown renderer used for this theme */
req.params.renderer = _.upperFirst(req.params.renderer); req.params.renderer = _.upperFirst(req.params.renderer);
let currentTheme; let currentTheme;
const completeStyles = []; const completeStyles = [];
const completeSnippets = []; const completeSnippets = [];
while (req.params.id) { while (req.params.id) {
//=== User Themes ===// //=== User Themes ===//
if(!isStaticTheme(req.params.renderer, req.params.id)) { if(!isStaticTheme(req.params.renderer, req.params.id)) {
@@ -286,14 +251,11 @@ const api = {
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' }; err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
throw err; throw err;
}); });
currentTheme = req.brew; currentTheme = req.brew;
splitTextStyleAndMetadata(currentTheme); splitTextStyleAndMetadata(currentTheme);
// If there is anything in the snippets or style members, append them to the appropriate array // If there is anything in the snippets or style members, append them to the appropriate array
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets)); if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`); if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
req.params.id = currentTheme.theme; req.params.id = currentTheme.theme;
req.params.renderer = currentTheme.renderer; req.params.renderer = currentTheme.renderer;
} }
@@ -303,17 +265,14 @@ const api = {
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`; const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
completeSnippets.push(localSnippets); completeSnippets.push(localSnippets);
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`); completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme; req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
} }
} }
const returnObj = { const returnObj = {
// Reverse the order of the arrays so they are listed oldest parent to youngest child. // Reverse the order of the arrays so they are listed oldest parent to youngest child.
styles : completeStyles.reverse(), styles : completeStyles.reverse(),
snippets : completeSnippets.reverse() snippets : completeSnippets.reverse()
}; };
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
return res.status(200).send(returnObj); return res.status(200).send(returnObj);
}, },
@@ -326,16 +285,13 @@ const api = {
res.setHeader('Content-Type', 'application/json'); 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.` })); 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(brewFromServer, brewFromClient); let brew = _.assign(brewFromServer, brewFromClient);
const googleId = brew.googleId; const googleId = brew.googleId;
const { saveToGoogle, removeFromGoogle } = req.query; const { saveToGoogle, removeFromGoogle } = req.query;
let afterSave = async ()=>true; let afterSave = async ()=>true;
brew.title = brew.title.trim(); brew.title = brew.title.trim();
brew.description = brew.description.trim() || ''; brew.description = brew.description.trim() || '';
brew.text = api.mergeBrewText(brew); brew.text = api.mergeBrewText(brew);
if(brew.googleId && removeFromGoogle) { 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 // 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 ()=>{ afterSave = async ()=>{
@@ -345,20 +301,17 @@ const api = {
res.status(err?.status || err?.response?.status || 500).send(err.message || err); res.status(err?.status || err?.response?.status || 500).send(err.message || err);
}); });
}; };
brew.googleId = undefined; brew.googleId = undefined;
} else if(!brew.googleId && saveToGoogle) { } 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 // 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); brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
if(!brew.googleId) return; if(!brew.googleId) return;
} else if(brew.googleId) { } else if(brew.googleId) {
// If the google id exists and no other actions are being performed, update the google brew // If the google id exists and no other actions are being performed, update the google brew
const updated = await api.updateGoogleBrew(req.account, api.excludeGoogleProps(brew), res, req); const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew));
if(!updated) return; if(!updated) return;
} }
if(brew.googleId) { 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 // 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); api.excludeStubProps(brew);
@@ -370,12 +323,10 @@ const api = {
} }
brew.updatedAt = new Date(); brew.updatedAt = new Date();
brew.version = (brew.version || 1) + 1; brew.version = (brew.version || 1) + 1;
if(req.account) { if(req.account) {
brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a)); brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
} }
// define a function to catch our save errors // define a function to catch our save errors
const saveError = (err)=>{ const saveError = (err)=>{
console.error(err); console.error(err);
@@ -398,15 +349,6 @@ const api = {
res.status(200).send(saved); res.status(200).send(saved);
}, },
updateGoogleBrew : async (account, brew, res, req)=>{
//let oAuth2Client;
//if(account.googleId)
// oAuth2Client = GoogleActions.authCheck(account, res);
return await GoogleActions.updateGoogleBrew(brew, undefined, req.ip);
},
deleteGoogleBrew : async (account, id, editId, res)=>{ deleteGoogleBrew : async (account, id, editId, res)=>{
const auth = await GoogleActions.authCheck(account, res); const auth = await GoogleActions.authCheck(account, res);
await GoogleActions.deleteGoogleBrew(auth, id, editId); await GoogleActions.deleteGoogleBrew(auth, id, editId);
@@ -425,21 +367,18 @@ const api = {
return next(); return next();
} }
} }
let brew = req.brew; let brew = req.brew;
const { googleId, editId } = brew; const { googleId, editId } = brew;
const account = req.account; const account = req.account;
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username); const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
// If the user is the owner and the file is saved to google, mark the google brew for deletion // If the user is the owner and the file is saved to google, mark the google brew for deletion
const shouldDeleteGoogleBrew = googleId && isOwner; const shouldDeleteGoogleBrew = googleId && isOwner;
if(brew._id) { if(brew._id) {
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
if(account) { if(account) {
// Remove current user as author // Remove current user as author
brew.authors = _.pull(brew.authors, account.username); brew.authors = _.pull(brew.authors, account.username);
} }
if(brew.authors.length === 0) { if(brew.authors.length === 0) {
// Delete brew if there are no authors left // Delete brew if there are no authors left
await HomebrewModel.deleteOne({ _id: brew._id }) await HomebrewModel.deleteOne({ _id: brew._id })
@@ -469,11 +408,9 @@ const api = {
}); });
if(!deleted) return; if(!deleted) return;
} }
res.status(204).send(); res.status(204).send();
} }
}; };
router.use('/api', require('./middleware/check-client-version.js')); router.use('/api', require('./middleware/check-client-version.js'));
router.post('/api', asyncHandler(api.newBrew)); router.post('/api', asyncHandler(api.newBrew));
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
@@ -481,5 +418,4 @@ router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHand
router.delete('/api/:id', asyncHandler(api.deleteBrew)); router.delete('/api/:id', asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', asyncHandler(api.deleteBrew)); router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle)); router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
module.exports = api; module.exports = api;