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
+
+ ;
+ } else {
+ return
+
+ ;
+ }
+
},
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
+
+ ;
+ },
+
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
+};