diff --git a/.eslintrc.js b/.eslintrc.js index 1d1457f6c..6d763454c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,7 +55,7 @@ module.exports = { 'array-bracket-spacing' : ['warn', 'never'], 'arrow-spacing' : ['warn', { before: false, after: false }], 'comma-spacing' : ['warn', { before: false, after: true }], - 'indent' : ['warn', 'tab'], + 'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }], 'keyword-spacing' : ['warn', { before : true, after : true, diff --git a/client/homebrew/googleDrive.png b/client/homebrew/googleDrive.png new file mode 100644 index 000000000..f555103cc Binary files /dev/null and b/client/homebrew/googleDrive.png differ diff --git a/client/homebrew/googleDriveMono.png b/client/homebrew/googleDriveMono.png new file mode 100644 index 000000000..a573196d7 Binary files /dev/null and b/client/homebrew/googleDriveMono.png differ diff --git a/client/homebrew/navbar/account.navitem.jsx b/client/homebrew/navbar/account.navitem.jsx index d85b35bf7..3d36e5bc6 100644 --- a/client/homebrew/navbar/account.navitem.jsx +++ b/client/homebrew/navbar/account.navitem.jsx @@ -2,17 +2,33 @@ const React = require('react'); const createClass = require('create-react-class'); const Nav = require('naturalcrit/nav/nav.jsx'); -module.exports = function(props){ - if(global.account){ - return - {global.account.username} +const Account = createClass({ + + getInitialState : function() { + return { + url : '' + }; + }, + + componentDidMount : function(){ + if(typeof window !== 'undefined'){ + this.setState({ + url : window.location.href + }); + } + }, + + render : function(){ + if(global.account){ + return + {global.account.username} + ; + } + + return + login ; } - let url = ''; - if(typeof window !== 'undefined'){ - url = window.location.href; - } - return - login - ; -}; \ No newline at end of file +}); + +module.exports = Account; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index cfa886fe2..93ce2a202 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ require('./editPage.less'); const React = require('react'); const createClass = require('create-react-class'); @@ -20,8 +21,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const Markdown = require('naturalcrit/markdown.js'); -const SAVE_TIMEOUT = 3000; +const googleDriveActive = require('../../googleDrive.png'); +const googleDriveInactive = require('../../googleDriveMono.png'); +const SAVE_TIMEOUT = 3000; const EditPage = createClass({ getDefaultProps : function() { @@ -32,6 +35,7 @@ const EditPage = createClass({ editId : null, createdAt : null, updatedAt : null, + gDrive : false, title : '', description : '', @@ -49,13 +53,19 @@ const EditPage = createClass({ isSaving : false, isPending : false, + saveGoogle : this.props.brew.googleId ? true : false, errors : null, htmlErrors : Markdown.validate(this.props.brew.text), + url : '' }; }, savedBrew : null, componentDidMount : function(){ + this.setState({ + url : window.location.href + }); + this.trySave(); window.onbeforeunload = ()=>{ if(this.state.isSaving || this.state.isPending){ @@ -74,7 +84,6 @@ const EditPage = createClass({ document.removeEventListener('keydown', this.handleControlKeys); }, - handleControlKeys : function(e){ if(!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; @@ -126,7 +135,13 @@ const EditPage = createClass({ } }, - save : function(){ + toggleGoogleStorage : function(){ + this.setState((prevState)=>({ + saveGoogle : !prevState.saveGoogle + })); + }, + + save : async function(){ if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); this.setState((prevState)=>({ @@ -135,22 +150,104 @@ const EditPage = createClass({ htmlErrors : Markdown.validate(prevState.brew.text) })); - request - .put(`/api/${this.props.brew.editId}`) - .send(this.state.brew) - .end((err, res)=>{ - if(err){ - this.setState({ - errors : err, - }); - } else { - this.savedBrew = res.body; - this.setState({ - isPending : false, - isSaving : false, - }); - } - }); + const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); + + if(this.state.saveGoogle) { + if(transfer) { + const res = await request + .post('/api/newGoogle/') + .send(this.state.brew) + .catch((err)=>{ + console.log(err.status === 401 + ? 'Not signed in!' + : 'Error Saving to Google!'); + this.setState({ errors: err }); + }); + + if(!res) { return; } + + console.log('Deleting Local Copy'); + await request.delete(`/api/${this.state.brew.editId}`) + .send() + .catch((err)=>{ + console.log('Error deleting Local Copy'); + }); + + this.savedBrew = res.body; + history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID + } else { + const res = await request + .put(`/api/updateGoogle/${this.state.brew.editId}`) + .send(this.state.brew) + .catch((err)=>{ + console.log(err.status === 401 + ? 'Not signed in!' + : 'Error Saving to Google!'); + this.setState({ errors: err }); + return; + }); + + this.savedBrew = res.body.brew; + } + } else { + console.log('Saving Locally'); + if(transfer) { + console.log('Moving to Local Storage'); + const res = await request.post('/api') + .send(this.state.brew) + .catch((err)=>{ + console.log('Error creating Local Copy'); + this.setState({ errors: err }); + return; + }); + + await request.get(`/api/removeGoogle/${this.state.brew.googleId}${this.state.brew.editId}`) + .send() + .catch((err)=>{ + console.log('Error Deleting Google Brew'); + }); + + console.log('GOT THIS SAVED BREW:'); + console.log(res); + this.savedBrew = res.body; + history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID + } else { + console.log('Updating existing local copy'); + const res = await request + .put(`/api/update/${this.state.brew.editId}`) + .send(this.state.brew) + .catch((err)=>{ + console.log('Error Updating Local Brew'); + this.setState({ errors: err }); + return; + }); + + this.savedBrew = res.body; + } + } + + console.log('Finished saving. About to merge saved brew with current'); + this.setState((prevState)=>({ + brew : _.merge({}, prevState.brew, { + googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null, + editId : this.savedBrew.editId, + }), + isPending : false, + isSaving : false, + })); + }, + + renderGoogleDriveIcon : function(){ + if(this.state.saveGoogle) { + return + googleDriveActive + ; + } else { + return + googleDriveInactive + ; + } + }, renderSaveButton : function(){ @@ -161,6 +258,19 @@ const EditPage = createClass({ errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``; } catch (e){} + if(this.state.errors.status == '401'){ + return + Oops! +
+ You must be signed in to a Google account + to save this to Google Drive!
+ Sign in + here. +
+
; + } + return Oops!
@@ -183,6 +293,7 @@ const EditPage = createClass({ return saved.; } }, + renderNavbar : function(){ return @@ -190,6 +301,7 @@ const EditPage = createClass({ + {this.renderGoogleDriveIcon()} {this.renderSaveButton()} diff --git a/client/homebrew/pages/editPage/editPage.less b/client/homebrew/pages/editPage/editPage.less index 85890df44..8e48c7dc5 100644 --- a/client/homebrew/pages/editPage/editPage.less +++ b/client/homebrew/pages/editPage/editPage.less @@ -15,8 +15,8 @@ top : 29px; left : -20px; z-index : 1000; - width : 120px; - padding : 8px; + width : 135px; + padding : 6px; background-color : #333; a{ color : @teal; @@ -24,4 +24,9 @@ } } } -} \ No newline at end of file + .googleDriveStorage img{ + height : 20px; + padding : 0px; + margin : -5px; + } +} diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 8f617a1d5..e875b3c05 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -24,6 +24,7 @@ const NewPage = createClass({ getInitialState : function() { return { metadata : { + gDrive : false, title : '', description : '', tags : '', @@ -32,11 +33,13 @@ const NewPage = createClass({ systems : [] }, - text : '', - isSaving : false, - errors : [] + text : '', + isSaving : false, + saveGoogle : (global.account.googleId ? true : false), + errors : [] }; }, + componentDidMount : function() { const storage = localStorage.getItem(KEY); if(storage){ @@ -80,12 +83,30 @@ const NewPage = createClass({ localStorage.setItem(KEY, text); }, - save : function(){ + save : async function(){ this.setState({ isSaving : true }); - request.post('/api') + console.log('saving new brew'); + + if(this.state.saveGoogle) { + const res = await request + .post('/api/newGoogle/') + .send(_.merge({}, this.state.metadata, { text: this.state.text })) + .catch((err)=>{ + console.log(err.status === 401 + ? 'Not signed in!' + : 'Error Creating New Google Brew!'); + this.setState({ isSaving: false }); + return; + }); + + const brew = res.body; + localStorage.removeItem(KEY); + window.location = `/edit/${brew.googleId}${brew.editId}`; + } else { + request.post('/api') .send(_.merge({}, this.state.metadata, { text : this.state.text })) @@ -101,6 +122,8 @@ const NewPage = createClass({ localStorage.removeItem(KEY); window.location = `/edit/${brew.editId}`; }); + } + }, renderSaveButton : function(){ diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 41056c7b4..8d3b10ce4 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -45,6 +45,17 @@ const SharePage = createClass({ } }, + renderSourceButton : function() { + let shareLink = this.props.brew.shareId; + if(this.props.brew.googleId) { + shareLink = this.props.brew.googleId + shareLink; + } + + return + source + ; + }, + render : function(){ return
@@ -55,9 +66,7 @@ const SharePage = createClass({ - - source - + {this.renderSourceButton()} diff --git a/client/homebrew/pages/userPage/brewItem/brewItem.jsx b/client/homebrew/pages/userPage/brewItem/brewItem.jsx index c3a109d09..bddf246bf 100644 --- a/client/homebrew/pages/userPage/brewItem/brewItem.jsx +++ b/client/homebrew/pages/userPage/brewItem/brewItem.jsx @@ -6,6 +6,8 @@ const cx = require('classnames'); const moment = require('moment'); const request = require('superagent'); +const googleDriveIcon = require('../../../googleDrive.png'); + const BrewItem = createClass({ getDefaultProps : function() { return { @@ -27,11 +29,19 @@ const BrewItem = createClass({ if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return; } - request.delete(`/api/${this.props.brew.editId}`) - .send() - .end(function(err, res){ - location.reload(); - }); + if(this.props.brew.googleId) { + request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`) + .send() + .end(function(err, res){ + location.reload(); + }); + } else { + request.delete(`/api/${this.props.brew.editId}`) + .send() + .end(function(err, res){ + location.reload(); + }); + } }, renderDeleteBrewLink : function(){ @@ -41,14 +51,41 @@ const BrewItem = createClass({ ; }, + renderEditLink : function(){ if(!this.props.brew.editId) return; - return + let editLink = this.props.brew.editId; + if(this.props.brew.googleId) { + editLink = this.props.brew.googleId + editLink; + } + + return ; }, + renderShareLink : function(){ + if(!this.props.brew.shareId) return; + + let shareLink = this.props.brew.shareId; + if(this.props.brew.googleId) { + shareLink = this.props.brew.googleId + shareLink; + } + + return + + ; + }, + + renderGoogleDriveIcon : function(){ + if(!this.props.brew.gDrive) return; + + return + googleDriveIcon + ; + }, + render : function(){ const brew = this.props.brew; return
@@ -66,12 +103,11 @@ const BrewItem = createClass({ {moment(brew.updatedAt).fromNow()} + {this.renderGoogleDriveIcon()}
- - - + {this.renderShareLink()} {this.renderEditLink()} {this.renderDeleteBrewLink()}
diff --git a/client/homebrew/pages/userPage/brewItem/brewItem.less b/client/homebrew/pages/userPage/brewItem/brewItem.less index 3dcbd5090..8a1b6cb35 100644 --- a/client/homebrew/pages/userPage/brewItem/brewItem.less +++ b/client/homebrew/pages/userPage/brewItem/brewItem.less @@ -7,6 +7,7 @@ box-sizing : border-box; overflow : hidden; width : 48%; + min-height : 80px; margin-right : 15px; margin-bottom : 15px; padding : 5px 15px 5px 8px; @@ -55,6 +56,14 @@ &:hover{ opacity : 1; } + i{ + cursor : pointer; + } } } -} \ No newline at end of file + .googleDriveIcon { + height : 20px; + padding : 0px; + margin : -5px; + } +} diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx index e0b371a7e..5d46265e1 100644 --- a/client/homebrew/pages/userPage/userPage.jsx +++ b/client/homebrew/pages/userPage/userPage.jsx @@ -22,8 +22,9 @@ const BrewItem = require('./brewItem/brewItem.jsx'); const UserPage = createClass({ getDefaultProps : function() { return { - username : '', - brews : [] + username : '', + brews : [], + googleBrews : [] }; }, diff --git a/package-lock.json b/package-lock.json index 4beb223ef..cf126e63a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1732,6 +1732,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -1782,6 +1790,29 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==" }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ajv": { "version": "6.12.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", @@ -1937,6 +1968,11 @@ "function-bind": "^1.1.1" } }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2087,6 +2123,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, "binary-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", @@ -2487,6 +2528,11 @@ "ieee754": "^1.1.4" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -3018,6 +3064,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deep-object-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.0.tgz", + "integrity": "sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw==" + }, "defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", @@ -3193,6 +3244,14 @@ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3553,6 +3612,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", @@ -3648,6 +3712,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -3755,6 +3824,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, "fbjs": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", @@ -3894,6 +3968,39 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gaxios": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.1.0.tgz", + "integrity": "sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + } + } + }, + "gcp-metadata": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.4.tgz", + "integrity": "sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA==", + "requires": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -3955,6 +4062,52 @@ "type-fest": "^0.8.1" } }, + "google-auth-library": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.6.tgz", + "integrity": "sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^3.0.0", + "gcp-metadata": "^4.1.0", + "gtoken": "^5.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.2.tgz", + "integrity": "sha512-tbjzndQvSIHGBLzHnhDs3cL4RBjLbLXc2pYvGH+imGVu5b4RMAttUTdnmW2UH0t11QeBTXZ7wlXPS7hrypO/tg==", + "requires": { + "node-forge": "^0.9.0" + } + }, + "googleapis": { + "version": "59.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-59.0.0.tgz", + "integrity": "sha512-GV/E4KRN89a4GxSk7D7cwUfRYgcJHR05sOgm/WGdwc/u8dxNXG5lWmz9gF5ZwFGk2yKtVxL4VZNn4zBuZ6rmGg==", + "requires": { + "google-auth-library": "^6.0.0", + "googleapis-common": "^4.4.0" + } + }, + "googleapis-common": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-4.4.0.tgz", + "integrity": "sha512-Bgrs8/1OZQFFIfVuX38L9t48rPAkVUXttZy6NzhhXxFOEMSHgfFIjxou7RIXOkBHxmx2pVwct9WjKkbnqMYImQ==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^3.0.0", + "google-auth-library": "^6.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^8.0.0" + } + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -3978,6 +4131,24 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, + "gtoken": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz", + "integrity": "sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg==", + "requires": { + "gaxios": "^3.0.0", + "google-p12-pem": "^3.0.0", + "jws": "^4.0.0", + "mime": "^2.2.0" + }, + "dependencies": { + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4154,6 +4325,30 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "iconv-lite": { "version": "0.4.21", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", @@ -4553,6 +4748,14 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -4622,6 +4825,25 @@ "object.assign": "^4.1.0" } }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "jwt-simple": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/jwt-simple/-/jwt-simple-0.5.6.tgz", @@ -4783,6 +5005,14 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5203,9 +5433,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==" }, "nanomatch": { "version": "1.2.13", @@ -5269,6 +5499,11 @@ "is-stream": "^1.0.1" } }, + "node-forge": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + }, "node-releases": { "version": "1.1.60", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", @@ -6591,14 +6826,6 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" }, - "shortid": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz", - "integrity": "sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==", - "requires": { - "nanoid": "^2.1.0" - } - }, "side-channel": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", @@ -7496,6 +7723,11 @@ "prepend-http": "^2.0.0" } }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -7519,6 +7751,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + }, "v8-compile-cache": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", @@ -7856,6 +8093,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yargs": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", diff --git a/package.json b/package.json index fc916ada0..798b36594 100644 --- a/package.json +++ b/package.json @@ -50,19 +50,20 @@ "create-react-class": "^15.6.3", "express": "^4.17.1", "fs-extra": "9.0.1", + "googleapis": "59.0.0", "jwt-simple": "^0.5.6", "less": "^3.12.2", "lodash": "^4.17.20", "marked": "^0.3.19", "moment": "^2.27.0", "mongoose": "^5.10.0", + "nanoid": "3.1.12", "nconf": "^0.10.0", "prop-types": "15.7.2", "query-string": "6.13.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "5.2.0", - "shortid": "^2.2.15", "superagent": "^6.0.0", "vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b" }, diff --git a/scripts/buildHomebrew.js b/scripts/buildHomebrew.js index bddd1182c..d4cb5dd39 100644 --- a/scripts/buildHomebrew.js +++ b/scripts/buildHomebrew.js @@ -1,7 +1,7 @@ const fs = require('fs-extra'); const Proj = require('./project.json'); -const { pack } = require('vitreum'); +const { pack, watchFile, livereload } = require('vitreum'); const isDev = !!process.argv.find((arg)=>arg=='--dev'); const lessTransform = require('vitreum/transforms/less.js'); @@ -29,3 +29,12 @@ pack('./client/homebrew/homebrew.jsx', { }) .then(build) .catch(console.error); + + +//In development set up a watch server and livereload +if(isDev){ + livereload('./build'); + watchFile('./server.js', { + watch : ['./homebrew'] // Watch additional folders if you want + }); +} diff --git a/server.js b/server.js index 67e9d055e..cebdb8ad9 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,9 @@ const jwt = require('jwt-simple'); const express = require('express'); const app = express(); +const homebrewApi = require('./server/homebrew.api.js'); +const GoogleActions = require('./server/googleActions.js'); + app.use(express.static(`${__dirname}/build`)); app.use(require('body-parser').json({ limit: '25mb' })); app.use(require('cookie-parser')()); @@ -24,21 +27,30 @@ mongoose.connection.on('error', ()=>{ throw 'Can not connect to Mongo'; }); - //Account Middleware app.use((req, res, next)=>{ if(req.cookies && req.cookies.nc_session){ try { req.account = jwt.decode(req.cookies.nc_session, config.get('secret')); + //console.log("Just loaded up JWT from cookie:"); + //console.log(req.account); } catch (e){} } + + req.config = { + googleClientId : config.get('googleClientId'), + googleClientSecret : config.get('googleClientSecret') + }; return next(); }); -app.use(require('./server/homebrew.api.js')); +app.use(homebrewApi); + app.use(require('./server/admin.api.js')); +//app.use('/user',require('./server/user.routes.js')); + const HomebrewModel = require('./server/homebrew.model.js').model; const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); @@ -53,57 +65,112 @@ app.get('/robots.txt', (req, res)=>{ //Source page app.get('/source/:id', (req, res)=>{ - HomebrewModel.get({ shareId: req.params.id }) + if(req.params.id.length > 12) { + const googleId = req.params.id.slice(0, -12); + const shareId = req.params.id.slice(-12); + GoogleActions.readFileMetadata(config.get('googleApiKey'), googleId, shareId, 'share') .then((brew)=>{ const text = brew.text.replaceAll('<', '<').replaceAll('>', '>'); return res.send(`
${text}
`); }) .catch((err)=>{ console.log(err); - return res.status(404).send('Could not find Homebrew with that id'); + return res.status(400).send('Can\'t get brew from Google'); }); + } else { + HomebrewModel.get({ shareId: req.params.id }) + .then((brew)=>{ + const text = brew.text.replaceAll('<', '<').replaceAll('>', '>'); + return res.send(`
${text}
`); + }) + .catch((err)=>{ + console.log(err); + return res.status(404).send('Could not find Homebrew with that id'); + }); + } }); //User Page -app.get('/user/:username', (req, res, next)=>{ +app.get('/user/:username', async (req, res, next)=>{ const fullAccess = req.account && (req.account.username == req.params.username); - HomebrewModel.getByUser(req.params.username, fullAccess) - .then((brews)=>{ - req.brews = brews; - return next(); - }) + + let googleBrews = []; + + if(req.account.googleId){ + console.log('GETTING DATA FOR USER PAGE'); + googleBrews = await GoogleActions.listGoogleBrews(req, res) .catch((err)=>{ - console.log(err); + console.error(err); }); + } + + const brews = await HomebrewModel.getByUser(req.params.username, fullAccess) + .catch((err)=>{ + console.log(err); + }); + + if(googleBrews) { + req.brews = _.concat(brews, googleBrews); + } else {req.brews = brews;} + + return next(); }); //Edit Page app.get('/edit/:id', (req, res, next)=>{ - HomebrewModel.get({ editId: req.params.id }) + if(req.params.id.length > 12) { + const googleId = req.params.id.slice(0, -12); + const editId = req.params.id.slice(-12); + GoogleActions.readFileMetadata(config.get('googleApiKey'), googleId, editId, 'edit') .then((brew)=>{ - req.brew = brew.sanatize(); + req.brew = brew; //TODO Need to sanitize later return next(); }) .catch((err)=>{ console.log(err); - return res.status(400).send(`Can't get that`); + return res.status(400).send('Can\'t get brew from Google'); }); + } else { + HomebrewModel.get({ editId: req.params.id }) + .then((brew)=>{ + req.brew = brew.sanatize(); + return next(); + }) + .catch((err)=>{ + console.log(err); + return res.status(400).send(`Can't get that`); + }); + } }); //Share Page app.get('/share/:id', (req, res, next)=>{ - HomebrewModel.get({ shareId: req.params.id }) + if(req.params.id.length > 12) { + const googleId = req.params.id.slice(0, -12); + const shareId = req.params.id.slice(-12); + GoogleActions.readFileMetadata(config.get('googleApiKey'), googleId, shareId, 'share') .then((brew)=>{ - return brew.increaseView(); - }) - .then((brew)=>{ - req.brew = brew.sanatize(true); + req.brew = brew; //TODO Need to sanitize later return next(); }) .catch((err)=>{ console.log(err); - return res.status(400).send(`Can't get that`); + return res.status(400).send('Can\'t get brew from Google'); }); + } else { + HomebrewModel.get({ shareId: req.params.id }) + .then((brew)=>{ + return brew.increaseView(); + }) + .then((brew)=>{ + req.brew = brew.sanatize(true); + return next(); + }) + .catch((err)=>{ + console.log(err); + return res.status(400).send(`Can't get that`); + }); + } }); //Print Page @@ -131,10 +198,11 @@ app.use((req, res)=>{ changelog : changelogText, brew : req.brew, brews : req.brews, + googleBrews : req.googleBrews, account : req.account, }; templateFn('homebrew', props) - .then((page)=>res.send(page)) + .then((page)=>{console.log('^--- END OF REQUEST ---^'); res.send(page);}) .catch((err)=>{ console.log(err); return res.sendStatus(500); diff --git a/server/googleActions.js b/server/googleActions.js new file mode 100644 index 000000000..0f465ac2f --- /dev/null +++ b/server/googleActions.js @@ -0,0 +1,432 @@ +/* eslint-disable max-lines */ +const _ = require('lodash'); +const { google } = require('googleapis'); +const { nanoid } = require('nanoid'); +const token = require('./token.js'); +const config = require('nconf') + .argv() + .env({ lowerCase: true }) // Load environment variables + .file('environment', { file: `config/${process.env.NODE_ENV}.json` }) + .file('defaults', { file: 'config/default.json' }); + +//let oAuth2Client; + +GoogleActions = { + + authCheck : (account, res)=>{ + console.log('RUNNING AUTH CHECK'); + + if(!account || !account.googleId){ // If not signed into Google + const err = new Error('Not Signed In'); + err.status = 401; + throw err; + } + + const oAuth2Client = new google.auth.OAuth2( + config.get('googleClientId'), + config.get('googleClientSecret'), + '/auth/google/redirect' + ); + + oAuth2Client.setCredentials({ + access_token : account.googleAccessToken, //Comment out to refresh token + refresh_token : account.googleRefreshToken + }); + + oAuth2Client.on('tokens', (tokens)=>{ + if(tokens.refresh_token) { + account.googleRefreshToken = tokens.refresh_token; + } + account.googleAccessToken = tokens.access_token; + const JWTToken = token.generateAccessToken(account); + console.log('Updated Access Token'); + + //Save updated token to cookie + res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' }); + //res.cookie('nc_session', JWTToken, {maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax', domain: '.naturalcrit.com'}); + }); + + return oAuth2Client; + }, + + getGoogleFolder : async (req, res)=>{ + console.log('getting google folder'); + oAuth2Client = GoogleActions.authCheck(req.account, res); + + const drive = google.drive({ version: 'v3', auth: oAuth2Client }); + + fileMetadata = { + 'name' : 'Homebrewery', + 'mimeType' : 'application/vnd.google-apps.folder' + }; + + const obj = await drive.files.list({ + q : 'mimeType = \'application/vnd.google-apps.folder\'' + }) + .catch((err)=>{ + console.log('Error searching Google Drive Folders'); + console.error(err); + }); + + let folderId; + + if(obj.data.files.length == 0){ + console.log('no folders found'); // CREATE APP FOLDER + + const obj = await drive.files.create({ + resource : fileMetadata + }) + .catch((err)=>{ + console.log('Error creating google app folder'); + console.error(err); + }); + + console.log('created new drive folder with ID:'); + console.log(obj.data.id); + folderId = obj.data.id; + } else { + folderId = obj.data.files[0].id; + } + + return folderId; + }, + + getGoogleFolderNew : async (auth)=>{ + console.log('getting google folder'); + const drive = google.drive({ version: 'v3', auth: auth }); + + fileMetadata = { + 'name' : 'Homebrewery', + 'mimeType' : 'application/vnd.google-apps.folder' + }; + + const obj = await drive.files.list({ + q : 'mimeType = \'application/vnd.google-apps.folder\'' + }) + .catch((err)=>{ + console.log('Error searching Google Drive Folders'); + console.error(err); + }); + + let folderId; + + if(obj.data.files.length == 0){ + console.log('no folders found'); // CREATE APP FOLDER + + const obj = await drive.files.create({ + resource : fileMetadata + }) + .catch((err)=>{ + console.log('Error creating google app folder'); + console.error(err); + }); + + console.log('created new drive folder with ID:'); + console.log(obj.data.id); + folderId = obj.data.id; + } else { + folderId = obj.data.files[0].id; + } + + return folderId; + }, + + listGoogleBrews : async (req, res)=>{ + + oAuth2Client = GoogleActions.authCheck(req.account, res); + + const drive = google.drive({ version: 'v3', auth: oAuth2Client }); + + const obj = await drive.files.list({ + pageSize : 100, + fields : 'nextPageToken, files(id, name, modifiedTime, properties)', + q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false' + }) + .catch((err)=>{ + return console.error(`Error Listing Google Brews: ${err}`); + }); + + if(obj.data.files.length) { + console.log('List Google Brews:'); + obj.data.files.map((file)=>{ + console.log(`${file.name} (${file.id})`); + }); + } else { + console.log('No files found.'); + } + + const brews = obj.data.files.map((file)=>{ + return { + text : '', + shareId : file.properties.shareId, + editId : file.properties.editId, + createdAt : null, + updatedAt : file.modifiedTime, + gDrive : true, + googleId : file.id, + + title : file.properties.title, + description : '', + tags : '', + published : false, + authors : [req.account.username], //TODO: properly save and load authors to google drive + systems : [] + }; + }); + + return brews; + }, + + existsGoogleBrew : async (auth, id)=>{ + const drive = google.drive({ version: 'v3', auth: auth }); + + const result = await drive.files.get({ fileId: id }) + .catch((err)=>{ + return false; + }); + + if(result){return true;} + + return false; + }, + + updateGoogleBrew : async (req, res)=>{ + oAuth2Client = GoogleActions.authCheck(req.account, res); + + const drive = google.drive({ version: 'v3', auth: oAuth2Client }); + const brew = req.body; + + const media = { + mimeType : 'text/plain', + body : brew.text + }; + + let obj; + + //CHECK IF FILE ALREADY EXISTS + if(await GoogleActions.existsGoogleBrew(oAuth2Client, req.body.googleId) == true) { + //IF SO, JUST UPDATE EXISTING FILE + const fileMetadata = { + 'name' : `${brew.title}.txt`, + 'properties' : { //AppProperties is not accessible + 'shareId' : brew.shareId, + 'editId' : brew.editId, + 'title' : brew.title, + } + }; + + obj = await drive.files.update({ + fileId : req.body.googleId, + resource : fileMetadata, + media : media + }) + .catch((err)=>{ + console.log('Error saving to google'); + console.error(err); + //return res.status(500).send('Error while saving'); + }); + } else { + //IF NOT, CREATE NEW FILE + const folderId = await GoogleActions.getGoogleFolder(req, res); + const fileMetadata = { + 'name' : `${brew.title}.txt`, + 'parents' : [folderId], + 'properties' : { //AppProperties is not accessible + 'shareId' : nanoid(12), + 'editId' : nanoid(12), + 'title' : brew.title, + } + }; + + obj = await drive.files.create({ + resource : fileMetadata, + media : media + }) + .catch((err)=>{ + console.log('Error saving to google'); + console.error(err); + }); + } + + if(obj) { + //Update permissions + const permissions = { + 'type' : 'anyone', + 'role' : 'writer', + }; + + await drive.permissions.create({ + resource : permissions, + fileId : obj.data.id, + fields : 'id', + }) + .catch((err)=>{ + console.log('Error updating permissions'); + console.error(err); + }); + + response = { + brew : brew, + googleId : obj.data.id + }; + + return res.status(200).send(response); + } + }, + + newGoogleBrew : async (auth, brew)=>{ + console.log('CREATE GOOGLE BREW'); + const drive = google.drive({ version: 'v3', auth: auth }); + + const media = { + mimeType : 'text/plain', + body : brew.text + }; + + const folderId = await GoogleActions.getGoogleFolderNew(auth); + + const fileMetadata = { + 'name' : `${brew.title}.txt`, + 'parents' : [folderId], + 'properties' : { //AppProperties is not accessible + 'shareId' : nanoid(12), + 'editId' : nanoid(12), + 'title' : brew.title, + } + }; + + const obj = await drive.files.create({ + resource : fileMetadata, + media : media + }) + .catch((err)=>{ + console.log('Error saving to google'); + console.error(err); + return res.status(500).send('Error while creating google brew'); + }); + + if(!obj) return; + + const permissions = { + 'type' : 'anyone', + 'role' : 'writer', + }; + + await drive.permissions.create({ + resource : permissions, + fileId : obj.data.id, + fields : 'id', + }) + .catch((err)=>{ + console.log('Error updating permissions'); + console.error(err); + }); + + const newHomebrew = { + text : brew.text, + shareId : fileMetadata.properties.shareId, + editId : fileMetadata.properties.editId, + createdAt : null, + updatedAt : null, + gDrive : true, + googleId : obj.data.id, + + title : brew.title, + description : '', + tags : '', + published : false, + authors : [], + systems : [] + }; + + return newHomebrew; + }, + + readFileMetadata : async (auth, id, accessId, accessType)=>{ + const drive = google.drive({ version: 'v3', auth }); + + const obj = await drive.files.get({ + fileId : id, + fields : 'properties' + }) + .catch((err)=>{ + console.log('Error loading from Google'); + console.error(err); + return; + }); + + console.log(`ACCESS TYPE: ${accessType}`); + + if(obj) { + if(accessType == 'edit' && obj.data.properties.editId != accessId){ + throw ('Edit ID does not match'); + } else if(accessType == 'share' && obj.data.properties.shareId != accessId){ + throw ('Share ID does not match'); + } + + const file = await drive.files.get({ + fileId : id, + alt : 'media' + }) + .catch((err)=>{ + console.log('Error getting file contents from Google'); + console.error(err); + }); + + const brew = { + text : file.data, + shareId : obj.data.properties.shareId, + editId : obj.data.properties.editId, + createdAt : null, + updatedAt : null, + gDrive : true, + googleId : id, + + title : obj.data.properties.title, + description : '', + tags : '', + published : false, + authors : [], + systems : [] + }; + + return (brew); + } + }, + + deleteGoogleBrew : async (req, res, id)=>{ + + console.log('trying to delete google brew'); + oAuth2Client = GoogleActions.authCheck(req.account, res); + const drive = google.drive({ version: 'v3', auth: oAuth2Client }); + + const googleId = id.slice(0, -12); + const accessId = id.slice(-12); + + const obj = await drive.files.get({ + fileId : googleId, + fields : 'properties' + }) + .catch((err)=>{ + console.log('Error loading from Google'); + console.error(err); + return; + }); + + if(obj && obj.data.properties.editId != accessId) { + throw ('Not authorized to delete this Google brew'); + } + + await drive.files.delete({ + fileId : googleId + }) + .catch((err)=>{ + console.log('Can\'t delete Google file'); + console.error(err); + }); + + return res.status(200).send(); + } +}; + +module.exports = GoogleActions; diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 4360e7260..285fdcb94 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const HomebrewModel = require('./homebrew.model.js').model; const router = require('express').Router(); const zlib = require('zlib'); +const GoogleActions = require('./googleActions.js'); // const getTopBrews = (cb) => { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { @@ -20,17 +21,22 @@ const getGoodBrewTitle = (text)=>{ }; const newBrew = (req, res)=>{ - const authors = (req.account) ? [req.account.username] : []; + const brew = req.body; + brew.authors = (req.account) ? [req.account.username] : []; - const newHomebrew = new HomebrewModel(_.merge({}, - req.body, - { authors: authors } - )); - - if(!newHomebrew.title) { - newHomebrew.title = getGoodBrewTitle(newHomebrew.text); + if(!brew.title) { + brew.title = getGoodBrewTitle(brew.text); } + delete brew.editId; + delete brew.shareId; + delete brew.googleId; + + console.log('creating new local file using this data:'); + console.log(brew); + const newHomebrew = new HomebrewModel(brew); + console.log('this is the new local homebrew'); + console.log(newHomebrew); // 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 @@ -41,11 +47,17 @@ const newBrew = (req, res)=>{ console.error(err, err.toString(), err.stack); return res.status(500).send(`Error while creating new brew, ${err.toString()}`); } - return res.json(obj); + + console.log('NEW BREW. gOING TO RETURN THIS:'); + obj = obj.toObject(); + obj.gDrive = false; + console.log(obj); + return res.status(200).send(obj); }); }; const updateBrew = (req, res)=>{ + console.log('UPDATE LOCAL'); HomebrewModel.get({ editId: req.params.id }) .then((brew)=>{ brew = _.merge(brew, req.body); @@ -62,8 +74,13 @@ const updateBrew = (req, res)=>{ brew.markModified('authors'); brew.markModified('systems'); + console.log('saving this brew'); + console.log(brew); + brew.save((err, obj)=>{ if(err) throw err; + console.log('sending this updated brew:'); + console.log(obj); return res.status(200).send(obj); }); }) @@ -103,11 +120,41 @@ const deleteBrew = (req, res)=>{ }); }; +newGoogleBrew = async (req, res, next)=>{ + let oAuth2Client; + + console.log('newGoogleBrew (API)'); + + try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); } + + const brew = req.body; + brew.authors = (req.account) ? [req.account.username] : []; + + if(!brew.title) { + brew.title = getGoodBrewTitle(brew.text); + } + + delete brew.editId; + delete brew.shareId; + delete brew.googleId; + + req.body = brew; + + console.log(oAuth2Client); + + const newHomebrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew); + + return res.status(200).send(newHomebrew); +}; + router.post('/api', newBrew); +router.post('/api/newGoogle/', newGoogleBrew); router.put('/api/:id', updateBrew); router.put('/api/update/:id', updateBrew); +router.put('/api/updateGoogle/:id', (req, res)=>{GoogleActions.updateGoogleBrew(req, res);}); router.delete('/api/:id', deleteBrew); router.get('/api/remove/:id', deleteBrew); +router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);}); module.exports = router; diff --git a/server/homebrew.model.js b/server/homebrew.model.js index 25c7a38e1..f16e4f93a 100644 --- a/server/homebrew.model.js +++ b/server/homebrew.model.js @@ -1,11 +1,11 @@ const mongoose = require('mongoose'); -const shortid = require('shortid'); +const { nanoid } = require('nanoid'); const _ = require('lodash'); const zlib = require('zlib'); const HomebrewSchema = mongoose.Schema({ - shareId : { type: String, default: shortid.generate, index: { unique: true } }, - editId : { type: String, default: shortid.generate, index: { unique: true } }, + shareId : { type: String, default: nanoid(12), index: { unique: true } }, + editId : { type: String, default: nanoid(12), index: { unique: true } }, title : { type: String, default: '' }, text : { type: String, default: '' }, textBin : { type: Buffer }, @@ -24,7 +24,6 @@ const HomebrewSchema = mongoose.Schema({ }, { versionKey: false }); - HomebrewSchema.methods.sanatize = function(full=false){ const brew = this.toJSON(); delete brew._id; @@ -35,7 +34,6 @@ HomebrewSchema.methods.sanatize = function(full=false){ return brew; }; - HomebrewSchema.methods.increaseView = function(){ return new Promise((resolve, reject)=>{ this.lastViewed = new Date(); @@ -47,8 +45,6 @@ HomebrewSchema.methods.increaseView = function(){ }); }; - - HomebrewSchema.statics.get = function(query){ return new Promise((resolve, reject)=>{ Homebrew.find(query, (err, brews)=>{ @@ -77,11 +73,9 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){ }); }; - - const Homebrew = mongoose.model('Homebrew', HomebrewSchema); module.exports = { schema : HomebrewSchema, model : Homebrew, -}; \ No newline at end of file +}; diff --git a/server/token.js b/server/token.js new file mode 100644 index 000000000..975120316 --- /dev/null +++ b/server/token.js @@ -0,0 +1,36 @@ +const jwt = require('jwt-simple'); + +// Load configuration values +const config = require('nconf') + .argv() + .env({ lowerCase: true }) // Load environment variables + .file('environment', { file: `config/${process.env.NODE_ENV}.json` }) + .file('defaults', { file: 'config/default.json' }); + +// Generate an Access Token for the given User ID +const generateAccessToken = (account)=>{ + const payload = account; + + // When the token was issued + payload.issued = (new Date()); + // Which service issued the Token + payload.issuer = config.get('authentication_token_issuer'); + // Which service is the token intended for + payload.audience = config.get('authentication_token_audience'); + // The signing key for signing the token + delete payload.password; + delete payload._id; + + console.log('THE PAYLOAD'); + console.log(payload); + + const secret = config.get('authentication_token_secret'); + + const token = jwt.encode(payload, secret); + + return token; +}; + +module.exports = { + generateAccessToken : generateAccessToken +};