0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-10 02:42:43 +00:00

Merge pull request #2114 from jeddai/google-document-stubs

Google Drive document stubs
This commit is contained in:
Trevor Buckner
2022-06-09 00:37:49 -04:00
committed by GitHub
8 changed files with 336 additions and 290 deletions

View File

@@ -16,8 +16,8 @@ const BrewItem = createClass({
brew : { brew : {
title : '', title : '',
description : '', description : '',
authors : [],
authors : [] stubbed : true
} }
}; };
}, },
@@ -50,7 +50,7 @@ const BrewItem = createClass({
if(!this.props.brew.editId) return; if(!this.props.brew.editId) return;
let editLink = this.props.brew.editId; 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; editLink = this.props.brew.googleId + editLink;
} }
@@ -63,7 +63,7 @@ const BrewItem = createClass({
if(!this.props.brew.shareId) return; if(!this.props.brew.shareId) return;
let shareLink = this.props.brew.shareId; 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; shareLink = this.props.brew.googleId + shareLink;
} }
@@ -76,7 +76,7 @@ const BrewItem = createClass({
if(!this.props.brew.shareId) return; if(!this.props.brew.shareId) return;
let shareLink = this.props.brew.shareId; 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; shareLink = this.props.brew.googleId + shareLink;
} }
@@ -86,7 +86,7 @@ const BrewItem = createClass({
}, },
renderGoogleDriveIcon : function(){ renderGoogleDriveIcon : function(){
if(!this.props.brew.gDrive) return; if(!this.props.brew.googleId) return;
return <span> return <span>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' /> <img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
@@ -104,8 +104,8 @@ const BrewItem = createClass({
</div> </div>
<hr /> <hr />
<div className='info'> <div className='info'>
<span title={`Authors:\n${brew.authors.join('\n')}`}> <span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors.join(', ')} <i className='fas fa-user'/> {brew.authors?.join(', ')}
</span> </span>
<br /> <br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}> <span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

@@ -200,7 +200,7 @@ const EditPage = createClass({
const brew = this.state.brew; const brew = this.state.brew;
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; 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 const res = await request
.put(`/api/update/${brew.editId}${params}`) .put(`/api/update/${brew.editId}${params}`)
.send(brew) .send(brew)
@@ -210,9 +210,7 @@ const EditPage = createClass({
}); });
this.savedBrew = res.body; this.savedBrew = res.body;
if(transfer) { history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
}
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { brew : _.merge({}, prevState.brew, {
@@ -340,7 +338,7 @@ const EditPage = createClass({
}, },
processShareId : function() { 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.googleId + this.state.brew.shareId :
this.state.brew.shareId; this.state.brew.shareId;
}, },

View File

@@ -162,7 +162,7 @@ const NewPage = createClass({
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request const res = await request
.post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`) .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew) .send(brew)
.catch((err)=>{ .catch((err)=>{
console.log(err); console.log(err);
@@ -174,7 +174,7 @@ const NewPage = createClass({
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`; window.location = `/edit/${brew.editId}`;
}, },
renderSaveButton : function(){ renderSaveButton : function(){

View File

@@ -49,7 +49,7 @@ const SharePage = createClass({
}, },
processShareId : function() { 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.googleId + this.props.brew.shareId :
this.props.brew.shareId; this.props.brew.shareId;
}, },

View File

@@ -9,47 +9,12 @@ const yaml = require('js-yaml');
const app = express(); const app = express();
const config = require('./config.js'); const config = require('./config.js');
const homebrewApi = require('./homebrew.api.js'); const { homebrewApi, getBrew } = require('./homebrew.api.js');
const GoogleActions = require('./googleActions.js'); const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js'); const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler'); 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)=>{ const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n'); brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) { 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('/', serveCompressedStaticAssets(`build`));
//app.use(express.static(`${__dirname}/build`)); //app.use(express.static(`${__dirname}/build`));
@@ -108,63 +82,58 @@ app.get('/robots.txt', (req, res)=>{
}); });
//Home page //Home page
app.get('/', async (req, res, next)=>{ app.get('/', (req, res, next)=>{
const brew = { req.brew = {
text : welcomeText text : welcomeText
}; };
req.brew = brew;
return next(); return next();
}); });
//Home page v3 //Home page v3
app.get('/v3_preview', async (req, res, next)=>{ app.get('/v3_preview', (req, res, next)=>{
const brew = { req.brew = {
text : welcomeTextV3, text : welcomeTextV3,
renderer : 'V3' renderer : 'V3'
}; };
splitTextStyleAndMetadata(brew); splitTextStyleAndMetadata(req.brew);
req.brew = brew;
return next(); return next();
}); });
//Legacy/Other Document -> v3 Migration Guide //Legacy/Other Document -> v3 Migration Guide
app.get('/migrate', async (req, res, next)=>{ app.get('/migrate', (req, res, next)=>{
const brew = { req.brew = {
text : migrateText, text : migrateText,
renderer : 'V3' renderer : 'V3'
}; };
splitTextStyleAndMetadata(brew); splitTextStyleAndMetadata(req.brew);
req.brew = brew;
return next(); return next();
}); });
//Changelog page //Changelog page
app.get('/changelog', async (req, res, next)=>{ app.get('/changelog', async (req, res, next)=>{
const brew = { req.brew = {
title : 'Changelog', title : 'Changelog',
text : changelogText, text : changelogText,
renderer : 'V3' renderer : 'V3'
}; };
splitTextStyleAndMetadata(brew); splitTextStyleAndMetadata(req.brew);
req.brew = brew;
return next(); return next();
}); });
//FAQ page //FAQ page
app.get('/faq', async (req, res, next)=>{ app.get('/faq', async (req, res, next)=>{
const brew = { req.brew = {
title : 'FAQ', title : 'FAQ',
text : faqText, text : faqText,
renderer : 'V3' renderer : 'V3'
}; };
splitTextStyleAndMetadata(brew); splitTextStyleAndMetadata(req.brew);
req.brew = brew;
return next(); return next();
}); });
//Source page //Source page
app.get('/source/:id', asyncHandler(async (req, res)=>{ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw'); const { brew } = req;
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' }; const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text; let text = brew.text;
@@ -173,11 +142,12 @@ app.get('/source/:id', asyncHandler(async (req, res)=>{
} }
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`; text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text); res.status(200).send(text);
})); });
//Download brew source page //Download brew source page
app.get('/download/:id', asyncHandler(async (req, res)=>{ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw'); const { brew } = req;
sanitizeBrew(brew, 'share');
const prefix = 'HB - '; const prefix = 'HB - ';
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); 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"` 'Content-Disposition' : `attachment; filename="${fileName}.txt"`
}); });
res.status(200).send(brew.text); res.status(200).send(brew.text);
})); });
//User Page //User Page
app.get('/user/:username', async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username); const ownAccount = req.account && (req.account.username == req.params.username);
const fields = [ const fields = [
'googleId',
'title', 'title',
'pageCount', 'pageCount',
'description', 'description',
@@ -220,58 +191,71 @@ app.get('/user/:username', async (req, res, next)=>{
console.error(err); 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] })); googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
brews = _.concat(brews, googleBrews); brews = _.concat(brews, googleBrews);
} }
} }
req.brews = _.map(brews, (brew)=>{ req.brews = _.map(brews, (brew)=>{
return sanitizeBrew(brew, !ownAccount); return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
}); });
return next(); return next();
}); });
//Edit Page //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. 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(); return next();
})); });
//New Page //New Page
app.get('/new/:id', asyncHandler(async (req, res, next)=>{ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share'); sanitizeBrew(req.brew, 'share');
brew.title = `CLONE - ${brew.title}`; splitTextStyleAndMetadata(req.brew);
req.brew = brew; req.brew.title = `CLONE - ${brew.title}`;
return next(); return next();
})); });
//Share Page //Share Page
app.get('/share/:id', asyncHandler(async (req, res, next)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share'); 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 googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12); const shareId = req.params.id.slice(-12);
await GoogleActions.increaseView(googleId, shareId, 'share', brew) await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);}); .catch((err)=>{next(err);});
} else { } else {
await HomebrewModel.increaseView({ shareId: brew.shareId }); await HomebrewModel.increaseView({ shareId: brew.shareId });
} }
sanitizeBrew(req.brew, 'share');
req.brew = brew; splitTextStyleAndMetadata(req.brew);
return next(); return next();
})); }));
//Print Page //Print Page
app.get('/print/:id', asyncHandler(async (req, res, next)=>{ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share'); sanitizeBrew(req.brew, 'share');
req.brew = brew; splitTextStyleAndMetadata(req.brew);
return next(); next();
})); });
const nodeEnv = config.get('node_env'); const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv); const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
@@ -291,7 +275,7 @@ if(isLocalEnvironment){
//Render the page //Render the page
const templateFn = require('./../client/template.js'); const templateFn = require('./../client/template.js');
app.use((req, res)=>{ app.use(asyncHandler(async (req, res, next)=>{
// Create configuration object // Create configuration object
const configuration = { const configuration = {
local : isLocalEnvironment, local : isLocalEnvironment,
@@ -309,13 +293,14 @@ app.use((req, res)=>{
config : configuration config : configuration
}; };
const title = req.brew ? req.brew.title : ''; const title = req.brew ? req.brew.title : '';
templateFn('homebrew', title, props) const page = await templateFn('homebrew', title, props)
.then((page)=>{ res.send(page); }) .catch((err)=>{
.catch((err)=>{ console.log(err);
console.log(err); return res.sendStatus(500);
return res.sendStatus(500); });
}); if(!page) return;
}); res.send(page);
}));
//v=====----- Error-Handling Middleware -----=====v// //v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent //Format Errors so all fields will be sent
@@ -339,6 +324,13 @@ app.use((err, req, res, next)=>{
console.error(err); console.error(err);
res.status(status).send(getPureError(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 = { module.exports = {

View File

@@ -143,12 +143,11 @@ const GoogleActions = {
description : `${brew.description}`, description : `${brew.description}`,
properties : { properties : {
title : brew.title, title : brew.title,
published : brew.published, shareId : brew.shareId || nanoid(12),
version : brew.version, editId : brew.editId || nanoid(12),
renderer : brew.renderer,
tags : brew.tags,
pageCount : brew.pageCount, pageCount : brew.pageCount,
systems : brew.systems.join(), renderer : brew.renderer || 'legacy',
isStubbed : true,
thumbnail : brew.thumbnail thumbnail : brew.thumbnail
} }
}, },
@@ -161,10 +160,9 @@ const GoogleActions = {
console.log('Error saving to google'); console.log('Error saving to google');
console.error(err); console.error(err);
throw (err); throw (err);
//return res.status(500).send('Error while saving');
}); });
return (brew); return true;
}, },
newGoogleBrew : async (auth, brew)=>{ newGoogleBrew : async (auth, brew)=>{
@@ -178,17 +176,18 @@ const GoogleActions = {
const folderId = await GoogleActions.getGoogleFolder(auth); const folderId = await GoogleActions.getGoogleFolder(auth);
const fileMetadata = { const fileMetadata = {
'name' : `${brew.title}.txt`, name : `${brew.title}.txt`,
'description' : `${brew.description}`, description : `${brew.description}`,
'parents' : [folderId], parents : [folderId],
'properties' : { //AppProperties is not accessible properties : { //AppProperties is not accessible
'shareId' : brew.shareId || nanoid(12), shareId : brew.shareId || nanoid(12),
'editId' : brew.editId || nanoid(12), editId : brew.editId || nanoid(12),
'title' : brew.title, title : brew.title,
'views' : '0', pageCount : brew.pageCount,
'pageCount' : brew.pageCount, renderer : brew.renderer || 'legacy',
'renderer' : brew.renderer || 'legacy', isStubbed : true,
'thumbnail' : brew.thumbnail || '' version : 1,
thumbnail : brew.thumbnail || ''
} }
}; };
@@ -215,26 +214,7 @@ const GoogleActions = {
console.error(err); console.error(err);
}); });
const newHomebrew = { return obj.data.id;
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;
}, },
getGoogleBrew : async (id, accessId, accessType)=>{ getGoogleBrew : async (id, accessId, accessType)=>{
@@ -247,7 +227,6 @@ const GoogleActions = {
.catch((err)=>{ .catch((err)=>{
console.log('Error loading from Google'); console.log('Error loading from Google');
throw (err); throw (err);
return;
}); });
if(obj) { if(obj) {
@@ -257,9 +236,7 @@ const GoogleActions = {
throw ('Share ID does not match'); throw ('Share ID does not match');
} }
const serviceDrive = google.drive({ version: 'v3' }); const file = await drive.files.get({
const file = await serviceDrive.files.get({
fileId : id, fileId : id,
fields : 'description, properties', fields : 'description, properties',
alt : 'media' alt : 'media'
@@ -276,7 +253,7 @@ const GoogleActions = {
text : file.data, text : file.data,
description : obj.data.description, 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(',') : [], systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [], authors : [],
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false, 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', renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
thumbnail : obj.data.properties.thumbnail || '', thumbnail : obj.data.properties.thumbnail || '',
gDrive : true,
googleId : id 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 drive = google.drive({ version: 'v3', auth });
const googleId = id.slice(0, -12);
const accessId = id.slice(-12);
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : googleId, fileId : id,
fields : 'properties' fields : 'properties'
}) })
.catch((err)=>{ .catch((err)=>{
@@ -315,11 +288,11 @@ const GoogleActions = {
}); });
if(obj && obj.data.properties.editId != accessId) { 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({ await drive.files.update({
fileId : googleId, fileId : id,
resource : { trashed: true } resource : { trashed: true }
}) })
.catch((err)=>{ .catch((err)=>{

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
const _ = require('lodash'); const _ = require('lodash');
const HomebrewModel = require('./homebrew.model.js').model; const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router(); const router = require('express').Router();
@@ -6,6 +7,7 @@ const GoogleActions = require('./googleActions.js');
const Markdown = require('../shared/naturalcrit/markdown.js'); const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid');
// 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) {
@@ -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)=>{ const mergeBrewText = (brew)=>{
let text = brew.text; let text = brew.text;
if(brew.style !== undefined) { if(brew.style !== undefined) {
@@ -33,15 +82,33 @@ const MAX_TITLE_LENGTH = 100;
const getGoodBrewTitle = (text)=>{ const getGoodBrewTitle = (text)=>{
const tokens = Markdown.marked.lexer(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); .slice(0, MAX_TITLE_LENGTH);
}; };
const excludePropsFromUpdate = (brew)=>{ const excludePropsFromUpdate = (brew)=>{
// Remove undesired properties // Remove undesired properties
const propsToExclude = ['views', 'lastViewed']; const modified = _.clone(brew);
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
for (const prop of propsToExclude) { 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; return brew;
}; };
@@ -55,33 +122,17 @@ const beforeNewSave = (account, brew)=>{
brew.text = mergeBrewText(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 newGoogleBrew = async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, 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 newBrew = async (req, res)=>{
const brew = req.body; const brew = req.body;
const { transferToGoogle } = req.query; const { saveToGoogle } = req.query;
delete brew.editId; delete brew.editId;
delete brew.shareId; delete brew.shareId;
@@ -89,148 +140,179 @@ const newBrew = async (req, res)=>{
beforeNewSave(req.account, brew); beforeNewSave(req.account, brew);
let saved; const newHomebrew = new HomebrewModel(brew);
if(transferToGoogle) { newHomebrew.editId = nanoid(12);
saved = await newGoogleBrew(req.account, brew, res) newHomebrew.shareId = nanoid(12);
let googleId, saved;
if(saveToGoogle) {
googleId = await newGoogleBrew(req.account, newHomebrew, res)
.catch((err)=>{ .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 { } else {
saved = await newLocalBrew(brew) // Compress brew text to binary before saving
.catch((err)=>{ newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
res.status(500).send(err); // 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; if(!saved) return;
return res.status(200).send(saved); saved = saved.toObject();
res.status(200).send(saved);
}; };
const updateBrew = async (req, res)=>{ const updateBrew = async (req, res)=>{
let brew = excludePropsFromUpdate(req.body); // 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
const { transferToGoogle, transferFromGoogle } = req.query; let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
const { saveToGoogle, removeFromGoogle } = req.query;
const googleId = brew.googleId;
let afterSave = async ()=>true;
let saved; brew.text = mergeBrewText(brew);
if(brew.googleId && transferFromGoogle) {
beforeNewSave(req.account, brew);
saved = await newLocalBrew(brew) if(brew.googleId && removeFromGoogle) {
.catch((err)=>{ // 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
console.error(err); afterSave = async ()=>{
res.status(500).send(err); return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
}); .catch((err)=>{
if(!saved) return; 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)=>{ .catch((err)=>{
console.error(err); console.error(err);
res.status(err.status || err.response.status).send(err.message || err); res.status(err.status || err.response.status).send(err.message || err);
}); });
} else if(!brew.googleId && transferToGoogle) { if(!brew.googleId) return;
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);
});
} else if(brew.googleId) { } else if(brew.googleId) {
brew.text = mergeBrewText(brew); // If the google id exists and no other actions are being performed, update the google brew
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
saved = await GoogleActions.updateGoogleBrew(brew)
.catch((err)=>{ .catch((err)=>{
console.error(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 { } 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 // Compress brew text to binary before saving
brew.textBin = zlib.deflateRawSync(brew.text); brew.textBin = zlib.deflateRawSync(brew.text);
// Delete the non-binary text field since it's not needed anymore // Delete the non-binary text field since it's not needed anymore
brew.text = undefined; 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; 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)=>{ const deleteGoogleBrew = async (account, id, editId, 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 auth = await GoogleActions.authCheck(account, res); const auth = await GoogleActions.authCheck(account, res);
await GoogleActions.deleteGoogleBrew(auth, id); await GoogleActions.deleteGoogleBrew(auth, id, editId);
return true; return true;
}; };
router.post('/api', asyncHandler(newBrew)); const deleteBrew = async (req, res)=>{
router.put('/api/:id', asyncHandler(updateBrew)); let brew = req.brew;
router.put('/api/update/:id', asyncHandler(updateBrew)); const { googleId, editId } = brew;
router.delete('/api/:id', asyncHandler(deleteBrew)); const account = req.account;
router.get('/api/remove/:id', asyncHandler(deleteBrew)); 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
};

View File

@@ -6,6 +6,7 @@ const zlib = require('zlib');
const HomebrewSchema = mongoose.Schema({ const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { 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: '' }, title : { type: String, default: '' },
text : { type: String, default: '' }, text : { type: String, default: '' },
textBin : { type: Buffer }, textBin : { type: Buffer },