From b3371fca534c03a8a0f00990c714cc9391cc1523 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 6 Mar 2019 17:05:44 +0200 Subject: [PATCH 1/5] Added fetching User data from patreon and writing it to the DiceCloud user database --- app/Model/Users/Users.js | 30 ++ app/client/views/user/profile/profile.html | 6 + app/client/views/user/profile/profile.js | 31 ++ app/package-lock.json | 515 ++++++++++++--------- app/package.json | 2 + app/server/patreon/patreon.js | 135 ++++++ app/server/publications/user.js | 1 + 7 files changed, 494 insertions(+), 226 deletions(-) create mode 100644 app/server/patreon/patreon.js diff --git a/app/Model/Users/Users.js b/app/Model/Users/Users.js index 6878df1d..82a2ccae 100644 --- a/app/Model/Users/Users.js +++ b/app/Model/Users/Users.js @@ -74,6 +74,36 @@ Schemas.User = new SimpleSchema({ type: String, optional: true, }, + patreon: { + type: Object, + optional: true, + }, + "patreon.accessToken": { + type: String, + optional: true, + }, + "patreon.refreshToken": { + type: String, + optional: true, + }, + "patreon.userId": { + type: String, + optional: true, + }, + "patreon.entitledCents": { + type: Number, + decimal: false, + optional: true, + }, + "patreon.entitledCentsOverride": { + type: Number, + decimal: false, + optional: true, + }, + "patreon.error": { + type: String, + optional: true, + }, }); Meteor.users.attachSchema(Schemas.User); diff --git a/app/client/views/user/profile/profile.html b/app/client/views/user/profile/profile.html index 5a8e6a50..488ec1f7 100644 --- a/app/client/views/user/profile/profile.html +++ b/app/client/views/user/profile/profile.html @@ -66,6 +66,12 @@
{{> atForm state="signIn"}}
+
+ Tier: {{tier}} + + Connect Patreon Account + +
{{> atNavButton }} diff --git a/app/client/views/user/profile/profile.js b/app/client/views/user/profile/profile.js index ca64f6ea..c1e63b09 100644 --- a/app/client/views/user/profile/profile.js +++ b/app/client/views/user/profile/profile.js @@ -1,3 +1,7 @@ +import { format as formatUrl } from 'url'; + +const CLIENT_ID = "zv38izfGZDf8s_Z9BI5kICjGGnvs45PawHYu6cqsTqftwZ_5DZFqEGKZfdP8Q6I2"; + Template.profile.onCreated(function(){ this.showApiKey = new ReactiveVar(false); }); @@ -12,6 +16,33 @@ Template.profile.helpers({ showApiKey: function(){ return Template.instance().showApiKey.get(); }, + patreonLoginUrl: function(){ + return formatUrl({ + protocol: 'https', + host: 'patreon.com', + pathname: '/oauth2/authorize', + query: { + response_type: 'code', + client_id: CLIENT_ID, + redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect', + state: Meteor.userId(), + scope: 'identity', + }, + }); + }, + patreon: function(){ + let user = Meteor.user(); + return user && user.patreon || {}; + }, + tier: function(){ + let user = Meteor.user(); + if (!user) return; + patreon = user.patreon; + if (!patreon) return; + let tier = patreon.entitledCents || 0; + if (patreon.entitledCentsOverride > tier) tier = patreon.entitledCentsOverride; + return tier; + } }); Template.profile.events({ diff --git a/app/package-lock.json b/app/package-lock.json index b7b60acc..486bfe7c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -26,21 +26,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - }, - "dependencies": { - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - } + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ansi-regex": { @@ -53,16 +46,34 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "bcrypt": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", @@ -72,6 +83,14 @@ "node-pre-gyp": "0.6.36" } }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, "block-stream": { "version": "0.0.9", "resolved": false, @@ -108,6 +127,11 @@ "window-or-global": "^1.0.1" } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -135,6 +159,14 @@ "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", @@ -155,6 +187,14 @@ "which": "^1.2.9" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -168,11 +208,33 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "dijkstrajs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", "integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=" }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -195,15 +257,20 @@ "strip-eof": "^1.0.0" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -228,36 +295,26 @@ "locate-path": "^2.0.0" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "1.0.6", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" - }, - "dependencies": { - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - } } }, + "form-urlencoded": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-2.0.9.tgz", + "integrity": "sha512-fWUzNiOnYa126vFAT6TFXd1mhJrvD8IqmQ9ilZPjkLYQfaRreBr5fIUoOpPlWtqaAG64nzoE7u5zSetifab9IA==" + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -268,6 +325,14 @@ "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": false, @@ -279,11 +344,11 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "^5.1.0", + "ajv": "^6.5.5", "har-schema": "^2.0.0" } }, @@ -300,74 +365,14 @@ "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" - }, - "dependencies": { - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" } }, "inherits": { @@ -401,11 +406,24 @@ "number-is-nan": "^1.0.0" } }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "isarray": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz", @@ -416,10 +434,49 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonapi-datastore": { + "version": "0.4.0-beta", + "resolved": "https://registry.npmjs.org/jsonapi-datastore/-/jsonapi-datastore-0.4.0-beta.tgz", + "integrity": "sha1-tJn86STUXivDxheGgVIAY+I2HxA=" }, "jsprim": { "version": "1.4.1", @@ -430,13 +487,6 @@ "extsprintf": "1.3.0", "json-schema": "0.2.3", "verror": "1.10.0" - }, - "dependencies": { - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - } } }, "lcid": { @@ -1228,16 +1278,16 @@ } }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", "requires": { - "mime-db": "~1.33.0" + "mime-db": "~1.38.0" } }, "mimic-fn": { @@ -1255,6 +1305,15 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, "node-pre-gyp": { "version": "0.6.36", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", @@ -1511,6 +1570,11 @@ "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -1592,6 +1656,17 @@ "pify": "^2.0.0" } }, + "patreon": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/patreon/-/patreon-0.4.1.tgz", + "integrity": "sha512-aLhjx4rg2BArTq0Kg61MrM4dkJnTQ9kPN8F6a2IlQoYVEtIH7kUK/dprClTx+QYQKlXMfKksN9NCux1YarQJsQ==", + "requires": { + "form-urlencoded": "^2.0.4", + "is-plain-object": "^2.0.4", + "isomorphic-fetch": "^2.2.1", + "jsonapi-datastore": "^0.4.0-beta" + } + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -1612,6 +1687,16 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "qrcode": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.3.0.tgz", @@ -1687,103 +1772,30 @@ "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" }, "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - }, - "dependencies": { - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" - } + "uuid": "^3.3.2" } }, "require-directory": { @@ -1883,6 +1895,16 @@ } } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -1953,6 +1975,22 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz", "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==" }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2164,10 +2202,11 @@ } }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { + "psl": "^1.1.24", "punycode": "^1.4.1" }, "dependencies": { @@ -2178,11 +2217,37 @@ } } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "underscore": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -2200,13 +2265,6 @@ "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - } } }, "webcomponents.js": { @@ -2214,6 +2272,11 @@ "resolved": "https://registry.npmjs.org/webcomponents.js/-/webcomponents.js-0.7.24.tgz", "integrity": "sha1-IRb7+hRo7EFqe+/aozPh0Rj2nAQ=" }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/app/package.json b/app/package.json index 29cbefcc..bfc69dbf 100644 --- a/app/package.json +++ b/app/package.json @@ -19,7 +19,9 @@ "fibers": "^2.0.2", "file-saver": "^2.0.1", "meteor-node-stubs": "^0.3.3", + "patreon": "^0.4.1", "qrcode": "^1.3.0", + "request": "^2.88.0", "source-map-support": "^0.5.9", "underscore": "^1.9.1" } diff --git a/app/server/patreon/patreon.js b/app/server/patreon/patreon.js new file mode 100644 index 00000000..feb03927 --- /dev/null +++ b/app/server/patreon/patreon.js @@ -0,0 +1,135 @@ +import request from 'request'; + +const CLIENT_ID = "zv38izfGZDf8s_Z9BI5kICjGGnvs45PawHYu6cqsTqftwZ_5DZFqEGKZfdP8Q6I2"; +const CLIENT_SECRET = Meteor.settings.patreon.clientSecret; + +// Handle redirects from patreon +Router.map(function () { + this.route("patreon-redirect", { + path: "/patreon-redirect", + where: "server", + action: function () { + let route = this; + let userId = route.params.query.state; + let singleUseCode = route.params.query.code; + requestToken(singleUseCode, Meteor.bindEnvironment((error, response, body) => { + // Should return an access token, valid for 1 month, which needs to be + // stored and used to make requests on behalf of the user + if (error){ + writePatreonError(userId, error); + return; + } + let token; + try { + token = JSON.parse(body); + writePatreonToken(userId, token); + } catch(error) { + writePatreonError(userId, error); + return; + } + getIdentity(token.access_token, Meteor.bindEnvironment((error, response, body) => { + if (error){ + writePatreonError(userId, error); + return; + } + try { + let identity = JSON.parse(body); + let entitledAmount = +identity.included[0].attributes + .currently_entitled_amount_cents; + //TODO also write the patreon userId + writeEntitledCents(userId, entitledAmount); + } catch(error) { + writePatreonError(userId, error); + } + })); + })); + route.response.writeHead(302, { + 'Location': Meteor.absoluteUrl() + "account", + }); + route.response.end(); + }, + }); +}); + +const requestToken = function(singleUseCode, callback){ + request({ + method: "POST", + uri: "https://www.patreon.com/api/oauth2/token", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + qs: { + code: singleUseCode, + grant_type: "authorization_code", + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect', + }, + }, callback); +} + +// Should return a new access token for the user +// callback is called with (error, response, body) +const refreshAccessToken = function(refreshToken, callback){ + request({ + method: "POST", + uri: "https://www.patreon.com/api/oauth2/token", + qs: { + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + } + }, callback); +}; + +const getIdentity = function(accessToken, callback){ + request({ + uri: "https://www.patreon.com/api/oauth2/v2/identity", + headers:{ + Authorization: "Bearer " + accessToken, + }, + qs: { + "include": "memberships", + "fields[member]": "currently_entitled_amount_cents", + } + }, callback); +}; + +const writePatreonToken = function(userId, {access_token, refresh_token}){ + console.log('Writing token ') + console.log({access_token, refresh_token}); + Meteor.users.update(userId, { + $set: { + "patreon.accessToken": access_token, + "patreon.refreshToken": refresh_token, + }, + $unset: { + "patreon.error": 1, + }, + }); + console.log(Meteor.users.findOne(userId).patreon); +}; + +const writeEntitledCents = function(userId, amount){ + console.log('Writing cents ') + console.log(arguments); + Meteor.users.update(userId, { + $set: { + "patreon.entitledCents": amount, + }, + $unset: { + "patreon.error": 1, + }, + }); + console.log(Meteor.users.findOne(userId).patreon); +}; + +const writePatreonError = function(userId, error){ + console.error(error); + Meteor.users.update(userId, { + $set: { + "patreon.error": error.toString(), + }, + }); +} diff --git a/app/server/publications/user.js b/app/server/publications/user.js index ab979722..fc6c9c6a 100644 --- a/app/server/publications/user.js +++ b/app/server/publications/user.js @@ -7,6 +7,7 @@ Meteor.publish("user", function(){ apiKey: 1, librarySubscriptions: 1, lastPatreonPostClicked: 1, + patreon: 1, }}), PatreonPosts.find({},{sort: {dateAdded: -1}, limit: 1}) ]; From 857213f157129abca38253d9cd040d36e2307e1e Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 7 Mar 2019 13:35:31 +0200 Subject: [PATCH 2/5] Improved Patreon linking --- app/Model/Users/Users.js | 5 + app/client/views/layout/layout.html | 3 + app/client/views/layout/layout.js | 14 ++ app/client/views/user/profile/profile.html | 28 ++- app/client/views/user/profile/profile.js | 24 ++- app/server/patreon/patreon.js | 201 +++++++++++++++++---- 6 files changed, 224 insertions(+), 51 deletions(-) diff --git a/app/Model/Users/Users.js b/app/Model/Users/Users.js index 82a2ccae..4f4aaa38 100644 --- a/app/Model/Users/Users.js +++ b/app/Model/Users/Users.js @@ -86,9 +86,14 @@ Schemas.User = new SimpleSchema({ type: String, optional: true, }, + "patreon.tokenExpiryDate": { + type: Date, + optional: true, + }, "patreon.userId": { type: String, optional: true, + index: 1, }, "patreon.entitledCents": { type: Number, diff --git a/app/client/views/layout/layout.html b/app/client/views/layout/layout.html index 50d6cb0c..9bb47c02 100644 --- a/app/client/views/layout/layout.html +++ b/app/client/views/layout/layout.html @@ -21,6 +21,9 @@ {{profileLink}} + + {{patreonTier}} tier + {{else}} Sign in diff --git a/app/client/views/layout/layout.js b/app/client/views/layout/layout.js index f4937071..a18c74c8 100644 --- a/app/client/views/layout/layout.js +++ b/app/client/views/layout/layout.js @@ -17,6 +17,20 @@ Template.appDrawer.helpers({ let post = PatreonPosts.findOne({}, {sort: {date: -1}}); return (post && post.link) || 'https://www.patreon.com/dicecloud'; }, + patreonTier: function(){ + let user = Meteor.user(); + if (!user) return; + patreon = user.patreon; + if (!patreon) return "$0"; + let entitledCents = patreon.entitledCents || 0; + if (patreon.entitledCentsOverride > entitledCents){ + return "$" + (patreon.entitledCentsOverride / 100).toFixed(0); + } else if (patreon.entitledCents === undefined){ + return "$0"; + } else { + return "$" + (patreon.entitledCents / 100).toFixed(0); + } + }, }); let drawerLayout; diff --git a/app/client/views/user/profile/profile.html b/app/client/views/user/profile/profile.html index 488ec1f7..c8b455f4 100644 --- a/app/client/views/user/profile/profile.html +++ b/app/client/views/user/profile/profile.html @@ -62,16 +62,32 @@ {{/if}} + + + Patreon + + {{#if patreon.accessToken}} + + {{tier}} tier + + + + + + {{else}} + + + + Connect Patreon account + + + + {{/if}} +
{{> atForm state="signIn"}}
-
- Tier: {{tier}} - - Connect Patreon Account - -
{{> atNavButton }} diff --git a/app/client/views/user/profile/profile.js b/app/client/views/user/profile/profile.js index c1e63b09..18e303fe 100644 --- a/app/client/views/user/profile/profile.js +++ b/app/client/views/user/profile/profile.js @@ -1,9 +1,10 @@ import { format as formatUrl } from 'url'; -const CLIENT_ID = "zv38izfGZDf8s_Z9BI5kICjGGnvs45PawHYu6cqsTqftwZ_5DZFqEGKZfdP8Q6I2"; +const CLIENT_ID = Meteor.settings.public.patreon.clientId; Template.profile.onCreated(function(){ this.showApiKey = new ReactiveVar(false); + this.loadingPatreon = new ReactiveVar(false); }); Template.profile.helpers({ @@ -39,10 +40,17 @@ Template.profile.helpers({ if (!user) return; patreon = user.patreon; if (!patreon) return; - let tier = patreon.entitledCents || 0; - if (patreon.entitledCentsOverride > tier) tier = patreon.entitledCentsOverride; - return tier; - } + let entitledCents = patreon.entitledCents || 0; + if (Template.instance().loadingPatreon.get()){ + return "loading..." + } else if (patreon.entitledCentsOverride > entitledCents){ + return `$ ${(patreon.entitledCentsOverride / 100).toFixed(0)} (Overridden)"`; + } else if (patreon.entitledCents === undefined){ + return "?"; + } else { + return "$" + (patreon.entitledCents / 100).toFixed(0); + } + }, }); Template.profile.events({ @@ -70,4 +78,10 @@ Template.profile.events({ Meteor.call("generateMyApiKey"); instance.showApiKey.set(true); }, + "click .refreshPatreon": function(event, instance){ + instance.loadingPatreon.set(true); + Meteor.call("updateMyPatreonDetails", (error) => { + instance.loadingPatreon.set(false); + }); + }, }); diff --git a/app/server/patreon/patreon.js b/app/server/patreon/patreon.js index feb03927..71d77ad7 100644 --- a/app/server/patreon/patreon.js +++ b/app/server/patreon/patreon.js @@ -1,7 +1,9 @@ import request from 'request'; -const CLIENT_ID = "zv38izfGZDf8s_Z9BI5kICjGGnvs45PawHYu6cqsTqftwZ_5DZFqEGKZfdP8Q6I2"; +const CLIENT_ID = Meteor.settings.public.patreon.clientId; const CLIENT_SECRET = Meteor.settings.patreon.clientSecret; +const CREATOR_ACCESS_TOKEN = Meteor.settings.patreon.creatorAccessToken; +const CAMPAIGN_ID = Meteor.settings.public.patreon.campaignId; // Handle redirects from patreon Router.map(function () { @@ -27,21 +29,7 @@ Router.map(function () { writePatreonError(userId, error); return; } - getIdentity(token.access_token, Meteor.bindEnvironment((error, response, body) => { - if (error){ - writePatreonError(userId, error); - return; - } - try { - let identity = JSON.parse(body); - let entitledAmount = +identity.included[0].attributes - .currently_entitled_amount_cents; - //TODO also write the patreon userId - writeEntitledCents(userId, entitledAmount); - } catch(error) { - writePatreonError(userId, error); - } - })); + updateIdentity(token.access_token, userId); })); route.response.writeHead(302, { 'Location': Meteor.absoluteUrl() + "account", @@ -68,21 +56,6 @@ const requestToken = function(singleUseCode, callback){ }, callback); } -// Should return a new access token for the user -// callback is called with (error, response, body) -const refreshAccessToken = function(refreshToken, callback){ - request({ - method: "POST", - uri: "https://www.patreon.com/api/oauth2/token", - qs: { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - } - }, callback); -}; - const getIdentity = function(accessToken, callback){ request({ uri: "https://www.patreon.com/api/oauth2/v2/identity", @@ -96,40 +69,188 @@ const getIdentity = function(accessToken, callback){ }, callback); }; -const writePatreonToken = function(userId, {access_token, refresh_token}){ - console.log('Writing token ') - console.log({access_token, refresh_token}); +// Should return a new access token for the user +// callback is called with (error, response, body) +const refreshAccessToken = Meteor.wrapAsync(function(refreshToken, userId, callback){ + request({ + method: "POST", + uri: "https://www.patreon.com/api/oauth2/token", + qs: { + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + } + }, Meteor.bindEnvironment((error, response, body) => { + // Should return an access token, valid for 1 month, which needs to be + // stored and used to make requests on behalf of the user + if (error){ + callback(error) + return; + } + let token; + try { + token = JSON.parse(body); + writePatreonToken(userId, token); + callback(undefined, token.access_token); + } catch(error) { + callback(error); + } + })); +}); + +const updateIdentity = Meteor.wrapAsync(function(accessToken, userId, callback){ + getIdentity(accessToken, Meteor.bindEnvironment((error, response, body) => { + if (error){ + writePatreonError(userId, error); + return; + } + try { + let identity = JSON.parse(body); + let membership = identity.included[0]; + let entitledAmount = membership && membership.attributes + .currently_entitled_amount_cents || 0; + let patreonUserId = identity.data.id; + writeEntitledCentsAndId(userId, entitledAmount, patreonUserId); + if (callback) callback(); + } catch(error) { + writePatreonError(userId, error); + if(callback) callback(error); + } + })); +}); + +Meteor.methods({ + updateMyPatreonDetails(){ + const userId = this.userId; + if (!userId) throw new Meteor.Error("not-logged-in", "You must be logged in to update Patreon details"); + const user = Meteor.users.findOne(userId, {fields: {patreon: 1}}); + Meteor.users.update(userId, {$unset: {"patreon.entitledCents": 1}}); + if (!user.patreon || !user.patreon.accessToken){ + throw new Meteor.Error("no-patreon-access", "Patreon access token not found for this user"); + } + let accessToken = user.patreon.accessToken; + if (user.patreon.tokenExpiryDate < new Date()){ + // Token expired, refresh it before continuing + accessToken = refreshAccessToken(user.patreon.refreshToken, userId); + } + updateIdentity(accessToken, userId); + }, +}); + +const writePatreonToken = function(userId, { + access_token, refresh_token, expires_in +}){ + // The expiry date is now plus `expires_in` seconds + let expiryDate = new Date(); + expiryDate.setSeconds(expiryDate.getSeconds() + expires_in); + // Expire a day early so we don't accidentally miss it + expiryDate.setDate(expiryDate.getDate() - 1); + + // Write Meteor.users.update(userId, { $set: { "patreon.accessToken": access_token, "patreon.refreshToken": refresh_token, + "patreon.tokenExpiryDate": expiryDate, }, $unset: { "patreon.error": 1, }, }); - console.log(Meteor.users.findOne(userId).patreon); }; -const writeEntitledCents = function(userId, amount){ - console.log('Writing cents ') - console.log(arguments); +const writeEntitledCentsAndId = function(userId, amount, patreonUserId){ Meteor.users.update(userId, { $set: { "patreon.entitledCents": amount, + "patreon.userId": patreonUserId, }, $unset: { "patreon.error": 1, }, }); - console.log(Meteor.users.findOne(userId).patreon); }; const writePatreonError = function(userId, error){ - console.error(error); + console.error({patreonError: error}); Meteor.users.update(userId, { $set: { "patreon.error": error.toString(), }, }); } + + +const requestMembers = Meteor.wrapAsync(function(cursor, members, callback){ + request({ + uri: `https://www.patreon.com/api/oauth2/v2/campaigns/${CAMPAIGN_ID}/members`, + headers:{ + Authorization: "Bearer " + CREATOR_ACCESS_TOKEN, + }, + qs: { + "include": "user", + "fields[member]": "currently_entitled_amount_cents", + "page[cursor]": cursor, + } + }, (error, reponse, body) => { + if (error){ + callback(error); + return; + } + let json = JSON.parse(body); + if (json.errors) { + callback(json.errors); + return; + } + let newMembers = json.data.map(member => ({ + id: member.relationships.user.data.id, + entitledCents: member.attributes.currently_entitled_amount_cents, + })); + members.push(...newMembers); + let next = json.meta.pagination.cursors && json.meta.pagination.cursors.next; + if (next){ + callback(undefined, next); + } else { + callback(undefined); + } + }); +}); + +const updatePatreonMembersEntitledCents = function(){ + let next = ""; + let members = []; + do { + next = requestMembers(next, members); + } while (next) + members.forEach(({id, entitledCents}) => { + Meteor.users.update({ + "patreon.userId": id + }, {$set: { + "patreon.entitledCents":entitledCents, + }}); + }); + return members; +} + +// Method to run a manual update +Meteor.methods({ + updatePatreonMembersEntitledCents(){ + const user = Meteor.users.findOne(this.userId); + if (!user || !_.contains(user.roles, "admin")) throw new Meteor.Error( + "permission-error", "You need to be logged in as an admin to run this method" + ); + return updatePatreonMembersEntitledCents(); + }, +}); + +// Cron job to run the update automatically +Meteor.startup(() => { + SyncedCron.add({ + name: "updatePatreonMembersEntitledCents", + schedule: function(parser) { + return parser.text('every 4 hours'); + }, + job: updatePatreonMembersEntitledCents, + }); +}) From 0a22073d672912e309048ed76edcae9a9264b9cb Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 7 Mar 2019 13:44:35 +0200 Subject: [PATCH 3/5] Added library link for $5 patrons --- app/client/views/layout/layout.html | 8 ++++++++ app/client/views/layout/layout.js | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/client/views/layout/layout.html b/app/client/views/layout/layout.html index 9bb47c02..14f287e8 100644 --- a/app/client/views/layout/layout.html +++ b/app/client/views/layout/layout.html @@ -44,6 +44,14 @@ Characters + {{#if isTier5}} + + + + Library (beta) + + + {{/if}} diff --git a/app/client/views/layout/layout.js b/app/client/views/layout/layout.js index a18c74c8..a1d0945d 100644 --- a/app/client/views/layout/layout.js +++ b/app/client/views/layout/layout.js @@ -17,16 +17,23 @@ Template.appDrawer.helpers({ let post = PatreonPosts.findOne({}, {sort: {date: -1}}); return (post && post.link) || 'https://www.patreon.com/dicecloud'; }, + isTier5: function(){ + let user = Meteor.user(); + if (!user) return false; + patreon = user.patreon; + if (!patreon) return false; + return patreon.entitledCents >= 500 || patreon.entitledCentsOverride >= 500; + }, patreonTier: function(){ let user = Meteor.user(); if (!user) return; patreon = user.patreon; - if (!patreon) return "$0"; + if (!patreon) return "free"; let entitledCents = patreon.entitledCents || 0; if (patreon.entitledCentsOverride > entitledCents){ return "$" + (patreon.entitledCentsOverride / 100).toFixed(0); - } else if (patreon.entitledCents === undefined){ - return "$0"; + } else if (!patreon.entitledCents){ + return "free"; } else { return "$" + (patreon.entitledCents / 100).toFixed(0); } From bc6dfbe498a8e36e9d06157695d499ee99bb9f94 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 7 Mar 2019 13:45:38 +0200 Subject: [PATCH 4/5] Fixed stray quotation mark --- app/client/views/user/profile/profile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/views/user/profile/profile.js b/app/client/views/user/profile/profile.js index 18e303fe..e8e0d1fc 100644 --- a/app/client/views/user/profile/profile.js +++ b/app/client/views/user/profile/profile.js @@ -44,7 +44,7 @@ Template.profile.helpers({ if (Template.instance().loadingPatreon.get()){ return "loading..." } else if (patreon.entitledCentsOverride > entitledCents){ - return `$ ${(patreon.entitledCentsOverride / 100).toFixed(0)} (Overridden)"`; + return `$ ${(patreon.entitledCentsOverride / 100).toFixed(0)} (Overridden)`; } else if (patreon.entitledCents === undefined){ return "?"; } else { From 721300700eb3a87fb4476a390ecdec5abc797076 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 7 Mar 2019 13:46:56 +0200 Subject: [PATCH 5/5] Fixed capitalization error --- app/client/views/user/profile/profile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/views/user/profile/profile.js b/app/client/views/user/profile/profile.js index e8e0d1fc..90234d86 100644 --- a/app/client/views/user/profile/profile.js +++ b/app/client/views/user/profile/profile.js @@ -44,7 +44,7 @@ Template.profile.helpers({ if (Template.instance().loadingPatreon.get()){ return "loading..." } else if (patreon.entitledCentsOverride > entitledCents){ - return `$ ${(patreon.entitledCentsOverride / 100).toFixed(0)} (Overridden)`; + return `$ ${(patreon.entitledCentsOverride / 100).toFixed(0)} (overridden)`; } else if (patreon.entitledCents === undefined){ return "?"; } else {