diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx index 8d677c635..5756b0df1 100644 --- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx +++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx @@ -16,8 +16,8 @@ const BrewItem = createClass({ brew : { title : '', description : '', - - authors : [] + authors : [], + stubbed : true } }; }, @@ -50,7 +50,7 @@ const BrewItem = createClass({ if(!this.props.brew.editId) return; let editLink = this.props.brew.editId; - if(this.props.brew.googleId) { + if(this.props.brew.googleId && !this.props.brew.stubbed) { editLink = this.props.brew.googleId + editLink; } @@ -63,7 +63,7 @@ const BrewItem = createClass({ if(!this.props.brew.shareId) return; let shareLink = this.props.brew.shareId; - if(this.props.brew.googleId) { + if(this.props.brew.googleId && !this.props.brew.stubbed) { shareLink = this.props.brew.googleId + shareLink; } @@ -76,7 +76,7 @@ const BrewItem = createClass({ if(!this.props.brew.shareId) return; let shareLink = this.props.brew.shareId; - if(this.props.brew.googleId) { + if(this.props.brew.googleId && !this.props.brew.stubbed) { shareLink = this.props.brew.googleId + shareLink; } @@ -86,7 +86,7 @@ const BrewItem = createClass({ }, renderGoogleDriveIcon : function(){ - if(!this.props.brew.gDrive) return; + if(!this.props.brew.googleId) return; return googleDriveIcon @@ -104,8 +104,8 @@ const BrewItem = createClass({
- - {brew.authors.join(', ')} + + {brew.authors?.join(', ')}
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index a0a262038..f8ac70a5d 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -200,7 +200,7 @@ const EditPage = createClass({ const brew = this.state.brew; brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; - const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`; + const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request .put(`/api/update/${brew.editId}${params}`) .send(brew) @@ -210,9 +210,7 @@ const EditPage = createClass({ }); this.savedBrew = res.body; - if(transfer) { - history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`); - } + history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); this.setState((prevState)=>({ brew : _.merge({}, prevState.brew, { @@ -340,7 +338,7 @@ const EditPage = createClass({ }, processShareId : function() { - return this.state.brew.googleId ? + return this.state.brew.googleId && !this.state.brew.stubbed ? this.state.brew.googleId + this.state.brew.shareId : this.state.brew.shareId; }, diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index cb8378bdd..ae57039b5 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -162,7 +162,7 @@ const NewPage = createClass({ brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; const res = await request - .post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`) + .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`) .send(brew) .catch((err)=>{ console.log(err); @@ -174,7 +174,7 @@ const NewPage = createClass({ localStorage.removeItem(BREWKEY); localStorage.removeItem(STYLEKEY); localStorage.removeItem(METAKEY); - window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`; + window.location = `/edit/${brew.editId}`; }, renderSaveButton : function(){ diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 372dbbc2c..c983512ad 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -49,7 +49,7 @@ const SharePage = createClass({ }, processShareId : function() { - return this.props.brew.googleId ? + return this.props.brew.googleId && !this.props.brew.stubbed ? this.props.brew.googleId + this.props.brew.shareId : this.props.brew.shareId; }, diff --git a/server/app.js b/server/app.js index dc7b388da..fcb00b29c 100644 --- a/server/app.js +++ b/server/app.js @@ -9,47 +9,12 @@ const yaml = require('js-yaml'); const app = express(); const config = require('./config.js'); -const homebrewApi = require('./homebrew.api.js'); +const { homebrewApi, getBrew } = require('./homebrew.api.js'); const GoogleActions = require('./googleActions.js'); const serveCompressedStaticAssets = require('./static-assets.mv.js'); const sanitizeFilename = require('sanitize-filename'); const asyncHandler = require('express-async-handler'); -const brewAccessTypes = ['edit', 'share', 'raw']; - -//Get the brew object from the HB database or Google Drive -const getBrewFromId = asyncHandler(async (id, accessType)=>{ - if(!brewAccessTypes.includes(accessType)) - throw ('Invalid Access Type when getting brew'); - let brew; - if(id.length > 12) { - const googleId = id.slice(0, -12); - id = id.slice(-12); - brew = await GoogleActions.getGoogleBrew(googleId, id, accessType); - } else { - brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id }); - brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object - } - - brew = sanitizeBrew(brew, accessType === 'edit' ? false : true); - //Split brew.text into text and style - //unless the Access Type is RAW, in which case return immediately - if(accessType == 'raw') { - return brew; - } - splitTextStyleAndMetadata(brew); - return brew; -}); - -const sanitizeBrew = (brew, full=false)=>{ - delete brew._id; - delete brew.__v; - if(full){ - delete brew.editId; - } - return brew; -}; - const splitTextStyleAndMetadata = (brew)=>{ brew.text = brew.text.replaceAll('\r\n', '\n'); if(brew.text.startsWith('```metadata')) { @@ -66,6 +31,15 @@ const splitTextStyleAndMetadata = (brew)=>{ } }; +const sanitizeBrew = (brew, accessType)=>{ + brew._id = undefined; + brew.__v = undefined; + if(accessType !== 'edit'){ + brew.editId = undefined; + } + return brew; +}; + app.use('/', serveCompressedStaticAssets(`build`)); //app.use(express.static(`${__dirname}/build`)); @@ -108,63 +82,58 @@ app.get('/robots.txt', (req, res)=>{ }); //Home page -app.get('/', async (req, res, next)=>{ - const brew = { +app.get('/', (req, res, next)=>{ + req.brew = { text : welcomeText }; - req.brew = brew; return next(); }); //Home page v3 -app.get('/v3_preview', async (req, res, next)=>{ - const brew = { +app.get('/v3_preview', (req, res, next)=>{ + req.brew = { text : welcomeTextV3, renderer : 'V3' }; - splitTextStyleAndMetadata(brew); - req.brew = brew; + splitTextStyleAndMetadata(req.brew); return next(); }); //Legacy/Other Document -> v3 Migration Guide -app.get('/migrate', async (req, res, next)=>{ - const brew = { +app.get('/migrate', (req, res, next)=>{ + req.brew = { text : migrateText, renderer : 'V3' }; - splitTextStyleAndMetadata(brew); - req.brew = brew; + splitTextStyleAndMetadata(req.brew); return next(); }); //Changelog page app.get('/changelog', async (req, res, next)=>{ - const brew = { + req.brew = { title : 'Changelog', text : changelogText, renderer : 'V3' }; - splitTextStyleAndMetadata(brew); - req.brew = brew; + splitTextStyleAndMetadata(req.brew); return next(); }); //FAQ page app.get('/faq', async (req, res, next)=>{ - const brew = { + req.brew = { title : 'FAQ', text : faqText, renderer : 'V3' }; - splitTextStyleAndMetadata(brew); - req.brew = brew; + splitTextStyleAndMetadata(req.brew); return next(); }); //Source page -app.get('/source/:id', asyncHandler(async (req, res)=>{ - const brew = await getBrewFromId(req.params.id, 'raw'); +app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; const replaceStrings = { '&': '&', '<': '<', '>': '>' }; let text = brew.text; @@ -173,11 +142,12 @@ app.get('/source/:id', asyncHandler(async (req, res)=>{ } text = `
${text}
`; res.status(200).send(text); -})); +}); //Download brew source page -app.get('/download/:id', asyncHandler(async (req, res)=>{ - const brew = await getBrewFromId(req.params.id, 'raw'); +app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; + sanitizeBrew(brew, 'share'); const prefix = 'HB - '; let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); @@ -188,13 +158,14 @@ app.get('/download/:id', asyncHandler(async (req, res)=>{ 'Content-Disposition' : `attachment; filename="${fileName}.txt"` }); res.status(200).send(brew.text); -})); +}); //User Page app.get('/user/:username', async (req, res, next)=>{ const ownAccount = req.account && (req.account.username == req.params.username); const fields = [ + 'googleId', 'title', 'pageCount', 'description', @@ -220,58 +191,71 @@ app.get('/user/:username', async (req, res, next)=>{ console.error(err); }); - if(googleBrews) { + if(googleBrews && googleBrews.length > 0) { + for (const brew of brews.filter((brew)=>brew.googleId)) { + const match = googleBrews.findIndex((b)=>b.editId === brew.editId); + if(match !== -1) { + brew.googleId = googleBrews[match].googleId; + brew.stubbed = true; + brew.pageCount = googleBrews[match].pageCount; + brew.renderer = googleBrews[match].renderer; + brew.version = googleBrews[match].version; + googleBrews.splice(match, 1); + } + } + googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] })); brews = _.concat(brews, googleBrews); } } req.brews = _.map(brews, (brew)=>{ - return sanitizeBrew(brew, !ownAccount); + return sanitizeBrew(brew, ownAccount ? 'edit' : 'share'); }); return next(); }); //Edit Page -app.get('/edit/:id', asyncHandler(async (req, res, next)=>{ +app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{ + req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; + 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. - const brew = await getBrewFromId(req.params.id, 'edit'); - req.brew = brew; return next(); -})); +}); //New Page -app.get('/new/:id', asyncHandler(async (req, res, next)=>{ - const brew = await getBrewFromId(req.params.id, 'share'); - brew.title = `CLONE - ${brew.title}`; - req.brew = brew; +app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{ + sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + req.brew.title = `CLONE - ${brew.title}`; return next(); -})); +}); //Share Page -app.get('/share/:id', asyncHandler(async (req, res, next)=>{ - const brew = await getBrewFromId(req.params.id, 'share'); +app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ + const { brew } = req; - if(req.params.id.length > 12) { + if(req.params.id.length > 12 && !brew._id) { const googleId = req.params.id.slice(0, -12); const shareId = req.params.id.slice(-12); await GoogleActions.increaseView(googleId, shareId, 'share', brew) - .catch((err)=>{next(err);}); + .catch((err)=>{next(err);}); } else { await HomebrewModel.increaseView({ shareId: brew.shareId }); } - - req.brew = brew; + sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); return next(); })); //Print Page -app.get('/print/:id', asyncHandler(async (req, res, next)=>{ - const brew = await getBrewFromId(req.params.id, 'share'); - req.brew = brew; - return next(); -})); +app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{ + sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + next(); +}); const nodeEnv = config.get('node_env'); const isLocalEnvironment = config.get('local_environments').includes(nodeEnv); @@ -291,7 +275,7 @@ if(isLocalEnvironment){ //Render the page const templateFn = require('./../client/template.js'); -app.use((req, res)=>{ +app.use(asyncHandler(async (req, res, next)=>{ // Create configuration object const configuration = { local : isLocalEnvironment, @@ -309,13 +293,14 @@ app.use((req, res)=>{ config : configuration }; const title = req.brew ? req.brew.title : ''; - templateFn('homebrew', title, props) - .then((page)=>{ res.send(page); }) - .catch((err)=>{ - console.log(err); - return res.sendStatus(500); - }); -}); + const page = await templateFn('homebrew', title, props) + .catch((err)=>{ + console.log(err); + return res.sendStatus(500); + }); + if(!page) return; + res.send(page); +})); //v=====----- Error-Handling Middleware -----=====v// //Format Errors so all fields will be sent @@ -339,6 +324,13 @@ app.use((err, req, res, next)=>{ console.error(err); res.status(status).send(getPureError(err)); }); + +app.use((req, res)=>{ + if(!res.headersSent) { + console.error('Headers have not been sent, responding with a server error.', req.url); + res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.'); + } +}); //^=====--------------------------------------=====^// module.exports = { diff --git a/server/googleActions.js b/server/googleActions.js index 89691e0d8..5d9e43b71 100644 --- a/server/googleActions.js +++ b/server/googleActions.js @@ -143,12 +143,11 @@ const GoogleActions = { description : `${brew.description}`, properties : { title : brew.title, - published : brew.published, - version : brew.version, - renderer : brew.renderer, - tags : brew.tags, + shareId : brew.shareId || nanoid(12), + editId : brew.editId || nanoid(12), pageCount : brew.pageCount, - systems : brew.systems.join(), + renderer : brew.renderer || 'legacy', + isStubbed : true, thumbnail : brew.thumbnail } }, @@ -161,10 +160,9 @@ const GoogleActions = { console.log('Error saving to google'); console.error(err); throw (err); - //return res.status(500).send('Error while saving'); }); - return (brew); + return true; }, newGoogleBrew : async (auth, brew)=>{ @@ -178,17 +176,18 @@ const GoogleActions = { const folderId = await GoogleActions.getGoogleFolder(auth); const fileMetadata = { - 'name' : `${brew.title}.txt`, - 'description' : `${brew.description}`, - 'parents' : [folderId], - 'properties' : { //AppProperties is not accessible - 'shareId' : brew.shareId || nanoid(12), - 'editId' : brew.editId || nanoid(12), - 'title' : brew.title, - 'views' : '0', - 'pageCount' : brew.pageCount, - 'renderer' : brew.renderer || 'legacy', - 'thumbnail' : brew.thumbnail || '' + name : `${brew.title}.txt`, + description : `${brew.description}`, + parents : [folderId], + properties : { //AppProperties is not accessible + shareId : brew.shareId || nanoid(12), + editId : brew.editId || nanoid(12), + title : brew.title, + pageCount : brew.pageCount, + renderer : brew.renderer || 'legacy', + isStubbed : true, + version : 1, + thumbnail : brew.thumbnail || '' } }; @@ -215,26 +214,7 @@ const GoogleActions = { console.error(err); }); - const newHomebrew = { - text : brew.text, - shareId : fileMetadata.properties.shareId, - editId : fileMetadata.properties.editId, - createdAt : new Date(), - updatedAt : new Date(), - gDrive : true, - googleId : obj.data.id, - pageCount : fileMetadata.properties.pageCount, - - title : brew.title, - description : brew.description, - tags : '', - published : brew.published, - renderer : brew.renderer, - authors : [], - systems : [] - }; - - return newHomebrew; + return obj.data.id; }, getGoogleBrew : async (id, accessId, accessType)=>{ @@ -247,7 +227,6 @@ const GoogleActions = { .catch((err)=>{ console.log('Error loading from Google'); throw (err); - return; }); if(obj) { @@ -257,9 +236,7 @@ const GoogleActions = { throw ('Share ID does not match'); } - const serviceDrive = google.drive({ version: 'v3' }); - - const file = await serviceDrive.files.get({ + const file = await drive.files.get({ fileId : id, fields : 'description, properties', alt : 'media' @@ -276,7 +253,7 @@ const GoogleActions = { text : file.data, description : obj.data.description, - tags : obj.data.properties.tags ? obj.data.properties.tags : '', + 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, @@ -291,7 +268,6 @@ const GoogleActions = { renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy', thumbnail : obj.data.properties.thumbnail || '', - gDrive : true, googleId : id }; @@ -299,14 +275,11 @@ const GoogleActions = { } }, - deleteGoogleBrew : async (auth, id)=>{ + deleteGoogleBrew : async (auth, id, accessId)=>{ const drive = google.drive({ version: 'v3', auth }); - const googleId = id.slice(0, -12); - const accessId = id.slice(-12); - const obj = await drive.files.get({ - fileId : googleId, + fileId : id, fields : 'properties' }) .catch((err)=>{ @@ -315,11 +288,11 @@ const GoogleActions = { }); if(obj && obj.data.properties.editId != accessId) { - throw ('Not authorized to delete this Google brew'); + throw { status: 403, message: 'Not authorized to delete this Google brew' }; } await drive.files.update({ - fileId : googleId, + fileId : id, resource : { trashed: true } }) .catch((err)=>{ diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 4415c948b..4af232b4c 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ const _ = require('lodash'); const HomebrewModel = require('./homebrew.model.js').model; const router = require('express').Router(); @@ -6,6 +7,7 @@ const GoogleActions = require('./googleActions.js'); const Markdown = require('../shared/naturalcrit/markdown.js'); const yaml = require('js-yaml'); const asyncHandler = require('express-async-handler'); +const { nanoid } = require('nanoid'); // const getTopBrews = (cb) => { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { @@ -13,6 +15,53 @@ const asyncHandler = require('express-async-handler'); // }); // }; +const getBrew = (accessType)=>{ + // Create middleware with the accessType passed in as part of the scope + return async (req, res, next)=>{ + // 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; + + // 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) { + googleId = id.slice(0, -12); + id = id.slice(-12); + } + // 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 }) + .catch((err)=>{ + if(googleId) { + console.warn(`Unable to find document stub for ${accessType}Id ${id}`); + } else { + console.warn(err); + } + }); + stub = stub?.toObject(); + + // If there is a google id, try to find the google brew + if(googleId || stub?.googleId) { + let googleError; + const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType) + .catch((err)=>{ + console.warn(err); + googleError = err; + }); + // If we can't find the google brew and there is a google id for the brew, throw an error. + if(!googleBrew) throw googleError; + // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew + stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew; + } + + // If after all of that we still don't have a brew, throw an exception + if(!stub) { + throw 'Brew not found in Homebrewery database or Google Drive'; + } + + req.brew = stub; + + next(); + }; +}; + const mergeBrewText = (brew)=>{ let text = brew.text; if(brew.style !== undefined) { @@ -33,15 +82,33 @@ const MAX_TITLE_LENGTH = 100; const getGoodBrewTitle = (text)=>{ const tokens = Markdown.marked.lexer(text); - return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title') + return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title') .slice(0, MAX_TITLE_LENGTH); }; const excludePropsFromUpdate = (brew)=>{ // Remove undesired properties - const propsToExclude = ['views', 'lastViewed']; + const modified = _.clone(brew); + const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId']; for (const prop of propsToExclude) { - delete brew[prop]; + delete modified[prop]; + } + return modified; +}; + +const excludeGoogleProps = (brew)=>{ + const modified = _.clone(brew); + const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views']; + for (const prop of propsToExclude) { + delete modified[prop]; + } + return modified; +}; + +const excludeStubProps = (brew)=>{ + const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version']; + for (const prop of propsToExclude) { + brew[prop] = undefined; } return brew; }; @@ -55,33 +122,17 @@ const beforeNewSave = (account, brew)=>{ brew.text = mergeBrewText(brew); }; -const newLocalBrew = async (brew)=>{ - const newHomebrew = new HomebrewModel(brew); - // 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; - - let saved = await newHomebrew.save() - .catch((err)=>{ - console.error(err, err.toString(), err.stack); - throw `Error while creating new brew, ${err.toString()}`; - }); - - saved = saved.toObject(); - saved.gDrive = false; - return saved; -}; - const newGoogleBrew = async (account, brew, res)=>{ const oAuth2Client = GoogleActions.authCheck(account, res); - return await GoogleActions.newGoogleBrew(oAuth2Client, brew); + const newBrew = excludeGoogleProps(brew); + + return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew); }; const newBrew = async (req, res)=>{ const brew = req.body; - const { transferToGoogle } = req.query; + const { saveToGoogle } = req.query; delete brew.editId; delete brew.shareId; @@ -89,148 +140,179 @@ const newBrew = async (req, res)=>{ beforeNewSave(req.account, brew); - let saved; - if(transferToGoogle) { - saved = await newGoogleBrew(req.account, brew, res) + const newHomebrew = new HomebrewModel(brew); + newHomebrew.editId = nanoid(12); + newHomebrew.shareId = nanoid(12); + + let googleId, saved; + if(saveToGoogle) { + googleId = await newGoogleBrew(req.account, newHomebrew, res) .catch((err)=>{ - res.status(err.status || err.response.status).send(err.message || err); + console.error(err); + res.status(err?.status || err?.response?.status || 500).send(err?.message || err); }); + if(!googleId) return; + excludeStubProps(newHomebrew); + newHomebrew.googleId = googleId; } else { - saved = await newLocalBrew(brew) - .catch((err)=>{ - res.status(500).send(err); - }); + // 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); + throw `Error while creating new brew, ${err.toString()}`; + }); if(!saved) return; - return res.status(200).send(saved); + saved = saved.toObject(); + + res.status(200).send(saved); }; const updateBrew = async (req, res)=>{ - let brew = excludePropsFromUpdate(req.body); - const { transferToGoogle, transferFromGoogle } = req.query; + // 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, excludePropsFromUpdate(req.body)); + const { saveToGoogle, removeFromGoogle } = req.query; + const googleId = brew.googleId; + let afterSave = async ()=>true; - let saved; - if(brew.googleId && transferFromGoogle) { - beforeNewSave(req.account, brew); + brew.text = mergeBrewText(brew); - saved = await newLocalBrew(brew) - .catch((err)=>{ - console.error(err); - res.status(500).send(err); - }); - if(!saved) return; + 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 deleteGoogleBrew(req.account, googleId, brew.editId, res) + .catch((err)=>{ + console.error(err); + res.status(err?.status || err?.response?.status || 500).send(err.message || err); + }); + }; - await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res) + 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 newGoogleBrew(req.account, excludeGoogleProps(brew), res) .catch((err)=>{ console.error(err); res.status(err.status || err.response.status).send(err.message || err); }); - } else if(!brew.googleId && transferToGoogle) { - saved = await newGoogleBrew(req.account, brew, res) - .catch((err)=>{ - console.error(err); - res.status(err.status || err.response.status).send(err.message || err); - }); - if(!saved) return; - - await deleteLocalBrew(req.account, brew.editId) - .catch((err)=>{ - console.error(err); - res.status(err.status).send(err.message); - }); + if(!brew.googleId) return; } else if(brew.googleId) { - brew.text = mergeBrewText(brew); - - saved = await GoogleActions.updateGoogleBrew(brew) + // If the google id exists and no other actions are being performed, update the google brew + const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew)) .catch((err)=>{ console.error(err); - res.status(err.response?.status || 500).send(err); + res.status(err?.response?.status || 500).send(err); }); + 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 + excludeStubProps(brew); } else { - const dbBrew = await HomebrewModel.get({ editId: req.params.id }) - .catch((err)=>{ - console.error(err); - return res.status(500).send('Error while saving'); - }); - - brew = _.merge(dbBrew, brew); - brew.text = mergeBrewText(brew); - // 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(); - - if(req.account) { - brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); - } - - brew.markModified('authors'); - brew.markModified('systems'); - - saved = await brew.save(); } + brew.updatedAt = new Date(); + + if(req.account) { + brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); + } + + // Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it + brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); + + if(!brew.markModified) { + // If it wasn't in the database, create a new db brew + brew = new HomebrewModel(brew); + } + + brew.markModified('authors'); + brew.markModified('systems'); + + // Save the database 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(!res.headersSent) return res.status(200).send(saved); + res.status(200).send(saved); }; -const deleteBrew = async (req, res)=>{ - if(req.params.id.length > 12) { - const deleted = await deleteGoogleBrew(req.account, req.params.id, res) - .catch((err)=>{ - res.status(500).send(err); - }); - if(deleted) return res.status(200).send(); - } else { - const deleted = await deleteLocalBrew(req.account, req.params.id) - .catch((err)=>{ - res.status(err.status).send(err.message); - }); - if(deleted) return res.status(200).send(deleted); - return res.status(200).send(); - } -}; - -const deleteLocalBrew = async (account, id)=>{ - const brew = await HomebrewModel.findOne({ editId: id }); - if(!brew) { - throw { status: 404, message: 'Can not find homebrew with that id' }; - } - - if(account) { - // Remove current user as author - brew.authors = _.pull(brew.authors, account.username); - brew.markModified('authors'); - } - - if(brew.authors.length === 0) { - // Delete brew if there are no authors left - await brew.remove() - .catch((err)=>{ - console.error(err); - throw { status: 500, message: 'Error while removing' }; - }); - } else { - // Otherwise, save the brew with updated author list - return await brew.save() - .catch((err)=>{ - throw { status: 500, message: err }; - }); - } -}; - -const deleteGoogleBrew = async (account, id, res)=>{ +const deleteGoogleBrew = async (account, id, editId, res)=>{ const auth = await GoogleActions.authCheck(account, res); - await GoogleActions.deleteGoogleBrew(auth, id); + await GoogleActions.deleteGoogleBrew(auth, id, editId); return true; }; -router.post('/api', asyncHandler(newBrew)); -router.put('/api/:id', asyncHandler(updateBrew)); -router.put('/api/update/:id', asyncHandler(updateBrew)); -router.delete('/api/:id', asyncHandler(deleteBrew)); -router.get('/api/remove/:id', asyncHandler(deleteBrew)); +const deleteBrew = async (req, res)=>{ + let brew = req.brew; + const { googleId, editId } = brew; + const account = req.account; + 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 + const shouldDeleteGoogleBrew = googleId && isOwner; -module.exports = router; + if(brew._id) { + brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); + if(account) { + // Remove current user as author + brew.authors = _.pull(brew.authors, account.username); + brew.markModified('authors'); + } + + if(brew.authors.length === 0) { + // Delete brew if there are no authors left + await brew.remove() + .catch((err)=>{ + console.error(err); + throw { status: 500, message: 'Error while removing' }; + }); + } else { + 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; + } + + // Otherwise, save the brew with updated author list + await brew.save() + .catch((err)=>{ + throw { status: 500, message: err }; + }); + } + } + if(shouldDeleteGoogleBrew) { + const deleted = await deleteGoogleBrew(account, googleId, editId, res) + .catch((err)=>{ + console.error(err); + res.status(500).send(err); + }); + if(!deleted) return; + } + + res.status(204).send(); +}; + +router.post('/api', asyncHandler(newBrew)); +router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew)); +router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew)); +router.delete('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew)); +router.get('/api/remove/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew)); + +module.exports = { + homebrewApi : router, + getBrew +}; diff --git a/server/homebrew.model.js b/server/homebrew.model.js index d0692f0a9..db0669e42 100644 --- a/server/homebrew.model.js +++ b/server/homebrew.model.js @@ -6,6 +6,7 @@ const zlib = require('zlib'); const HomebrewSchema = mongoose.Schema({ shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, + googleId : { type: String }, title : { type: String, default: '' }, text : { type: String, default: '' }, textBin : { type: Buffer },