From 770d0c141de073059158f7516d57403494f7ec9d Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 13 Dec 2022 21:03:51 -0600 Subject: [PATCH 1/8] add 409 return when server version is greater than updating version This also moves the version back onto the stub --- server/homebrew.api.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 0a113c8b9..d5cced977 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -111,7 +111,7 @@ const excludePropsFromUpdate = (brew)=>{ const 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]; } @@ -119,7 +119,7 @@ const excludeGoogleProps = (brew)=>{ }; const excludeStubProps = (brew)=>{ - const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version']; + const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount']; for (const prop of propsToExclude) { brew[prop] = undefined; } @@ -187,7 +187,13 @@ const newBrew = async (req, res)=>{ const 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, excludePropsFromUpdate(req.body)); + const updateBrew = excludePropsFromUpdate(req.body); + if(req.brew.version > updateBrew.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, updateBrew); const { saveToGoogle, removeFromGoogle } = req.query; const googleId = brew.googleId; let afterSave = async ()=>true; @@ -233,6 +239,7 @@ const updateBrew = async (req, res)=>{ brew.text = undefined; } brew.updatedAt = new Date(); + brew.version += 1; if(req.account) { brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); @@ -252,7 +259,7 @@ const updateBrew = async (req, res)=>{ // if the brew does have a stub id, update it using the stub id as the key. brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); saved = await brew.save() - .catch(saveError); + .catch(saveError); } if(!saved) return; // Call and wait for afterSave to complete From 63e043593a12f9fe08c40603490dba57d5b08916 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 13 Dec 2022 21:05:00 -0600 Subject: [PATCH 2/8] add invitedAuthors key and move invited author to authors on save --- server/homebrew.api.js | 5 ++++- server/homebrew.model.js | 15 ++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index d5cced977..f28412281 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -43,7 +43,9 @@ const getBrew = (accessType, stubOnly = false)=>{ } }); stub = stub?.toObject(); - if(accessType === 'edit' && stub?.authors?.length > 0 && !stub?.authors.includes(req.account?.username)) { + const authorsExistAndIsNotAuthor = stub?.authors?.length > 0 && !stub?.authors.includes(req.account?.username); + const isNotInvited = stub?.invitedAuthors?.length > 0 && !stub?.invitedAuthors.includes(req.account?.username); + if(accessType === 'edit' && authorsExistAndIsNotAuthor && isNotInvited) { throw 'Current logged in user does not have access to this brew.'; } @@ -243,6 +245,7 @@ const updateBrew = async (req, res)=>{ if(req.account) { brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); + brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a)); } // define a function to catch our save errors diff --git a/server/homebrew.model.js b/server/homebrew.model.js index a514e3fd8..da8853de7 100644 --- a/server/homebrew.model.js +++ b/server/homebrew.model.js @@ -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 }, From 354d01e98079aea70df90601ff38faf85b56515e Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 13 Dec 2022 21:05:45 -0600 Subject: [PATCH 3/8] set the version in the ui from the server response on save --- client/homebrew/pages/editPage/editPage.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 0ae7cdba0..0f6c5387e 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -230,7 +230,8 @@ const EditPage = createClass({ brew : { ...prevState.brew, googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null, editId : this.savedBrew.editId, - shareId : this.savedBrew.shareId + shareId : this.savedBrew.shareId, + version : this.savedBrew.version }, isPending : false, isSaving : false, From e5febc1feffbc28499cbee32b1fb767f47c2c8be Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 13 Dec 2022 21:06:47 -0600 Subject: [PATCH 4/8] update ui to include invitedAuthors array --- .../editor/metadataEditor/metadataEditor.jsx | 21 +++++++++++++-- .../editor/metadataEditor/metadataEditor.less | 20 +++++++++++--- .../stringArrayEditor/stringArrayEditor.jsx | 27 ++++++++++++------- client/homebrew/pages/editPage/editPage.jsx | 11 ++++++++ 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index ac644f1a1..227993ebd 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -250,6 +250,8 @@ const MetadataEditor = createClass({ render : function(){ return
+

Brew

+
this.handleFieldChange('tags', e)}/> - {this.renderAuthors()} -
@@ -296,6 +296,23 @@ const MetadataEditor = createClass({ {this.renderRenderOptions()} +
+ +

Authors

+ + {this.renderAuthors()} + + !this.props.metadata.authors.includes(v)]} + placeholder='invite author' unique={true} + values={this.props.metadata.invitedAuthors} + notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} + onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> + +
+ +

Privacy

+
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less index f0974fb6c..be1a9aa9c 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/metadataEditor/metadataEditor.less @@ -10,6 +10,15 @@ height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this. overflow-y : auto; + .sectionHead { + font-weight: 1000; + margin: 20px 0; + + &:first-of-type { + margin-top: 0; + } + } + & > div { margin-bottom: 10px; } @@ -30,6 +39,7 @@ } .field{ display : flex; + flex-wrap : wrap; width : 100%; min-width : 200px; &>label{ @@ -78,6 +88,11 @@ font-size : 0.8em; } } + + small { + font-size : 0.6em; + font-style : italic; + } } @@ -128,10 +143,6 @@ button.unpublish{ .button(@silver); } - small{ - font-size : 0.6em; - font-style : italic; - } } .delete.field .value{ @@ -196,6 +207,7 @@ } .field .list { display: flex; + flex: 1 0; flex-wrap: wrap; > * { diff --git a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx index 790cfba95..4fc570cef 100644 --- a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx +++ b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx @@ -9,7 +9,9 @@ const StringArrayEditor = createClass({ label : '', values : [], valuePatterns : null, + validators : [], placeholder : '', + notes : [], unique : false, cannotEdit : [], onChange : ()=>{} @@ -83,7 +85,8 @@ const StringArrayEditor = createClass({ } const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern)); const uniqueIfSet = !this.props.unique || !values.includes(value); - return matchesPatterns && uniqueIfSet; + const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value)); + return matchesPatterns && uniqueIfSet && passesValidators; }, handleValueInputKeyDown : function(event, index) { @@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
); - return
+ return
-
- {valueElements} -
- this.handleValueInputKeyDown(e)} - onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/> - {this.valueIsValid(this.state.temporaryValue) ?
{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}>
: null} +
+
+ {valueElements} +
+ this.handleValueInputKeyDown(e)} + onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/> + {this.valueIsValid(this.state.temporaryValue) ?
{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}>
: null} +
+ + {this.props.notes ? this.props.notes.map((n)=>

{n}

) : null}
; } diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 0f6c5387e..61c42297d 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -330,6 +330,17 @@ const EditPage = createClass({ ; } + if(this.state.errors.response.error.status === 409) { + console.log(this.state.errors.response); + const message = this.state.errors.response.body?.message; + return + Oops! +
+ {message ? message : 'Conflict: please refresh to get latest changes'} +
+
; + } + return Oops!
From a3dc5e78fd5c26ccbdf61bde97d93ba4946f02ec Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 14 Dec 2022 10:04:43 -0600 Subject: [PATCH 5/8] remove console log --- client/homebrew/pages/editPage/editPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 61c42297d..e8c0db03e 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -331,7 +331,6 @@ const EditPage = createClass({ } if(this.state.errors.response.error.status === 409) { - console.log(this.state.errors.response); const message = this.state.errors.response.body?.message; return Oops! From 7c61a270848721e57a795c4491a42bf625bc8bb4 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 21 Dec 2022 15:59:00 -0600 Subject: [PATCH 6/8] update authorship edit check and error --- server/homebrew.api.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index f28412281..c10e82099 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -43,11 +43,6 @@ const getBrew = (accessType, stubOnly = false)=>{ } }); stub = stub?.toObject(); - const authorsExistAndIsNotAuthor = stub?.authors?.length > 0 && !stub?.authors.includes(req.account?.username); - const isNotInvited = stub?.invitedAuthors?.length > 0 && !stub?.invitedAuthors.includes(req.account?.username); - if(accessType === 'edit' && authorsExistAndIsNotAuthor && isNotInvited) { - 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(!stubOnly && (googleId || stub?.googleId)) { @@ -62,6 +57,14 @@ const getBrew = (accessType, stubOnly = false)=>{ // 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; } + 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 && !stubOnly) { @@ -189,13 +192,13 @@ const newBrew = async (req, res)=>{ const 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 - const updateBrew = excludePropsFromUpdate(req.body); - if(req.brew.version > updateBrew.version) { + const brewFromClient = 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, updateBrew); + let brew = _.assign(req.brew, brewFromClient); const { saveToGoogle, removeFromGoogle } = req.query; const googleId = brew.googleId; let afterSave = async ()=>true; From 2f5bc8db54841a2f0504d29af9e34771fd6658c1 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 21 Dec 2022 16:19:41 -0600 Subject: [PATCH 7/8] remove redundant boolean comparison logic --- server/homebrew.api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index c10e82099..9f006bb70 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -58,8 +58,8 @@ const getBrew = (accessType, stubOnly = false)=>{ stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, 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); + 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. From 314f758d622ab7391a30e4e71254cee46eafc272 Mon Sep 17 00:00:00 2001 From: Charlie Date: Thu, 22 Dec 2022 15:58:33 -0600 Subject: [PATCH 8/8] Update server/homebrew.api.js Co-authored-by: Trevor Buckner --- server/homebrew.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 9f006bb70..e0984d4fe 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -60,7 +60,7 @@ const getBrew = (accessType, stubOnly = false)=>{ 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))) { + 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.`;