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/changelog.md b/changelog.md index 2bb9d0624..a1d95b955 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # changelog +### Wednesday, 07/10/2020 - v2.10.0 +- Google Drive integration -- Sign in with your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal Google Drive storage, and Google will keep a backup of each version! No more lost work surprises! + ### Friday, 28/08/2020 - v2.9.2 - Many dependency updates - Finally fixed this changelog page to not run off the edge :P @@ -40,11 +43,13 @@ ### Friday, 03/03/2017 - v2.7.3 - Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews. +``` +``` + ### Saturday, 18/02/2017 - v2.7.2 - Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309) -``` -``` + ### Thursday, 19/01/2017 - v2.7.0 - Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282) @@ -81,13 +86,13 @@ - Added in a snippet for a split table - Added an account nav item to new page +\page + ### Sunday, 27/11/2016 - v2.5.1 - Fixed the column rendering on the new user page. Really should have tested that better - Added a hover tooltip to fully read the brew description - Made the brew items take up only 25% allowing you to view more per row. -\page - ### Wednesday, 23/11/2016 - v2.5.0 - Metadata can now be added to brews - Added a metadata editor onto the edit and new pages @@ -125,6 +130,9 @@ ### Friday, 29/07/2016 - v2.2.7 - Adding in descriptive note blocks. (Thanks calculuschild!) +``` +``` + ### Thursday, 07/07/2016 - v2.2.6 - Added a new nav item on the homepage for accessing both recently viewed and edited brews (thanks [ChosenSeraph!](https://github.com/stolksdorf/homebrewery/issues/147)) @@ -149,6 +157,9 @@ - Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `
` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step! - Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins! - The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up. + +\page + - Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation. - Paragraphs now indent properly after lists (thanks u/slitjen!) @@ -156,8 +167,6 @@ - Updated the issue template for (hopefully) better reporting - Added suggestion to use chrome while PDF printing -\page - ### Wednesday, 25/05/2016 -v2.0.5 - The class table generators have the proper ability score improvement progression. diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx index d093424bc..973a01035 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -4,7 +4,7 @@ const createClass = require('create-react-class'); const _ = require('lodash'); const cx = require('classnames'); //Unused variable -const DISMISS_KEY = 'dismiss_notification7-24-19'; +const DISMISS_KEY = 'dismiss_notification7-10-20'; const NotificationPopup = createClass({ getInitialState : function() { @@ -22,17 +22,22 @@ const NotificationPopup = createClass({ notifications : { psa : function(){ return
  • - Known bug: Grey Shadow Boxes
    - The shadows around certain brew elements such as notes and statblocks might appear as a solid grey box when generating a PDF.   - - See this Reddit post - for updates and possible workarounds. + Google Drive Integration!
    + We have added Google Drive integration to the Homebrewery! Sign in with + your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal + Google Drive storage, and Google will keep a backup of each version! No more lost work surprises! +

    + However, we are aware that there may be uncaught bugs. We encourage you to copy your brew into a text document before transferring to Google + Drive just in case any issues arise as this update is rolled out. +

    + Note: Transferring an existing brew to Google Drive will change the edit and share links of your document. If you have shared your + document online, remember to update the links there as well.
  • ; }, faq : function(){ return
  • Protect your work!
    - At the moment we do not save a history of your projects, so please make frequent backups of your brews!   + If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!   See the FAQ to learn how to avoid losing your work! diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less index 2cf1fbea4..69bffd583 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less @@ -9,11 +9,11 @@ .notificationPopup{ position : relative; float : right; - display : inline-block; + display : inline-block; width : 350px; - padding : 20px; + padding : 15px; padding-bottom : 10px; - padding-left : 85px; + padding-left : 55px; background-color : @blue; color : white; a{ @@ -22,8 +22,8 @@ } i.info{ position : absolute; - top : 24px; - left : 24px; + top : 12px; + left : 12px; opacity : 0.8; font-size : 2.5em; } 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/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx index 011adae11..3dfe61203 100644 --- a/client/homebrew/navbar/navbar.jsx +++ b/client/homebrew/navbar/navbar.jsx @@ -4,6 +4,7 @@ const createClass = require('create-react-class'); const _ = require('lodash'); const Nav = require('naturalcrit/nav/nav.jsx'); +const PatreonNavItem = require('./patreon.navitem.jsx'); const Navbar = createClass({ getInitialState : function() { @@ -40,7 +41,7 @@ const Navbar = createClass({
    The Homebrewery
    {`v${this.state.ver}`} - + {/*this.renderChromeWarning()*/} {this.props.children} diff --git a/client/homebrew/navbar/navbar.less b/client/homebrew/navbar/navbar.less index bb2a84362..36cbdf935 100644 --- a/client/homebrew/navbar/navbar.less +++ b/client/homebrew/navbar/navbar.less @@ -1,4 +1,12 @@ @navbarHeight : 28px; +@keyframes coloring { + //from {color: white;} + //to {color: red;} + 0% {color: pink;} + 50% {color: pink;} + 75% {color: red;} + 100% {color: pink;} +} .homebrew nav{ .homebrewLogo{ .animate(color); @@ -47,11 +55,16 @@ text-transform : initial; } .patreon.navItem{ + border-left : 1px solid #666; + border-right : 1px solid #666; + &:hover i { + color: red; + } i{ .animate(color); - &:hover{ - color : @red; - } + animation-name: coloring; + animation-duration: 2s; + color: pink; } } .recent.navItem{ @@ -125,4 +138,4 @@ text-align : center; } } -} \ No newline at end of file +} diff --git a/client/homebrew/navbar/patreon.navitem.jsx b/client/homebrew/navbar/patreon.navitem.jsx index e6a9ebeed..03fb69af4 100644 --- a/client/homebrew/navbar/patreon.navitem.jsx +++ b/client/homebrew/navbar/patreon.navitem.jsx @@ -6,9 +6,9 @@ module.exports = function(props){ return help out ; -}; \ No newline at end of file +}; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index cfa886fe2..38f9eb482 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,15 @@ const EditPage = createClass({ } }, - save : function(){ + toggleGoogleStorage : function(){ + this.setState((prevState)=>({ + saveGoogle : !prevState.saveGoogle, + isSaving : false, + errors : null + }), ()=>this.trySave()); + }, + + save : async function(){ if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); this.setState((prevState)=>({ @@ -135,22 +152,99 @@ 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; + } + } else { + if(transfer) { + 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'); + }); + + this.savedBrew = res.body; + history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID + } else { + 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; + } + } + + this.setState((prevState)=>({ + brew : _.merge({}, prevState.brew, { + googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null, + editId : this.savedBrew.editId, + shareId : this.savedBrew.shareId + }), + isPending : false, + isSaving : false, + })); + }, + + renderGoogleDriveIcon : function(){ + if(this.state.saveGoogle) { + return + googleDriveActive + ; + } else { + return + googleDriveInactive + ; + } + }, renderSaveButton : function(){ @@ -161,6 +255,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 +290,13 @@ const EditPage = createClass({ return saved.; } }, + + processShareId : function() { + return this.state.brew.googleId ? + this.state.brew.googleId + this.state.brew.shareId : + this.state.brew.shareId; + }, + renderNavbar : function(){ return @@ -190,12 +304,13 @@ const EditPage = createClass({ + {this.renderGoogleDriveIcon()} {this.renderSaveButton()} - + Share - + 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/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 517980aaf..8795156e7 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -8,7 +8,6 @@ const { Meta } = require('vitreum/headtags'); const Nav = require('naturalcrit/nav/nav.jsx'); const Navbar = require('../../navbar/navbar.jsx'); -const PatreonNavItem = require('../../navbar/patreon.navitem.jsx'); const IssueNavItem = require('../../navbar/issue.navitem.jsx'); const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const AccountNavItem = require('../../navbar/account.navitem.jsx'); @@ -56,7 +55,6 @@ const HomePage = createClass({ renderNavbar : function(){ return - Changelog diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 8f617a1d5..2fcc27481 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 && 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..c04a95435 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -45,6 +45,12 @@ const SharePage = createClass({ } }, + processShareId : function() { + return this.props.brew.googleId ? + this.props.brew.googleId + this.props.brew.shareId : + this.props.brew.shareId; + }, + render : function(){ return
    @@ -54,8 +60,8 @@ const SharePage = createClass({ - - + + source 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..d229c8c55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "homebrewery", - "version": "2.8.2", + "version": "2.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -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..f5f060b8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebrewery", "description": "Create authentic looking D&D homebrews using only markdown", - "version": "2.8.2", + "version": "2.10.0", "engines": { "node": "12.16.x" }, @@ -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..0f83c23f6 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 = { + google_client_id : config.get('google_client_id'), + google_client_secret : config.get('google_client_secret') + }; 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,70 +65,142 @@ 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('google_api_key'), 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 && req.account.googleId){ + 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('google_api_key'), 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('google_api_key'), 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 app.get('/print/: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('google_api_key'), googleId, shareId, 'share') .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)=>{ + req.brew = brew.sanatize(true); + return next(); + }) + .catch((err)=>{ + console.log(err); + return res.status(400).send(`Can't get that`); + }); + } +}); + +app.get('/source/:id', (req, res)=>{ + }); @@ -131,10 +215,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)=>{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..e976c0537 --- /dev/null +++ b/server/googleActions.js @@ -0,0 +1,312 @@ +/* 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)=>{ + 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('google_client_id'), + config.get('google_client_secret'), + '/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); + + //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 (auth)=>{ + 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){ + 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('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 (auth, brew)=>{ + const drive = google.drive({ version: 'v3', auth: auth }); + + if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) { + + await drive.files.update({ + fileId : brew.googleId, + resource : { name : `${brew.title}.txt`, + properties : { title: brew.title } //AppProperties is not accessible via API key + }, + media : { mimeType : 'text/plain', + body : brew.text } + }) + .catch((err)=>{ + console.log('Error saving to google'); + console.error(err); + //return res.status(500).send('Error while saving'); + }); + } + + return (brew); + }, + + newGoogleBrew : async (auth, brew)=>{ + const drive = google.drive({ version: 'v3', auth: auth }); + + const media = { + mimeType : 'text/plain', + body : brew.text + }; + + const folderId = await GoogleActions.getGoogleFolder(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.error(err); + return res.status(500).send('Error while creating google brew'); + }); + + if(!obj) return; + + await drive.permissions.create({ + resource : { type : 'anyone', + role : 'writer' }, + 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: auth }); + console.log(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)=>{ + + 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..108201d5e 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,18 @@ 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; + + 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 @@ -41,7 +43,10 @@ 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); + + obj = obj.toObject(); + obj.gDrive = false; + return res.status(200).send(obj); }); }; @@ -103,49 +108,48 @@ const deleteBrew = (req, res)=>{ }); }; +const newGoogleBrew = async (req, res, next)=>{ + let oAuth2Client; + + 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 newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew); + + return res.status(200).send(newBrew); +}; + +const updateGoogleBrew = async (req, res, next)=>{ + let oAuth2Client; + + try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); } + + const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, req.body); + + return res.status(200).send(updatedBrew); +}; + 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', updateGoogleBrew); 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; - -/* -module.exports = function(app) { - app; - - app.get('/api/search', mw.adminOnly, function(req, res) { - var page = req.query.page || 0; - var count = req.query.count || 20; - - var query = {}; - if (req.query && req.query.id) { - query = { - "$or": [{ - editId : req.query.id - }, { - shareId : req.query.id - }] - }; - } - - HomebrewModel.find(query, { - text : 0 //omit the text - }, { - skip: page*count, - limit: count*1 - }, function(err, objs) { - if (err) console.error(err); - return res.json({ - page : page, - count : count, - total : homebrewTotal, - brews : objs - }); - }); - }) - - return app; -} -*/ diff --git a/server/homebrew.model.js b/server/homebrew.model.js index 25c7a38e1..785459da1 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: ()=>{return nanoid(12);}, index: { unique: true } }, + editId : { type: String, default: ()=>{return 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..40d76a484 --- /dev/null +++ b/server/token.js @@ -0,0 +1,33 @@ +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; + + const secret = config.get('authentication_token_secret'); + + const token = jwt.encode(payload, secret); + + return token; +}; + +module.exports = { + generateAccessToken : generateAccessToken +};