diff --git a/.circleci/config.yml b/.circleci/config.yml index c717e0ef7d..31c609b656 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -113,6 +113,19 @@ jobs: keys: - v1-dev-bundle-cache-{{ checksum "meteor" }} - v1-dev-bundle-cache- + - run: + name: Combine NPM Shrinkwrap Files + command: | + for d in packages/*/.npm/package; do cat $d/npm-shrinkwrap.json >> shrinkwraps.txt; done + for d in packages/*/.npm/plugin/*; do cat $d/npm-shrinkwrap.json >> shrinkwraps.txt; done + - restore_cache: + keys: + - package-npm-deps-cache-group1-v1-{{ checksum "shrinkwraps.txt" }} + - package-npm-deps-cache-group1-v1- + - restore_cache: + keys: + - package-npm-deps-cache-group2-v1-{{ checksum "shrinkwraps.txt" }} + - package-npm-deps-cache-group2-v1- - restore_cache: keys: - v2-other-deps-cache-{{ .Branch }}-{{ .Revision }} @@ -166,7 +179,8 @@ jobs: ./meteor self-test \ 'add debugOnly and prodOnly packages' \ --retries ${METEOR_SELF_TEST_RETRIES} \ - --headless + --headless \ + --phantom no_output_timeout: 20m - run: name: "Running self-test (Custom Warehouse Tests)" @@ -176,6 +190,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --with-tag "custom-warehouse" no_output_timeout: 20m - run: @@ -212,6 +227,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/0.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -252,6 +268,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/1.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -292,6 +309,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/2.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -332,6 +350,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/3.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -372,6 +391,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/4.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -412,6 +432,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/5.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -452,6 +473,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/6.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -492,6 +514,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/7.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -532,6 +555,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/8.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -572,6 +596,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/9.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -612,6 +637,7 @@ jobs: --retries ${METEOR_SELF_TEST_RETRIES} \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --phantom \ --junit ./tmp/results/junit/10.xml \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -691,6 +717,49 @@ jobs: key: v1-dev-bundle-cache-{{ checksum "meteor" }} paths: - "dev_bundle" + # The package npm dependencies are split into two caches to avoid an AWS + # `MetadataTooLarge` error that consistently appears if we put all of + # these folders in the same cache + - save_cache: + key: package-npm-deps-cache-group1-v1-{{ checksum "shrinkwraps.txt" }} + paths: + - packages/meteor/.npm/package/node_modules + - packages/modules-runtime/.npm/package/node_modules + - packages/modules/.npm/package/node_modules + - packages/ecmascript-runtime-server/.npm/package/node_modules + - packages/promise/.npm/package/node_modules + - packages/babel-compiler/.npm/package/node_modules + - packages/babel-runtime/.npm/package/node_modules + - packages/http/.npm/package/node_modules + - packages/socket-stream-client/.npm/package/node_modules + - packages/ddp-client/.npm/package/node_modules + - packages/npm-mongo/.npm/package/node_modules + - packages/package-version-parser/.npm/package/node_modules + - packages/boilerplate-generator/.npm/package/node_modules + - save_cache: + key: package-npm-deps-cache-group2-v1-{{ checksum "shrinkwraps.txt" }} + paths: + - packages/xmlbuilder/.npm/package/node_modules + - packages/logging/.npm/package/node_modules + - packages/webapp/.npm/package/node_modules + - packages/ddp-server/.npm/package/node_modules + - packages/mongo/.npm/package/node_modules + - packages/npm-bcrypt/.npm/package/node_modules + - packages/email/.npm/package/node_modules + - packages/caching-compiler/.npm/package/node_modules + - packages/less/.npm/plugin/compileLessBatch/node_modules + - packages/non-core/blaze/packages/spacebars-compiler/.npm/package/node_modules + - packages/boilerplate-generator-tests/.npm/package/node_modules + - packages/non-core/bundle-visualizer/.npm/package/node_modules + - packages/d3-hierarchy/.npm/package/node_modules + - packages/non-core/coffeescript-compiler/.npm/package/node_modules + - packages/server-render/.npm/package/node_modules + - packages/es5-shim/.npm/package/node_modules + - packages/force-ssl-common/.npm/package/node_modules + - packages/jshint/.npm/plugin/lintJshint/node_modules + - packages/minifier-css/.npm/package/node_modules + - packages/minifier-js/.npm/package/node_modules + - packages/standard-minifier-css/.npm/plugin/minifyStdCSS/node_modules - save_cache: key: v2-other-deps-cache-{{ .Branch }}-{{ .Revision }} paths: diff --git a/.travis.yml b/.travis.yml index e943bf20c2..0cf166baac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: node_js node_js: - - "4.0" + - "8.11.1" cache: directories: - ".meteor" - ".babel-cache" -script: TEST_PACKAGES_EXCLUDE="less" ./packages/test-in-console/run.sh +script: TEST_PACKAGES_EXCLUDE="less" phantom=false ./packages/test-in-console/run.sh sudo: false env: - CXX=g++-4.8 diff --git a/History.md b/History.md index caea3420f1..b04ded5633 100644 --- a/History.md +++ b/History.md @@ -57,6 +57,11 @@ `meteor` always refers to the same `meteor` used to run `meteor npm`. [PR #9941](https://github.com/meteor/meteor/pull/9941) +* Meteor's `self-test` has been updated to use "headless" Chrome rather + than PhantomJS for browser tests. PhantomJS can still be forced by + passing the `--phantom` flag to the `meteor self-test` command. + [PR #9814](https://github.com/meteor/meteor/pull/9814) + * Importing a directory containing an `index.*` file now works for non-`.js` file extensions. As before, the list of possible extensions is defined by which compiler plugins you have enabled. diff --git a/LICENSES/Apache.txt b/LICENSES/Apache.txt index 9ea185e261..93e53f9de2 100644 --- a/LICENSES/Apache.txt +++ b/LICENSES/Apache.txt @@ -203,3 +203,9 @@ valid-identifier: https://github.com/purplecabbage/valid-identifier ---------- Jesse MacFadyen + +---------- +puppeteer: https://github.com/GoogleChrome/puppeteer +---------- + +Copyright 2017 Google Inc. diff --git a/tools/tests/apps/dynamic-import/packages/meteor-phantomjs-tests/.npm/package/.gitignore b/examples/unfinished/accounts-ui-viewer/.gitignore similarity index 100% rename from tools/tests/apps/dynamic-import/packages/meteor-phantomjs-tests/.npm/package/.gitignore rename to examples/unfinished/accounts-ui-viewer/.gitignore diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/.id b/examples/unfinished/accounts-ui-viewer/.meteor/.id new file mode 100644 index 0000000000..ca1973bfa3 --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +wllgu394zq2.rrlkgpniscl diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/packages b/examples/unfinished/accounts-ui-viewer/.meteor/packages index 426d145113..fcf09ec39c 100644 --- a/examples/unfinished/accounts-ui-viewer/.meteor/packages +++ b/examples/unfinished/accounts-ui-viewer/.meteor/packages @@ -11,6 +11,8 @@ less accounts-google accounts-github accounts-password -underscore accounts-facebook standard-app-packages +facebook-config-ui +github-config-ui +google-config-ui diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/platforms b/examples/unfinished/accounts-ui-viewer/.meteor/platforms new file mode 100644 index 0000000000..8a3a35f9f6 --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/.meteor/platforms @@ -0,0 +1,2 @@ +browser +server diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js index ebde65defc..9e4f67c8e5 100644 --- a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js @@ -1,23 +1,30 @@ -Meteor.users.allow({update: function () { return true; }}); +Meteor.users.allow({ update: () => true }); + +const { ServiceConfiguration } = Package['service-configuration']; + +Meteor.methods({ + 'removeService': service => ServiceConfiguration.configurations.remove({ service }), +}) if (Meteor.isClient) { - Accounts.STASH = _.extend({}, Accounts); + Accounts.STASH = { ...Accounts }; Accounts.STASH.loggingIn = Meteor.loggingIn; - var handleSetting = function (key, value) { + const handleSetting = (key, value) => { if (key === "numServices") { - _.each(['facebook', 'github', 'google'], - function (serv, i) { - if (i < value) - Accounts[serv] = Accounts.STASH[serv]; - else - Accounts[serv] = null; - }); + const registeredServices = Accounts.oauth.serviceNames(); + ['facebook', 'github', 'google'].forEach((serv, i) => { + if (i < value && !registeredServices.includes(serv)) { + Accounts.oauth.registerService(serv); + } else if (i >= value && registeredServices.includes(serv)) { + Accounts.oauth.unregisterService(serv); + } + }); } else if (key === "hasPasswords") { - Accounts.password = value && Accounts.STASH.password || null; - var user = Meteor.user(); + Package['accounts-password'] = value ? {} : null; + const user = Meteor.user(); if (user) { if (! value) { // make sure we have no username if "app" has no passwords @@ -32,12 +39,13 @@ if (Meteor.isClient) { } else if (key === "signupFields") { Accounts.ui._options.passwordSignupFields = value; } else if (key === "fakeLoggingIn") { - Meteor.loggingIn = (value ? function () { return true; } : + Meteor.loggingIn = (value ? () => true : Accounts.STASH.loggingIn); } }; - if (! Session.get('settings')) + const settings = Session.get('settings'); + if (! settings) { Session.set('settings', { alignRight: false, positioning: "relative", @@ -47,22 +55,32 @@ if (Meteor.isClient) { fakeLoggingIn: false, bgcolor: 'white' }); - else - _.each(Session.get('settings'), function (v,k) { - handleSetting(k, v); - }); + } else { + Object.keys(settings).forEach(key => handleSetting(key, settings[key])); + } - Template.page.settings = function () { - return Session.get('settings'); - }; + Template.page.helpers({ + settings: () => Session.get('settings'), + settingsClass: () => { + var settings = Session.get('settings'); + var classes = []; + if (settings.positioning) + classes.push('positioning-' + settings.positioning.toLowerCase()); + return classes.join(' '); + }, + match: kv => { + kv = keyValueFromId(kv); + if (! kv) + return false; + + return Session.get('settings')[kv[0]] === kv[1]; + }, + dropdownAlign: function() { + var settings = this; + return settings.alignRight ? 'right' : 'left'; + } + }); - Template.page.settingsClass = function () { - var settings = Session.get('settings'); - var classes = []; - if (settings.positioning) - classes.push('positioning-' + settings.positioning.toLowerCase()); - return classes.join(' '); - }; var keyValueFromId = function (id) { var match; @@ -74,7 +92,7 @@ if (Meteor.isClient) { return null; }; - var castValue = function (value) { + const castValue = value => { if (value === "false") value = false; else if (value === "true") @@ -84,32 +102,21 @@ if (Meteor.isClient) { return value; }; - Template.radio.maybeChecked = function () { - var curValue = Session.get('settings')[this.key]; - if (castValue(this.value) === curValue) - return 'checked'; - return ''; - }; + Template.radio.helpers({ + maybeChecked: function() { + var curValue = Session.get('settings')[this.key]; + if (castValue(this.value) === curValue) + return 'checked'; + return ''; + }, + }); - Template.page.match = function (kv) { - kv = keyValueFromId(kv); - if (! kv) - return false; - - return Session.get('settings')[kv[0]] === kv[1]; - }; - - Template.page.dropdownAlign = function () { - var settings = this; - return settings.alignRight ? 'right' : 'left'; - }; - - var fakeLogin = function (callback) { + const fakeLogin = callback => { Accounts.createUser( {username: Random.id(), password: "password", profile: { name: "Joe Schmoe" }}, - function () { + () => { var user = Meteor.user(); if (! user) return; @@ -124,7 +131,7 @@ if (Meteor.isClient) { }); }; - var exitFlows = function () { + const exitFlows = () => { Accounts._loginButtonsSession.set('inSignupFlow', false); Accounts._loginButtonsSession.set('inForgotPasswordFlow', false); Accounts._loginButtonsSession.set('inChangePasswordFlow', false); @@ -132,17 +139,17 @@ if (Meteor.isClient) { }; Template.page.events({ - 'change #controlpane input[type=radio]': function (event) { - var input = event.currentTarget; - var keyValue; + 'change #controlpane input[type=radio]': event => { + const input = event.currentTarget; + let keyValue; if (input && input.id && (keyValue = keyValueFromId(input.id))) { - var key = keyValue[0]; - var value = keyValue[1]; + const key = keyValue[0]; + const value = keyValue[1]; if (value === "false") value = false; else if (value === "true") value = true; - var settings = Session.get('settings'); + const settings = Session.get('settings'); settings[key] = value; Session.set('settings', settings); @@ -150,14 +157,15 @@ if (Meteor.isClient) { } }, 'click #controlpane button': function (event) { + const { ServiceConfiguration } = Package['service-configuration']; if (this.key === "fakeConfig") { - var service = this.value; - if (! ServiceConfiguration.configurations.findOne({service: service})) + const service = this.value; + if (! ServiceConfiguration.configurations.findOne({ service })) ServiceConfiguration.configurations.insert( - {service: service, fake: true}); + { service, fake: true }); } else if (this.key === "unconfig") { - var service = this.value; - ServiceConfiguration.configurations.remove({service: service}); + const service = this.value; + Meteor.call('removeService', service); } else if (this.key === "messages") { if (this.value === "error") { Accounts._loginButtonsSession.errorMessage('An error occurred! Gee golly gosh.'); @@ -190,20 +198,22 @@ if (Meteor.isClient) { exitFlows(); Accounts._loginButtonsSession.set("dropdownVisible", true); if (! Meteor.userId()) - fakeLogin(); + fakeLogin(() => {}); if (this.value === "changePassword") Accounts._loginButtonsSession.set("inChangePasswordFlow", true); else if (this.value === "messageOnly") Accounts._loginButtonsSession.set("inMessageOnlyFlow", true); } else if (this.key === "modals") { - var value = this.value; - _.each([ + const { value } = this; + [ 'resetPasswordToken', 'enrollAccountToken', - 'justVerifiedEmail'], function (k) { - Accounts._loginButtonsSession.set( - k, k.indexOf(value) >= 0 ? 'foo' : null); - }); + 'justVerifiedEmail' + ].forEach(k => { + Accounts._loginButtonsSession.set( + k, k.indexOf(value) >= 0 ? 'foo' : null + ); + }); } } }); diff --git a/examples/unfinished/accounts-ui-viewer/package-lock.json b/examples/unfinished/accounts-ui-viewer/package-lock.json new file mode 100644 index 0000000000..4df17dff5d --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/package-lock.json @@ -0,0 +1,682 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.38.tgz", + "integrity": "sha512-ZvPtlcvH2ZRzr1U5pkmCE7U3RIun3Nf29XHem47aScmJgMuL06ulkp+4oPBee3QrUVFErDjwNWtC67BzNuxLVw==", + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "meteor-node-stubs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-0.3.2.tgz", + "integrity": "sha512-l93SS/HutbqBRJODO2m7hup8cYI2acF5bB39+ZvP2BX8HMmCSCXeFH7v0sr4hD7zrVvHQA5UqS0pcDYKn0VM6g==", + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.1.4", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.11.1", + "domain-browser": "1.1.7", + "events": "1.1.1", + "http-browserify": "1.7.0", + "https-browserify": "0.0.1", + "os-browserify": "0.2.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502", + "stream-browserify": "2.0.1", + "string_decoder": "1.0.3", + "timers-browserify": "1.4.2", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "Base64": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", + "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" + }, + "asn1.js": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.1", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browserify-aes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.0.tgz", + "integrity": "sha512-W2bIMLYoZ9oow7TyePpMJk9l9LY7O3R61a/68bVCDOtnJynnwe3ZeW2IzzSkrQnPKNdJrxVDn3ALZNisSBwb7g==", + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.1", + "safe-buffer": "5.1.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "requires": { + "browserify-aes": "1.1.0", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.1" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.5" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.1", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "0.2.9" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "2.0.1", + "safe-buffer": "5.1.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.1", + "ripemd160": "2.0.1", + "sha.js": "2.4.9" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.1", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "crypto-browserify": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", + "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.1", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.5" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "2.0.1", + "minimalistic-assert": "1.0.0" + } + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.5" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.1", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.1", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "requires": { + "inherits": "2.0.1" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "http-browserify": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz", + "integrity": "sha1-M3la3nLfiKz7/TZ3PO/tp2RzWyA=", + "requires": { + "Base64": "0.2.1", + "inherits": "2.0.1" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.1" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "2.0.1", + "safe-buffer": "5.1.1" + } + } + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "requires": { + "asn1.js": "4.9.1", + "browserify-aes": "1.1.0", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.5" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "readable-stream": { + "version": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502", + "requires": { + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sha.js": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", + "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", + "requires": { + "inherits": "2.0.1", + "safe-buffer": "5.1.1" + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "2.0.1", + "readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "requires": { + "process": "0.11.10" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } +} diff --git a/examples/unfinished/accounts-ui-viewer/package.json b/examples/unfinished/accounts-ui-viewer/package.json new file mode 100644 index 0000000000..ebfd46233b --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/package.json @@ -0,0 +1,11 @@ +{ + "name": "accounts-ui-viewer", + "private": true, + "scripts": { + "start": "meteor run" + }, + "dependencies": { + "@babel/runtime": "^7.0.0-beta.38", + "meteor-node-stubs": "^0.3.2" + } +} \ No newline at end of file diff --git a/meteor b/meteor index bf1025e683..8266482148 100755 --- a/meteor +++ b/meteor @@ -132,7 +132,14 @@ fi # the script take precedence over $NODE_PATH; it used to be that users would # screw up their meteor installs by have a ~/node_modules +if [ "$ARCH" = "i686" ]; then + # 32-bit platforms cannot request 4GB + MAX_OLD_SPACE_SIZE=3072 +else + MAX_OLD_SPACE_SIZE=4096 +fi + exec "$DEV_BUNDLE/bin/node" \ - --max-old-space-size=4096 \ + --max-old-space-size=${MAX_OLD_SPACE_SIZE} \ ${TOOL_NODE_FLAGS} \ "$METEOR" "$@" diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index eda73a033b..17c1e38a28 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -23,7 +23,7 @@ export class AccountsClient extends AccountsCommon { this._pageLoadLoginCallbacks = []; this._pageLoadLoginAttemptInfo = null; - // Defined in url_client.js. + this.savedHash = window.location.hash; this._initUrlMatching(); // Defined in localstorage_token.js. @@ -114,16 +114,15 @@ export class AccountsClient extends AccountsCommon { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. */ logout(callback) { - var self = this; - self._loggingOut.set(true); - self.connection.apply('logout', [], { + this._loggingOut.set(true); + this.connection.apply('logout', [], { wait: true - }, function (error, result) { - self._loggingOut.set(false); + }, (error, result) => { + this._loggingOut.set(false); if (error) { callback && callback(error); } else { - self.makeClientLoggedOut(); + this.makeClientLoggedOut(); callback && callback(); } }); @@ -135,8 +134,6 @@ export class AccountsClient extends AccountsCommon { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. */ logoutOtherClients(callback) { - var self = this; - // We need to make two method calls: one to replace our current token, // and another to remove all tokens except the current one. We want to // call these two methods one after the other, without any other @@ -153,14 +150,14 @@ export class AccountsClient extends AccountsCommon { // `getNewToken`, we won't actually send the `removeOtherTokens` call // until the `getNewToken` callback has finished running, because they // are both wait methods. - self.connection.apply( + this.connection.apply( 'getNewToken', [], { wait: true }, - function (err, result) { + (err, result) => { if (! err) { - self._storeLoginToken( - self.userId(), + this._storeLoginToken( + this.userId(), result.token, result.tokenExpires ); @@ -168,248 +165,572 @@ export class AccountsClient extends AccountsCommon { } ); - self.connection.apply( + this.connection.apply( 'removeOtherTokens', [], { wait: true }, - function (err) { - callback && callback(err); - } + err => callback && callback(err) ); } -}; -var Ap = AccountsClient.prototype; + /// + /// LOGIN METHODS + /// -/** - * @summary True if a login method (such as `Meteor.loginWithPassword`, `Meteor.loginWithFacebook`, or `Accounts.createUser`) is currently in progress. A reactive data source. - * @locus Client - * @importFromPackage meteor - */ -Meteor.loggingIn = function () { - return Accounts.loggingIn(); -}; - -/** - * @summary True if a logout method (such as `Meteor.logout`) is currently in progress. A reactive data source. - * @locus Client - * @importFromPackage meteor - */ -Meteor.loggingOut = function () { - return Accounts.loggingOut(); -}; - -/// -/// LOGIN METHODS -/// - -// Call a login method on the server. -// -// A login method is a method which on success calls `this.setUserId(id)` and -// `Accounts._setLoginToken` on the server and returns an object with fields -// 'id' (containing the user id), 'token' (containing a resume token), and -// optionally `tokenExpires`. -// -// This function takes care of: -// - Updating the Meteor.loggingIn() reactive data source -// - Calling the method in 'wait' mode -// - On success, saving the resume token to localStorage -// - On success, calling Accounts.connection.setUserId() -// - Setting up an onReconnect handler which logs in with -// the resume token -// -// Options: -// - methodName: The method to call (default 'login') -// - methodArguments: The arguments for the method -// - validateResult: If provided, will be called with the result of the -// method. If it throws, the client will not be logged in (and -// its error will be passed to the callback). -// - userCallback: Will be called with no arguments once the user is fully -// logged in, or with the error on error. -// -Ap.callLoginMethod = function (options) { - var self = this; - - options = _.extend({ - methodName: 'login', - methodArguments: [{}], - _suppressLoggingIn: false - }, options); - - // Set defaults for callback arguments to no-op functions; make sure we - // override falsey values too. - _.each(['validateResult', 'userCallback'], function (f) { - if (!options[f]) - options[f] = function () {}; - }); - - // Prepare callbacks: user provided and onLogin/onLoginFailure hooks. - var loginCallbacks = _.once(function ({ error, loginDetails }) { - if (!error) { - self._onLoginHook.each(function (callback) { - callback(loginDetails); - return true; - }); - } else { - self._onLoginFailureHook.each(function (callback) { - callback({ error }); - return true; - }); - } - options.userCallback(error, loginDetails); - }); - - var reconnected = false; - - // We want to set up onReconnect as soon as we get a result token back from - // the server, without having to wait for subscriptions to rerun. This is - // because if we disconnect and reconnect between getting the result and - // getting the results of subscription rerun, we WILL NOT re-send this - // method (because we never re-send methods whose results we've received) - // but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce" - // time. This will lead to makeClientLoggedIn(result.id) even though we - // haven't actually sent a login method! + // Call a login method on the server. // - // But by making sure that we send this "resume" login in that case (and - // calling makeClientLoggedOut if it fails), we'll end up with an accurate - // client-side userId. (It's important that livedata_connection guarantees - // that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback - // will occur before the callback from the resume login call.) - var onResultReceived = function (err, result) { - if (err || !result || !result.token) { - // Leave onReconnect alone if there was an error, so that if the user was - // already logged in they will still get logged in on reconnect. - // See issue #4970. - } else { - // First clear out any previously set Acccounts login onReconnect - // callback (to make sure we don't keep piling up duplicate callbacks, - // which would then all be triggered when reconnecting). - if (self._reconnectStopper) { - self._reconnectStopper.stop(); + // A login method is a method which on success calls `this.setUserId(id)` and + // `Accounts._setLoginToken` on the server and returns an object with fields + // 'id' (containing the user id), 'token' (containing a resume token), and + // optionally `tokenExpires`. + // + // This function takes care of: + // - Updating the Meteor.loggingIn() reactive data source + // - Calling the method in 'wait' mode + // - On success, saving the resume token to localStorage + // - On success, calling Accounts.connection.setUserId() + // - Setting up an onReconnect handler which logs in with + // the resume token + // + // Options: + // - methodName: The method to call (default 'login') + // - methodArguments: The arguments for the method + // - validateResult: If provided, will be called with the result of the + // method. If it throws, the client will not be logged in (and + // its error will be passed to the callback). + // - userCallback: Will be called with no arguments once the user is fully + // logged in, or with the error on error. + // + callLoginMethod(options) { + options = { + methodName: 'login', + methodArguments: [{}], + _suppressLoggingIn: false, + ...options, + }; + + // Set defaults for callback arguments to no-op functions; make sure we + // override falsey values too. + ['validateResult', 'userCallback'].forEach(f => { + if (!options[f]) + options[f] = () => null; + }) + + // Prepare callbacks: user provided and onLogin/onLoginFailure hooks. + let called; + const loginCallbacks = ({ error, loginDetails }) => { + if (!called) { + called = true; + if (!error) { + this._onLoginHook.each(callback => { + callback(loginDetails); + return true; + }); + } else { + this._onLoginFailureHook.each(callback => { + callback({ error }); + return true; + }); + } + options.userCallback(error, loginDetails); + } + } + + let reconnected = false; + + // We want to set up onReconnect as soon as we get a result token back from + // the server, without having to wait for subscriptions to rerun. This is + // because if we disconnect and reconnect between getting the result and + // getting the results of subscription rerun, we WILL NOT re-send this + // method (because we never re-send methods whose results we've received) + // but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce" + // time. This will lead to makeClientLoggedIn(result.id) even though we + // haven't actually sent a login method! + // + // But by making sure that we send this "resume" login in that case (and + // calling makeClientLoggedOut if it fails), we'll end up with an accurate + // client-side userId. (It's important that livedata_connection guarantees + // that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback + // will occur before the callback from the resume login call.) + const onResultReceived = (err, result) => { + if (err || !result || !result.token) { + // Leave onReconnect alone if there was an error, so that if the user was + // already logged in they will still get logged in on reconnect. + // See issue #4970. + } else { + // First clear out any previously set Acccounts login onReconnect + // callback (to make sure we don't keep piling up duplicate callbacks, + // which would then all be triggered when reconnecting). + if (this._reconnectStopper) { + this._reconnectStopper.stop(); + } + + this._reconnectStopper = DDP.onReconnect(conn => { + if (conn != this.connection) { + return; + } + reconnected = true; + // If our token was updated in storage, use the latest one. + const storedToken = this._storedLoginToken(); + if (storedToken) { + result = { + token: storedToken, + tokenExpires: this._storedLoginTokenExpires() + }; + } + if (!result.tokenExpires) + result.tokenExpires = this._tokenExpiration(new Date()); + if (this._tokenExpiresSoon(result.tokenExpires)) { + this.makeClientLoggedOut(); + } else { + this.callLoginMethod({ + methodArguments: [{resume: result.token}], + // Reconnect quiescence ensures that the user doesn't see an + // intermediate state before the login method finishes. So we don't + // need to show a logging-in animation. + _suppressLoggingIn: true, + userCallback: (error, loginDetails) => { + const storedTokenNow = this._storedLoginToken(); + if (error) { + // If we had a login error AND the current stored token is the + // one that we tried to log in with, then declare ourselves + // logged out. If there's a token in storage but it's not the + // token that we tried to log in with, we don't know anything + // about whether that token is valid or not, so do nothing. The + // periodic localStorage poll will decide if we are logged in or + // out with this token, if it hasn't already. Of course, even + // with this check, another tab could insert a new valid token + // immediately before we clear localStorage here, which would + // lead to both tabs being logged out, but by checking the token + // in storage right now we hope to make that unlikely to happen. + // + // If there is no token in storage right now, we don't have to + // do anything; whatever code removed the token from storage was + // responsible for calling `makeClientLoggedOut()`, or the + // periodic localStorage poll will call `makeClientLoggedOut` + // eventually if another tab wiped the token from storage. + if (storedTokenNow && storedTokenNow === result.token) { + this.makeClientLoggedOut(); + } + } + // Possibly a weird callback to call, but better than nothing if + // there is a reconnect between "login result received" and "data + // ready". + loginCallbacks({ error, loginDetails }); + }}); + } + }); + } + }; + + // This callback is called once the local cache of the current-user + // subscription (and all subscriptions, in fact) are guaranteed to be up to + // date. + const loggedInAndDataReadyCallback = (error, result) => { + // If the login method returns its result but the connection is lost + // before the data is in the local cache, it'll set an onReconnect (see + // above). The onReconnect will try to log in using the token, and *it* + // will call userCallback via its own version of this + // loggedInAndDataReadyCallback. So we don't have to do anything here. + if (reconnected) + return; + + // Note that we need to call this even if _suppressLoggingIn is true, + // because it could be matching a _setLoggingIn(true) from a + // half-completed pre-reconnect login method. + this._setLoggingIn(false); + if (error || !result) { + error = error || new Error( + `No result from call to ${options.methodName}` + ); + loginCallbacks({ error }); + return; + } + try { + options.validateResult(result); + } catch (e) { + loginCallbacks({ error: e }); + return; } - self._reconnectStopper = DDP.onReconnect(function (conn) { - if (conn != self.connection) { - return; - } - reconnected = true; - // If our token was updated in storage, use the latest one. - var storedToken = self._storedLoginToken(); - if (storedToken) { - result = { - token: storedToken, - tokenExpires: self._storedLoginTokenExpires() - }; - } - if (! result.tokenExpires) - result.tokenExpires = self._tokenExpiration(new Date()); - if (self._tokenExpiresSoon(result.tokenExpires)) { - self.makeClientLoggedOut(); - } else { - self.callLoginMethod({ - methodArguments: [{resume: result.token}], - // Reconnect quiescence ensures that the user doesn't see an - // intermediate state before the login method finishes. So we don't - // need to show a logging-in animation. - _suppressLoggingIn: true, - userCallback: function (error, loginDetails) { - var storedTokenNow = self._storedLoginToken(); - if (error) { - // If we had a login error AND the current stored token is the - // one that we tried to log in with, then declare ourselves - // logged out. If there's a token in storage but it's not the - // token that we tried to log in with, we don't know anything - // about whether that token is valid or not, so do nothing. The - // periodic localStorage poll will decide if we are logged in or - // out with this token, if it hasn't already. Of course, even - // with this check, another tab could insert a new valid token - // immediately before we clear localStorage here, which would - // lead to both tabs being logged out, but by checking the token - // in storage right now we hope to make that unlikely to happen. - // - // If there is no token in storage right now, we don't have to - // do anything; whatever code removed the token from storage was - // responsible for calling `makeClientLoggedOut()`, or the - // periodic localStorage poll will call `makeClientLoggedOut` - // eventually if another tab wiped the token from storage. - if (storedTokenNow && storedTokenNow === result.token) { - self.makeClientLoggedOut(); - } - } - // Possibly a weird callback to call, but better than nothing if - // there is a reconnect between "login result received" and "data - // ready". - loginCallbacks({ error, loginDetails }); - }}); - } + // Make the client logged in. (The user data should already be loaded!) + this.makeClientLoggedIn(result.id, result.token, result.tokenExpires); + loginCallbacks({ loginDetails: { type: result.type } }); + }; + + if (!options._suppressLoggingIn) { + this._setLoggingIn(true); + } + this.connection.apply( + options.methodName, + options.methodArguments, + { wait: true, onResultReceived: onResultReceived }, + loggedInAndDataReadyCallback); + } + + makeClientLoggedOut() { + // Ensure client was successfully logged in before running logout hooks. + if (this.connection._userId) { + this._onLogoutHook.each(callback => { + callback(); + return true; }); } - }; - - // This callback is called once the local cache of the current-user - // subscription (and all subscriptions, in fact) are guaranteed to be up to - // date. - var loggedInAndDataReadyCallback = function (error, result) { - // If the login method returns its result but the connection is lost - // before the data is in the local cache, it'll set an onReconnect (see - // above). The onReconnect will try to log in using the token, and *it* - // will call userCallback via its own version of this - // loggedInAndDataReadyCallback. So we don't have to do anything here. - if (reconnected) - return; - - // Note that we need to call this even if _suppressLoggingIn is true, - // because it could be matching a _setLoggingIn(true) from a - // half-completed pre-reconnect login method. - self._setLoggingIn(false); - if (error || !result) { - error = error || new Error( - "No result from call to " + options.methodName); - loginCallbacks({ error }); - return; - } - try { - options.validateResult(result); - } catch (e) { - loginCallbacks({ error: e }); - return; - } - - // Make the client logged in. (The user data should already be loaded!) - self.makeClientLoggedIn(result.id, result.token, result.tokenExpires); - loginCallbacks({ - loginDetails: { - type: result.type, - }, - }); - }; - - if (!options._suppressLoggingIn) - self._setLoggingIn(true); - self.connection.apply( - options.methodName, - options.methodArguments, - {wait: true, onResultReceived: onResultReceived}, - loggedInAndDataReadyCallback); -}; - -Ap.makeClientLoggedOut = function () { - // Ensure client was successfully logged in before running logout hooks. - if (this.connection._userId) { - this._onLogoutHook.each(function (callback) { - callback(); - return true; - }); + this._unstoreLoginToken(); + this.connection.setUserId(null); + this._reconnectStopper && this._reconnectStopper.stop(); } - this._unstoreLoginToken(); - this.connection.setUserId(null); - this._reconnectStopper && this._reconnectStopper.stop(); + + makeClientLoggedIn(userId, token, tokenExpires) { + this._storeLoginToken(userId, token, tokenExpires); + this.connection.setUserId(userId); + } + + /// + /// LOGIN SERVICES + /// + + // A reactive function returning whether the loginServiceConfiguration + // subscription is ready. Used by accounts-ui to hide the login button + // until we have all the configuration loaded + // + loginServicesConfigured() { + return this._loginServicesHandle.ready(); + }; + + // Some login services such as the redirect login flow or the resume + // login handler can log the user in at page load time. The + // Meteor.loginWithX functions have a callback argument, but the + // callback function instance won't be in memory any longer if the + // page was reloaded. The `onPageLoadLogin` function allows a + // callback to be registered for the case where the login was + // initiated in a previous VM, and we now have the result of the login + // attempt in a new VM. + + // Register a callback to be called if we have information about a + // login attempt at page load time. Call the callback immediately if + // we already have the page load login attempt info, otherwise stash + // the callback to be called if and when we do get the attempt info. + // + onPageLoadLogin(f) { + if (this._pageLoadLoginAttemptInfo) { + f(this._pageLoadLoginAttemptInfo); + } else { + this._pageLoadLoginCallbacks.push(f); + } + }; + + // Receive the information about the login attempt at page load time. + // Call registered callbacks, and also record the info in case + // someone's callback hasn't been registered yet. + // + _pageLoadLogin(attemptInfo) { + if (this._pageLoadLoginAttemptInfo) { + Meteor._debug( + 'Ignoring unexpected duplicate page load login attempt info' + ); + return; + } + + this._pageLoadLoginCallbacks.forEach(callback => callback(attemptInfo)); + this._pageLoadLoginCallbacks = []; + this._pageLoadLoginAttemptInfo = attemptInfo; + }; + + /// + /// LOGIN TOKENS + /// + + // These methods deal with storing a login token and user id in the + // browser's localStorage facility. It polls local storage every few + // seconds to synchronize login state between multiple tabs in the same + // browser. + + loginWithToken(token, callback) { + this.callLoginMethod({ + methodArguments: [{ + resume: token + }], + userCallback: callback + }); + }; + + // Semi-internal API. Call this function to re-enable auto login after + // if it was disabled at startup. + _enableAutoLogin() { + this._autoLoginEnabled = true; + this._pollStoredLoginToken(); + }; + + /// + /// STORING + /// + + // Call this from the top level of the test file for any test that does + // logging in and out, to protect multiple tabs running the same tests + // simultaneously from interfering with each others' localStorage. + _isolateLoginTokenForTest() { + this.LOGIN_TOKEN_KEY = this.LOGIN_TOKEN_KEY + Random.id(); + this.USER_ID_KEY = this.USER_ID_KEY + Random.id(); + }; + + _storeLoginToken(userId, token, tokenExpires) { + Meteor._localStorage.setItem(this.USER_ID_KEY, userId); + Meteor._localStorage.setItem(this.LOGIN_TOKEN_KEY, token); + if (! tokenExpires) + tokenExpires = this._tokenExpiration(new Date()); + Meteor._localStorage.setItem(this.LOGIN_TOKEN_EXPIRES_KEY, tokenExpires); + + // to ensure that the localstorage poller doesn't end up trying to + // connect a second time + this._lastLoginTokenWhenPolled = token; + }; + + _unstoreLoginToken() { + Meteor._localStorage.removeItem(this.USER_ID_KEY); + Meteor._localStorage.removeItem(this.LOGIN_TOKEN_KEY); + Meteor._localStorage.removeItem(this.LOGIN_TOKEN_EXPIRES_KEY); + + // to ensure that the localstorage poller doesn't end up trying to + // connect a second time + this._lastLoginTokenWhenPolled = null; + }; + + // This is private, but it is exported for now because it is used by a + // test in accounts-password. + _storedLoginToken() { + return Meteor._localStorage.getItem(this.LOGIN_TOKEN_KEY); + }; + + _storedLoginTokenExpires() { + return Meteor._localStorage.getItem(this.LOGIN_TOKEN_EXPIRES_KEY); + }; + + _storedUserId() { + return Meteor._localStorage.getItem(this.USER_ID_KEY); + }; + + _unstoreLoginTokenIfExpiresSoon() { + const tokenExpires = this._storedLoginTokenExpires(); + if (tokenExpires && this._tokenExpiresSoon(new Date(tokenExpires))) { + this._unstoreLoginToken(); + } + }; + + /// + /// AUTO-LOGIN + /// + + _initLocalStorage() { + // Key names to use in localStorage + this.LOGIN_TOKEN_KEY = "Meteor.loginToken"; + this.LOGIN_TOKEN_EXPIRES_KEY = "Meteor.loginTokenExpires"; + this.USER_ID_KEY = "Meteor.userId"; + + const rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + if (rootUrlPathPrefix || this.connection !== Meteor.connection) { + // We want to keep using the same keys for existing apps that do not + // set a custom ROOT_URL_PATH_PREFIX, so that most users will not have + // to log in again after an app updates to a version of Meteor that + // contains this code, but it's generally preferable to namespace the + // keys so that connections from distinct apps to distinct DDP URLs + // will be distinct in Meteor._localStorage. + let namespace = `:${this.connection._stream.rawUrl}`; + if (rootUrlPathPrefix) { + namespace += `:${rootUrlPathPrefix}`; + } + this.LOGIN_TOKEN_KEY += namespace; + this.LOGIN_TOKEN_EXPIRES_KEY += namespace; + this.USER_ID_KEY += namespace; + } + + let token; + if (this._autoLoginEnabled) { + // Immediately try to log in via local storage, so that any DDP + // messages are sent after we have established our user account + this._unstoreLoginTokenIfExpiresSoon(); + token = this._storedLoginToken(); + if (token) { + // On startup, optimistically present us as logged in while the + // request is in flight. This reduces page flicker on startup. + const userId = this._storedUserId(); + userId && this.connection.setUserId(userId); + this.loginWithToken(token, err => { + if (err) { + Meteor._debug(`Error logging in with token: ${err}`); + this.makeClientLoggedOut(); + } + + this._pageLoadLogin({ + type: "resume", + allowed: !err, + error: err, + methodName: "login", + // XXX This is duplicate code with loginWithToken, but + // loginWithToken can also be called at other times besides + // page load. + methodArguments: [{resume: token}] + }); + }); + } + } + + // Poll local storage every 3 seconds to login if someone logged in in + // another tab + this._lastLoginTokenWhenPolled = token; + + if (this._pollIntervalTimer) { + // Unlikely that _initLocalStorage will be called more than once for + // the same AccountsClient instance, but just in case... + clearInterval(this._pollIntervalTimer); + } + + this._pollIntervalTimer = setInterval(() => { + this._pollStoredLoginToken(); + }, 3000); + }; + + _pollStoredLoginToken() { + if (! this._autoLoginEnabled) { + return; + } + + const currentLoginToken = this._storedLoginToken(); + + // != instead of !== just to make sure undefined and null are treated the same + if (this._lastLoginTokenWhenPolled != currentLoginToken) { + if (currentLoginToken) { + this.loginWithToken(currentLoginToken, (err) => { + if (err) { + this.makeClientLoggedOut(); + } + }); + } else { + this.logout(); + } + } + + this._lastLoginTokenWhenPolled = currentLoginToken; + }; + + /// + /// URLS + /// + + _initUrlMatching() { + // By default, allow the autologin process to happen. + this._autoLoginEnabled = true; + + // We only support one callback per URL. + this._accountsCallbacks = {}; + + // Try to match the saved value of window.location.hash. + this._attemptToMatchHash(); + }; + + // Separate out this functionality for testing + _attemptToMatchHash() { + attemptToMatchHash(this, this.savedHash, defaultSuccessHandler); + }; + + /** + * @summary Register a function to call when a reset password link is clicked + * in an email sent by + * [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail). + * This function should be called in top-level code, not inside + * `Meteor.startup()`. + * @memberof! Accounts + * @name onResetPasswordLink + * @param {Function} callback The function to call. It is given two arguments: + * + * 1. `token`: A password reset token that can be passed to + * [`Accounts.resetPassword`](#accounts_resetpassword). + * 2. `done`: A function to call when the password reset UI flow is complete. The normal + * login process is suspended until this function is called, so that the + * password for user A can be reset even if user B was logged in. + * @locus Client + */ + onResetPasswordLink(callback) { + if (this._accountsCallbacks["reset-password"]) { + Meteor._debug("Accounts.onResetPasswordLink was called more than once. " + + "Only one callback added will be executed."); + } + + this._accountsCallbacks["reset-password"] = callback; + }; + + /** + * @summary Register a function to call when an email verification link is + * clicked in an email sent by + * [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail). + * This function should be called in top-level code, not inside + * `Meteor.startup()`. + * @memberof! Accounts + * @name onEmailVerificationLink + * @param {Function} callback The function to call. It is given two arguments: + * + * 1. `token`: An email verification token that can be passed to + * [`Accounts.verifyEmail`](#accounts_verifyemail). + * 2. `done`: A function to call when the email verification UI flow is complete. + * The normal login process is suspended until this function is called, so + * that the user can be notified that they are verifying their email before + * being logged in. + * @locus Client + */ + onEmailVerificationLink(callback) { + if (this._accountsCallbacks["verify-email"]) { + Meteor._debug("Accounts.onEmailVerificationLink was called more than once. " + + "Only one callback added will be executed."); + } + + this._accountsCallbacks["verify-email"] = callback; + }; + + /** + * @summary Register a function to call when an account enrollment link is + * clicked in an email sent by + * [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). + * This function should be called in top-level code, not inside + * `Meteor.startup()`. + * @memberof! Accounts + * @name onEnrollmentLink + * @param {Function} callback The function to call. It is given two arguments: + * + * 1. `token`: A password reset token that can be passed to + * [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly + * enrolled account a password. + * 2. `done`: A function to call when the enrollment UI flow is complete. + * The normal login process is suspended until this function is called, so that + * user A can be enrolled even if user B was logged in. + * @locus Client + */ + onEnrollmentLink(callback) { + if (this._accountsCallbacks["enroll-account"]) { + Meteor._debug("Accounts.onEnrollmentLink was called more than once. " + + "Only one callback added will be executed."); + } + + this._accountsCallbacks["enroll-account"] = callback; + }; + }; -Ap.makeClientLoggedIn = function (userId, token, tokenExpires) { - this._storeLoginToken(userId, token, tokenExpires); - this.connection.setUserId(userId); -}; +/** + * @summary True if a login method (such as `Meteor.loginWithPassword`, + * `Meteor.loginWithFacebook`, or `Accounts.createUser`) is currently in + * progress. A reactive data source. + * @locus Client + * @importFromPackage meteor + */ +Meteor.loggingIn = () => Accounts.loggingIn(); + +/** + * @summary True if a logout method (such as `Meteor.logout`) is currently in + * progress. A reactive data source. + * @locus Client + * @importFromPackage meteor + */ +Meteor.loggingOut = () => Accounts.loggingOut(); /** * @summary Log the user out. @@ -417,9 +738,7 @@ Ap.makeClientLoggedIn = function (userId, token, tokenExpires) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage meteor */ -Meteor.logout = function (callback) { - return Accounts.logout(callback); -}; +Meteor.logout = callback => Accounts.logout(callback); /** * @summary Log out other clients logged in as the current user, but does not log out the client that calls this function. @@ -427,65 +746,19 @@ Meteor.logout = function (callback) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage meteor */ -Meteor.logoutOtherClients = function (callback) { - return Accounts.logoutOtherClients(callback); -}; - - -/// -/// LOGIN SERVICES -/// - -// A reactive function returning whether the loginServiceConfiguration -// subscription is ready. Used by accounts-ui to hide the login button -// until we have all the configuration loaded -// -Ap.loginServicesConfigured = function () { - return this._loginServicesHandle.ready(); -}; - - -// Some login services such as the redirect login flow or the resume -// login handler can log the user in at page load time. The -// Meteor.loginWithX functions have a callback argument, but the -// callback function instance won't be in memory any longer if the -// page was reloaded. The `onPageLoadLogin` function allows a -// callback to be registered for the case where the login was -// initiated in a previous VM, and we now have the result of the login -// attempt in a new VM. - -// Register a callback to be called if we have information about a -// login attempt at page load time. Call the callback immediately if -// we already have the page load login attempt info, otherwise stash -// the callback to be called if and when we do get the attempt info. -// -Ap.onPageLoadLogin = function (f) { - if (this._pageLoadLoginAttemptInfo) { - f(this._pageLoadLoginAttemptInfo); - } else { - this._pageLoadLoginCallbacks.push(f); - } -}; - - -// Receive the information about the login attempt at page load time. -// Call registered callbacks, and also record the info in case -// someone's callback hasn't been registered yet. -// -Ap._pageLoadLogin = function (attemptInfo) { - if (this._pageLoadLoginAttemptInfo) { - Meteor._debug("Ignoring unexpected duplicate page load login attempt info"); - return; - } - - _.each(this._pageLoadLoginCallbacks, function (callback) { - callback(attemptInfo); - }); - - this._pageLoadLoginCallbacks = []; - this._pageLoadLoginAttemptInfo = attemptInfo; -}; +Meteor.logoutOtherClients = callback => Accounts.logoutOtherClients(callback); +/** + * @summary Login with a Meteor access token. + * @locus Client + * @param {Object} [token] Local storage token for use with login across + * multiple tabs in the same browser. + * @param {Function} [callback] Optional callback. Called with no arguments on + * success. + * @importFromPackage meteor + */ +Meteor.loginWithToken = (token, callback) => + Accounts.loginWithToken(token, callback); /// /// HANDLEBARS HELPERS @@ -494,15 +767,15 @@ Ap._pageLoadLogin = function (attemptInfo) { // If our app has a Blaze, register the {{currentUser}} and {{loggingIn}} // global helpers. if (Package.blaze) { + const { Template } = Package.blaze.Blaze; + /** * @global * @name currentUser * @isHelper true * @summary Calls [Meteor.user()](#meteor_user). Use `{{#if currentUser}}` to check whether the user is logged in. */ - Package.blaze.Blaze.Template.registerHelper('currentUser', function () { - return Meteor.user(); - }); + Template.registerHelper('currentUser', () => Meteor.user()); /** * @global @@ -510,9 +783,7 @@ if (Package.blaze) { * @isHelper true * @summary Calls [Meteor.loggingIn()](#meteor_loggingin). */ - Package.blaze.Blaze.Template.registerHelper('loggingIn', function () { - return Meteor.loggingIn(); - }); + Template.registerHelper('loggingIn', () => Meteor.loggingIn()); /** * @global @@ -520,9 +791,7 @@ if (Package.blaze) { * @isHelper true * @summary Calls [Meteor.loggingOut()](#meteor_loggingout). */ - Package.blaze.Blaze.Template.registerHelper('loggingOut', function () { - return Meteor.loggingOut(); - }); + Template.registerHelper('loggingOut', () => Meteor.loggingOut()); /** * @global @@ -530,7 +799,64 @@ if (Package.blaze) { * @isHelper true * @summary Calls [Meteor.loggingIn()](#meteor_loggingin) or [Meteor.loggingOut()](#meteor_loggingout). */ - Package.blaze.Blaze.Template.registerHelper('loggingInOrOut', function () { - return (Meteor.loggingIn() || Meteor.loggingOut()); + Template.registerHelper( + 'loggingInOrOut', + () => Meteor.loggingIn() || Meteor.loggingOut() + ); +} + +const defaultSuccessHandler = function(token, urlPart) { + // put login in a suspended state to wait for the interaction to finish + this._autoLoginEnabled = false; + + // wait for other packages to register callbacks + Meteor.startup(() => { + // if a callback has been registered for this kind of token, call it + if (this._accountsCallbacks[urlPart]) { + this._accountsCallbacks[urlPart](token, () => this._enableAutoLogin()); + } }); } + +// Note that both arguments are optional and are currently only passed by +// accounts_url_tests.js. +const attemptToMatchHash = (accounts, hash, success) => { + // All of the special hash URLs we support for accounts interactions + ["reset-password", "verify-email", "enroll-account"].forEach(urlPart => { + let token; + + const tokenRegex = new RegExp(`^\\#\\/${urlPart}\\/(.*)$`); + const match = hash.match(tokenRegex); + + if (match) { + token = match[1]; + + // XXX COMPAT WITH 0.9.3 + if (urlPart === "reset-password") { + accounts._resetPasswordToken = token; + } else if (urlPart === "verify-email") { + accounts._verifyEmailToken = token; + } else if (urlPart === "enroll-account") { + accounts._enrollAccountToken = token; + } + } else { + return; + } + + // If no handlers match the hash, then maybe it's meant to be consumed + // by some entirely different code, so we only clear it the first time + // a handler successfully matches. Note that later handlers reuse the + // savedHash, so clearing window.location.hash here will not interfere + // with their needs. + window.location.hash = ""; + + // Do some stuff with the token we matched + success.call(accounts, token, urlPart); + }); +} + +// Export for testing +export const AccountsTest = { + attemptToMatchHash: (hash, success) => + attemptToMatchHash(Accounts, hash, success), +}; diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 55bf07b05f..d89ed40fa1 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -40,6 +40,33 @@ export class AccountsCommon { bindEnvironment: false, debugPrintExceptions: "onLogout callback" }); + + // Expose for testing. + this.DEFAULT_LOGIN_EXPIRATION_DAYS = DEFAULT_LOGIN_EXPIRATION_DAYS; + this.LOGIN_UNEXPIRING_TOKEN_DAYS = LOGIN_UNEXPIRING_TOKEN_DAYS; + + // Thrown when the user cancels the login process (eg, closes an oauth + // popup, declines retina scan, etc) + const lceName = 'Accounts.LoginCancelledError'; + this.LoginCancelledError = Meteor.makeErrorType( + lceName, + function (description) { + this.message = description; + } + ); + this.LoginCancelledError.prototype.name = lceName; + + // This is used to transmit specific subclass errors over the wire. We + // should come up with a more generic way to do this (eg, with some sort of + // symbolic error code rather than a number). + this.LoginCancelledError.numericError = 0x8acdc2f; + + // loginServiceConfiguration and ConfigError are maintained for backwards compatibility + Meteor.startup(() => { + const { ServiceConfiguration } = Package['service-configuration']; + this.loginServiceConfiguration = ServiceConfiguration.configurations; + this.ConfigError = ServiceConfiguration.ConfigError; + }); } /** @@ -55,7 +82,7 @@ export class AccountsCommon { * @locus Anywhere */ user() { - var userId = this.userId(); + const userId = this.userId(); return userId ? this.users.findOne(userId) : null; } @@ -107,8 +134,6 @@ export class AccountsCommon { * @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to false. */ config(options) { - var self = this; - // We don't want users to accidentally only call Accounts.config on the // client, where some of the options will have partial effects (eg removing // the "create account" button from accounts-ui if forbidClientAccountCreation @@ -126,32 +151,35 @@ export class AccountsCommon { // We need to validate the oauthSecretKey option at the time // Accounts.config is called. We also deliberately don't store the // oauthSecretKey in Accounts._options. - if (_.has(options, "oauthSecretKey")) { - if (Meteor.isClient) + if (Object.prototype.hasOwnProperty.call(options, 'oauthSecretKey')) { + if (Meteor.isClient) { throw new Error("The oauthSecretKey option may only be specified on the server"); - if (! Package["oauth-encryption"]) + } + if (! Package["oauth-encryption"]) { throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); + } Package["oauth-encryption"].OAuthEncryption.loadKey(options.oauthSecretKey); - options = _.omit(options, "oauthSecretKey"); + options = { ...options }; + delete options.oauthSecretKey; } // validate option keys - var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays", + const VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays", "restrictCreationByEmailDomain", "loginExpirationInDays", "passwordResetTokenExpirationInDays", "ambiguousErrorMessages", "bcryptRounds"]; - _.each(_.keys(options), function (key) { - if (!_.contains(VALID_KEYS, key)) { - throw new Error("Accounts.config: Invalid key: " + key); + Object.keys(options).forEach(key => { + if (!VALID_KEYS.includes(key)) { + throw new Error(`Accounts.config: Invalid key: ${key}`); } }); // set values in Accounts._options - _.each(VALID_KEYS, function (key) { + VALID_KEYS.forEach(key => { if (key in options) { - if (key in self._options) { - throw new Error("Can't set `" + key + "` more than once"); + if (key in this._options) { + throw new Error(`Can't set \`${key}\` more than once`); } - self._options[key] = options[key]; + this._options[key] = options[key]; } }); } @@ -201,7 +229,6 @@ export class AccountsCommon { // It would be much preferable for this to be in accounts_client.js, // but it has to be here because it's needed to create the // Meteor.users collection. - if (options.connection) { this.connection = options.connection; } else if (options.ddpUrl) { @@ -251,16 +278,15 @@ export class AccountsCommon { } _tokenExpiresSoon(when) { - var minLifetimeMs = .1 * this._getTokenLifetimeMs(); - var minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000; - if (minLifetimeMs > minLifetimeCapMs) + let minLifetimeMs = .1 * this._getTokenLifetimeMs(); + const minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000; + if (minLifetimeMs > minLifetimeCapMs) { minLifetimeMs = minLifetimeCapMs; + } return new Date() > (new Date(when) - minLifetimeMs); } } -var Ap = AccountsCommon.prototype; - // Note that Accounts is defined separately in accounts_client.js and // accounts_server.js. @@ -269,64 +295,30 @@ var Ap = AccountsCommon.prototype; * @locus Anywhere but publish functions * @importFromPackage meteor */ -Meteor.userId = function () { - return Accounts.userId(); -}; +Meteor.userId = () => Accounts.userId(); /** * @summary Get the current user record, or `null` if no user is logged in. A reactive data source. * @locus Anywhere but publish functions * @importFromPackage meteor */ -Meteor.user = function () { - return Accounts.user(); -}; +Meteor.user = () => Accounts.user(); // how long (in days) until a login token expires const DEFAULT_LOGIN_EXPIRATION_DAYS = 90; -// Expose for testing. -Ap.DEFAULT_LOGIN_EXPIRATION_DAYS = DEFAULT_LOGIN_EXPIRATION_DAYS; - // how long (in days) until reset password token expires -var DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS = 3; +const DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS = 3; // how long (in days) until enrol password token expires -var DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS = 30; +const DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS = 30; // Clients don't try to auto-login with a token that is going to expire within // .1 * DEFAULT_LOGIN_EXPIRATION_DAYS, capped at MIN_TOKEN_LIFETIME_CAP_SECS. // Tries to avoid abrupt disconnects from expiring tokens. -var MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour +const MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour // how often (in milliseconds) we check for expired tokens -EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes +export const EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes // how long we wait before logging out clients when Meteor.logoutOtherClients is // called -CONNECTION_CLOSE_DELAY_MS = 10 * 1000; - +export const CONNECTION_CLOSE_DELAY_MS = 10 * 1000; // A large number of expiration days (approximately 100 years worth) that is // used when creating unexpiring tokens. const LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100; -// Expose for testing. -Ap.LOGIN_UNEXPIRING_TOKEN_DAYS = LOGIN_UNEXPIRING_TOKEN_DAYS; - -// loginServiceConfiguration and ConfigError are maintained for backwards compatibility -Meteor.startup(function () { - var ServiceConfiguration = - Package['service-configuration'].ServiceConfiguration; - Ap.loginServiceConfiguration = ServiceConfiguration.configurations; - Ap.ConfigError = ServiceConfiguration.ConfigError; -}); - -// Thrown when the user cancels the login process (eg, closes an oauth -// popup, declines retina scan, etc) -var lceName = 'Accounts.LoginCancelledError'; -Ap.LoginCancelledError = Meteor.makeErrorType( - lceName, - function (description) { - this.message = description; - } -); -Ap.LoginCancelledError.prototype.name = lceName; - -// This is used to transmit specific subclass errors over the wire. We should -// come up with a more generic way to do this (eg, with some sort of symbolic -// error code rather than a number). -Ap.LoginCancelledError.numericError = 0x8acdc2f; diff --git a/packages/accounts-base/accounts_rate_limit.js b/packages/accounts-base/accounts_rate_limit.js deleted file mode 100644 index 47e8aee951..0000000000 --- a/packages/accounts-base/accounts_rate_limit.js +++ /dev/null @@ -1,31 +0,0 @@ -import {AccountsCommon} from "./accounts_common.js"; - -var Ap = AccountsCommon.prototype; -var defaultRateLimiterRuleId; -// Removes default rate limiting rule -Ap.removeDefaultRateLimit = function () { - const resp = DDPRateLimiter.removeRule(defaultRateLimiterRuleId); - defaultRateLimiterRuleId = null; - return resp; -}; - -// Add a default rule of limiting logins, creating new users and password reset -// to 5 times every 10 seconds per connection. -Ap.addDefaultRateLimit = function () { - if (!defaultRateLimiterRuleId) { - defaultRateLimiterRuleId = DDPRateLimiter.addRule({ - userId: null, - clientAddress: null, - type: 'method', - name: function (name) { - return _.contains(['login', 'createUser', 'resetPassword', - 'forgotPassword'], name); - }, - connectionId: function (connectionId) { - return true; - } - }, 5, 10000); - } -}; - -Ap.addDefaultRateLimit(); diff --git a/packages/accounts-base/accounts_reconnect_tests.js b/packages/accounts-base/accounts_reconnect_tests.js index 21d2ab9224..fe1f27d9fb 100644 --- a/packages/accounts-base/accounts_reconnect_tests.js +++ b/packages/accounts-base/accounts_reconnect_tests.js @@ -14,17 +14,15 @@ if (Meteor.isClient) { }, onUser1LoggedIn); }; - Tinytest.addAsync('accounts - reconnect auto-login', function(test, done) { - var onReconnectCalls = 0; - var reconnectHandler = function () { - onReconnectCalls++; - }; + Tinytest.addAsync('accounts - reconnect auto-login', (test, done) => { + let onReconnectCalls = 0; + const reconnectHandler = () => onReconnectCalls++; Meteor.connection.onReconnect = reconnectHandler; - var username2 = 'testuser2-' + Random.id(); - var password2 = 'password2-' + Random.id(); - var timeoutHandle; - var onLoginStopper; + const username2 = `testuser2-${Random.id()}`; + const password2 = `password2-${Random.id()}`; + let timeoutHandle; + let onLoginStopper; loginAsUser1((err) => { test.isUndefined(err, 'Unexpected error logging in as user1'); @@ -34,20 +32,20 @@ if (Meteor.isClient) { }, onUser2LoggedIn); }); - function onUser2LoggedIn(err) { + const onUser2LoggedIn = err => { test.isUndefined(err, 'Unexpected error logging in as user2'); onLoginStopper = Accounts.onLogin(onUser2LoggedInAfterReconnect); Meteor.disconnect(); Meteor.reconnect(); } - function onUser2LoggedInAfterReconnect() { + const onUser2LoggedInAfterReconnect = () => { onLoginStopper.stop(); Meteor.loginWithPassword('non-existent-user', 'or-wrong-password', onFailedLogin); } - function onFailedLogin(err) { + const onFailedLogin = err => { test.instanceOf(err, Meteor.Error, 'No Meteor.Error on login failure'); onLoginStopper = Accounts.onLogin(onUser2LoggedInAfterReconnectAfterFailedLogin); Meteor.disconnect(); @@ -55,19 +53,19 @@ if (Meteor.isClient) { timeoutHandle = Meteor.setTimeout(failTest, 1000); } - function failTest() { + const failTest = () => { onLoginStopper.stop(); test.fail('Issue #4970 has occured.'); Meteor.call('getConnectionUserId', checkFinalState); } - function onUser2LoggedInAfterReconnectAfterFailedLogin() { + const onUser2LoggedInAfterReconnectAfterFailedLogin = () => { onLoginStopper.stop(); Meteor.clearTimeout(timeoutHandle); Meteor.call('getConnectionUserId', checkFinalState); } - function checkFinalState(err, connectionUserId) { + const checkFinalState = (err, connectionUserId) => { test.isUndefined(err, 'Unexpected error calling getConnectionUserId'); test.equal(connectionUserId, Meteor.userId(), 'userId is different on client and server'); @@ -83,7 +81,7 @@ if (Meteor.isClient) { // Addresses: https://github.com/meteor/meteor/issues/9140 Tinytest.addAsync( 'accounts - verify single onReconnect callback', - function (test, done) { + (test, done) => { loginAsUser1((err) => { test.isUndefined(err, 'Unexpected error logging in as user1'); test.equal( diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index e0a17077b3..37da4975af 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -1,6 +1,11 @@ -var crypto = Npm.require('crypto'); +import crypto from 'crypto'; +import { + AccountsCommon, + EXPIRE_TOKENS_INTERVAL_MS, + CONNECTION_CLOSE_DELAY_MS +} from './accounts_common.js'; -import {AccountsCommon} from "./accounts_common.js"; +const hasOwn = Object.prototype.hasOwnProperty; /** * @summary Constructor for the `Accounts` namespace on the server. @@ -60,6 +65,15 @@ export class AccountsServer extends AccountsCommon { this._deleteSavedTokensForAllUsersOnStartup(); this._skipCaseInsensitiveChecksForTest = {}; + + // XXX These should probably not actually be public? + this.urls = { + resetPassword: token => Meteor.absoluteUrl(`#/reset-password/${token}`), + verifyEmail: token => Meteor.absoluteUrl(`#/verify-email/${token}`), + enrollAccount: token => Meteor.absoluteUrl(`#/enroll-account/${token}`), + } + + this.addDefaultRateLimit() } /// @@ -133,128 +147,1149 @@ export class AccountsServer extends AccountsCommon { this._onExternalLoginHook = func; } -}; + _validateLogin(connection, attempt) { + this._validateLoginHook.each(callback => { + let ret; + try { + ret = callback(cloneAttemptWithConnection(connection, attempt)); + } + catch (e) { + attempt.allowed = false; + // XXX this means the last thrown error overrides previous error + // messages. Maybe this is surprising to users and we should make + // overriding errors more explicit. (see + // https://github.com/meteor/meteor/issues/1960) + attempt.error = e; + return true; + } + if (! ret) { + attempt.allowed = false; + // don't override a specific error provided by a previous + // validator or the initial attempt (eg "incorrect password"). + if (!attempt.error) + attempt.error = new Meteor.Error(403, "Login forbidden"); + } + return true; + }); + }; -var Ap = AccountsServer.prototype; + _successfulLogin(connection, attempt) { + this._onLoginHook.each(callback => { + callback(cloneAttemptWithConnection(connection, attempt)); + return true; + }); + }; + + _failedLogin(connection, attempt) { + this._onLoginFailureHook.each(callback => { + callback(cloneAttemptWithConnection(connection, attempt)); + return true; + }); + }; + + _successfulLogout(connection, userId) { + const user = userId && this.users.findOne(userId); + this._onLogoutHook.each(callback => { + callback({ user, connection }); + return true; + }); + }; + + /// + /// LOGIN METHODS + /// + + // Login methods return to the client an object containing these + // fields when the user was logged in successfully: + // + // id: userId + // token: * + // tokenExpires: * + // + // tokenExpires is optional and intends to provide a hint to the + // client as to when the token will expire. If not provided, the + // client will call Accounts._tokenExpiration, passing it the date + // that it received the token. + // + // The login method will throw an error back to the client if the user + // failed to log in. + // + // + // Login handlers and service specific login methods such as + // `createUser` internally return a `result` object containing these + // fields: + // + // type: + // optional string; the service name, overrides the handler + // default if present. + // + // error: + // exception; if the user is not allowed to login, the reason why. + // + // userId: + // string; the user id of the user attempting to login (if + // known), required for an allowed login. + // + // options: + // optional object merged into the result returned by the login + // method; used by HAMK from SRP. + // + // stampedLoginToken: + // optional object with `token` and `when` indicating the login + // token is already present in the database, returned by the + // "resume" login handler. + // + // For convenience, login methods can also throw an exception, which + // is converted into an {error} result. However, if the id of the + // user attempting the login is known, a {userId, error} result should + // be returned instead since the user id is not captured when an + // exception is thrown. + // + // This internal `result` object is automatically converted into the + // public {id, token, tokenExpires} object returned to the client. + + // Try a login method, converting thrown exceptions into an {error} + // result. The `type` argument is a default, inserted into the result + // object if not explicitly returned. + // + // Log in a user on a connection. + // + // We use the method invocation to set the user id on the connection, + // not the connection object directly. setUserId is tied to methods to + // enforce clear ordering of method application (using wait methods on + // the client, and a no setUserId after unblock restriction on the + // server) + // + // The `stampedLoginToken` parameter is optional. When present, it + // indicates that the login token has already been inserted into the + // database and doesn't need to be inserted again. (It's used by the + // "resume" login handler). + _loginUser(methodInvocation, userId, stampedLoginToken) { + if (! stampedLoginToken) { + stampedLoginToken = this._generateStampedLoginToken(); + this._insertLoginToken(userId, stampedLoginToken); + } + + // This order (and the avoidance of yields) is important to make + // sure that when publish functions are rerun, they see a + // consistent view of the world: the userId is set and matches + // the login token on the connection (not that there is + // currently a public API for reading the login token on a + // connection). + Meteor._noYieldsAllowed(() => + this._setLoginToken( + userId, + methodInvocation.connection, + this._hashLoginToken(stampedLoginToken.token) + ) + ); + + methodInvocation.setUserId(userId); + + return { + id: userId, + token: stampedLoginToken.token, + tokenExpires: this._tokenExpiration(stampedLoginToken.when) + }; + }; + + // After a login method has completed, call the login hooks. Note + // that `attemptLogin` is called for *all* login attempts, even ones + // which aren't successful (such as an invalid password, etc). + // + // If the login is allowed and isn't aborted by a validate login hook + // callback, log in the user. + // + _attemptLogin( + methodInvocation, + methodName, + methodArgs, + result + ) { + if (!result) + throw new Error("result is required"); + + // XXX A programming error in a login handler can lead to this occuring, and + // then we don't call onLogin or onLoginFailure callbacks. Should + // tryLoginMethod catch this case and turn it into an error? + if (!result.userId && !result.error) + throw new Error("A login method must specify a userId or an error"); + + let user; + if (result.userId) + user = this.users.findOne(result.userId); + + const attempt = { + type: result.type || "unknown", + allowed: !! (result.userId && !result.error), + methodName: methodName, + methodArguments: Array.from(methodArgs) + }; + if (result.error) { + attempt.error = result.error; + } + if (user) { + attempt.user = user; + } + + // _validateLogin may mutate `attempt` by adding an error and changing allowed + // to false, but that's the only change it can make (and the user's callbacks + // only get a clone of `attempt`). + this._validateLogin(methodInvocation.connection, attempt); + + if (attempt.allowed) { + const ret = { + ...this._loginUser( + methodInvocation, + result.userId, + result.stampedLoginToken + ), + ...result.options + }; + ret.type = attempt.type; + this._successfulLogin(methodInvocation.connection, attempt); + return ret; + } + else { + this._failedLogin(methodInvocation.connection, attempt); + throw attempt.error; + } + }; + + // All service specific login methods should go through this function. + // Ensure that thrown exceptions are caught and that login hook + // callbacks are still called. + // + _loginMethod( + methodInvocation, + methodName, + methodArgs, + type, + fn + ) { + return this._attemptLogin( + methodInvocation, + methodName, + methodArgs, + tryLoginMethod(type, fn) + ); + }; + + + // Report a login attempt failed outside the context of a normal login + // method. This is for use in the case where there is a multi-step login + // procedure (eg SRP based password login). If a method early in the + // chain fails, it should call this function to report a failure. There + // is no corresponding method for a successful login; methods that can + // succeed at logging a user in should always be actual login methods + // (using either Accounts._loginMethod or Accounts.registerLoginHandler). + _reportLoginFailure( + methodInvocation, + methodName, + methodArgs, + result + ) { + const attempt = { + type: result.type || "unknown", + allowed: false, + error: result.error, + methodName: methodName, + methodArguments: Array.from(methodArgs) + }; + + if (result.userId) { + attempt.user = this.users.findOne(result.userId); + } + + this._validateLogin(methodInvocation.connection, attempt); + this._failedLogin(methodInvocation.connection, attempt); + + // _validateLogin may mutate attempt to set a new error message. Return + // the modified version. + return attempt; + }; + + /// + /// LOGIN HANDLERS + /// + + // The main entry point for auth packages to hook in to login. + // + // A login handler is a login method which can return `undefined` to + // indicate that the login request is not handled by this handler. + // + // @param name {String} Optional. The service name, used by default + // if a specific service name isn't returned in the result. + // + // @param handler {Function} A function that receives an options object + // (as passed as an argument to the `login` method) and returns one of: + // - `undefined`, meaning don't handle; + // - a login method result object + + registerLoginHandler(name, handler) { + if (! handler) { + handler = name; + name = null; + } + + this._loginHandlers.push({ + name: name, + handler: handler + }); + }; + + + // Checks a user's credentials against all the registered login + // handlers, and returns a login token if the credentials are valid. It + // is like the login method, except that it doesn't set the logged-in + // user on the connection. Throws a Meteor.Error if logging in fails, + // including the case where none of the login handlers handled the login + // request. Otherwise, returns {id: userId, token: *, tokenExpires: *}. + // + // For example, if you want to login with a plaintext password, `options` could be + // { user: { username: }, password: }, or + // { user: { email: }, password: }. + + // Try all of the registered login handlers until one of them doesn't + // return `undefined`, meaning it handled this call to `login`. Return + // that return value. + _runLoginHandlers(methodInvocation, options) { + for (let handler of this._loginHandlers) { + const result = tryLoginMethod( + handler.name, + () => handler.handler.call(methodInvocation, options) + ); + + if (result) { + return result; + } + + if (result !== undefined) { + throw new Meteor.Error(400, "A login handler should return a result or undefined"); + } + } + + return { + type: null, + error: new Meteor.Error(400, "Unrecognized options for login request") + }; + }; + + // Deletes the given loginToken from the database. + // + // For new-style hashed token, this will cause all connections + // associated with the token to be closed. + // + // Any connections associated with old-style unhashed tokens will be + // in the process of becoming associated with hashed tokens and then + // they'll get closed. + destroyToken(userId, loginToken) { + this.users.update(userId, { + $pull: { + "services.resume.loginTokens": { + $or: [ + { hashedToken: loginToken }, + { token: loginToken } + ] + } + } + }); + }; + + _initServerMethods() { + // The methods created in this function need to be created here so that + // this variable is available in their scope. + const accounts = this; + + + // This object will be populated with methods and then passed to + // accounts._server.methods further below. + const methods = {}; + + // @returns {Object|null} + // If successful, returns {token: reconnectToken, id: userId} + // If unsuccessful (for example, if the user closed the oauth login popup), + // throws an error describing the reason + methods.login = function (options) { + // Login handlers should really also check whatever field they look at in + // options, but we don't enforce it. + check(options, Object); + + const result = accounts._runLoginHandlers(this, options); + + return accounts._attemptLogin(this, "login", arguments, result); + }; + + methods.logout = function () { + const token = accounts._getLoginToken(this.connection.id); + accounts._setLoginToken(this.userId, this.connection, null); + if (token && this.userId) { + accounts.destroyToken(this.userId, token); + } + accounts._successfulLogout(this.connection, this.userId); + this.setUserId(null); + }; + + // Delete all the current user's tokens and close all open connections logged + // in as this user. Returns a fresh new login token that this client can + // use. Tests set Accounts._noConnectionCloseDelayForTest to delete tokens + // immediately instead of using a delay. + // + // XXX COMPAT WITH 0.7.2 + // This single `logoutOtherClients` method has been replaced with two + // methods, one that you call to get a new token, and another that you + // call to remove all tokens except your own. The new design allows + // clients to know when other clients have actually been logged + // out. (The `logoutOtherClients` method guarantees the caller that + // the other clients will be logged out at some point, but makes no + // guarantees about when.) This method is left in for backwards + // compatibility, especially since application code might be calling + // this method directly. + // + // @returns {Object} Object with token and tokenExpires keys. + methods.logoutOtherClients = function () { + const user = accounts.users.findOne(this.userId, { + fields: { + "services.resume.loginTokens": true + } + }); + if (user) { + // Save the current tokens in the database to be deleted in + // CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the + // caller's browser time to find the fresh token in localStorage. We save + // the tokens in the database in case we crash before actually deleting + // them. + const tokens = user.services.resume.loginTokens; + const newToken = accounts._generateStampedLoginToken(); + accounts.users.update(this.userId, { + $set: { + "services.resume.loginTokensToDelete": tokens, + "services.resume.haveLoginTokensToDelete": true + }, + $push: { "services.resume.loginTokens": accounts._hashStampedToken(newToken) } + }); + Meteor.setTimeout(() => { + // The observe on Meteor.users will take care of closing the connections + // associated with `tokens`. + accounts._deleteSavedTokensForUser(this.userId, tokens); + }, accounts._noConnectionCloseDelayForTest ? 0 : + CONNECTION_CLOSE_DELAY_MS); + // We do not set the login token on this connection, but instead the + // observe closes the connection and the client will reconnect with the + // new token. + return { + token: newToken.token, + tokenExpires: accounts._tokenExpiration(newToken.when) + }; + } else { + throw new Meteor.Error("You are not logged in."); + } + }; + + // Generates a new login token with the same expiration as the + // connection's current token and saves it to the database. Associates + // the connection with this new token and returns it. Throws an error + // if called on a connection that isn't logged in. + // + // @returns Object + // If successful, returns { token: , id: , + // tokenExpires: }. + methods.getNewToken = function () { + const user = accounts.users.findOne(this.userId, { + fields: { "services.resume.loginTokens": 1 } + }); + if (! this.userId || ! user) { + throw new Meteor.Error("You are not logged in."); + } + // Be careful not to generate a new token that has a later + // expiration than the curren token. Otherwise, a bad guy with a + // stolen token could use this method to stop his stolen token from + // ever expiring. + const currentHashedToken = accounts._getLoginToken(this.connection.id); + const currentStampedToken = user.services.resume.loginTokens.find( + stampedToken => stampedToken.hashedToken === currentHashedToken + ); + if (! currentStampedToken) { // safety belt: this should never happen + throw new Meteor.Error("Invalid login token"); + } + const newStampedToken = accounts._generateStampedLoginToken(); + newStampedToken.when = currentStampedToken.when; + accounts._insertLoginToken(this.userId, newStampedToken); + return accounts._loginUser(this, this.userId, newStampedToken); + }; + + // Removes all tokens except the token associated with the current + // connection. Throws an error if the connection is not logged + // in. Returns nothing on success. + methods.removeOtherTokens = function () { + if (! this.userId) { + throw new Meteor.Error("You are not logged in."); + } + const currentToken = accounts._getLoginToken(this.connection.id); + accounts.users.update(this.userId, { + $pull: { + "services.resume.loginTokens": { hashedToken: { $ne: currentToken } } + } + }); + }; + + // Allow a one-time configuration for a login service. Modifications + // to this collection are also allowed in insecure mode. + methods.configureLoginService = (options) => { + check(options, Match.ObjectIncluding({service: String})); + // Don't let random users configure a service we haven't added yet (so + // that when we do later add it, it's set up with their configuration + // instead of ours). + // XXX if service configuration is oauth-specific then this code should + // be in accounts-oauth; if it's not then the registry should be + // in this package + if (!(accounts.oauth + && accounts.oauth.serviceNames().includes(options.service))) { + throw new Meteor.Error(403, "Service unknown"); + } + + const { ServiceConfiguration } = Package['service-configuration']; + if (ServiceConfiguration.configurations.findOne({service: options.service})) + throw new Meteor.Error(403, `Service ${options.service} already configured`); + + if (hasOwn.call(options, 'secret') && usingOAuthEncryption()) + options.secret = OAuthEncryption.seal(options.secret); + + ServiceConfiguration.configurations.insert(options); + }; + + accounts._server.methods(methods); + }; + + _initAccountDataHooks() { + this._server.onConnection(connection => { + this._accountData[connection.id] = { + connection: connection + }; + + connection.onClose(() => { + this._removeTokenFromConnection(connection.id); + delete this._accountData[connection.id]; + }); + }); + }; + + _initServerPublications() { + // Bring into lexical scope for publish callbacks that need `this` + const { users, _autopublishFields } = this; + + // Publish all login service configuration fields other than secret. + this._server.publish("meteor.loginServiceConfiguration", () => { + const { ServiceConfiguration } = Package['service-configuration']; + return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); + }, {is_auto: true}); // not techincally autopublish, but stops the warning. + + // Publish the current user's record to the client. + this._server.publish(null, function () { + if (this.userId) { + return users.find({ + _id: this.userId + }, { + fields: { + profile: 1, + username: 1, + emails: 1 + } + }); + } else { + return null; + } + }, /*suppress autopublish warning*/{is_auto: true}); + + // Use Meteor.startup to give other packages a chance to call + // addAutopublishFields. + Package.autopublish && Meteor.startup(() => { + // ['profile', 'username'] -> {profile: 1, username: 1} + const toFieldSelector = fields => fields.reduce((prev, field) => ( + { ...prev, [field]: 1 }), + {} + ); + this._server.publish(null, function () { + if (this.userId) { + return users.find({ _id: this.userId }, { + fields: toFieldSelector(_autopublishFields.loggedInUser), + }) + } else { + return null; + } + }, /*suppress autopublish warning*/{is_auto: true}); + + // XXX this publish is neither dedup-able nor is it optimized by our special + // treatment of queries on a specific _id. Therefore this will have O(n^2) + // run-time performance every time a user document is changed (eg someone + // logging in). If this is a problem, we can instead write a manual publish + // function which filters out fields based on 'this.userId'. + this._server.publish(null, function () { + const selector = this.userId ? { _id: { $ne: this.userId } } : {}; + return users.find(selector, { + fields: toFieldSelector(_autopublishFields.otherUsers), + }) + }, /*suppress autopublish warning*/{is_auto: true}); + }); + }; + + // Add to the list of fields or subfields to be automatically + // published if autopublish is on. Must be called from top-level + // code (ie, before Meteor.startup hooks run). + // + // @param opts {Object} with: + // - forLoggedInUser {Array} Array of fields published to the logged-in user + // - forOtherUsers {Array} Array of fields published to users that aren't logged in + addAutopublishFields(opts) { + this._autopublishFields.loggedInUser.push.apply( + this._autopublishFields.loggedInUser, opts.forLoggedInUser); + this._autopublishFields.otherUsers.push.apply( + this._autopublishFields.otherUsers, opts.forOtherUsers); + }; + + /// + /// ACCOUNT DATA + /// + + // HACK: This is used by 'meteor-accounts' to get the loginToken for a + // connection. Maybe there should be a public way to do that. + _getAccountData(connectionId, field) { + const data = this._accountData[connectionId]; + return data && data[field]; + }; + + _setAccountData(connectionId, field, value) { + const data = this._accountData[connectionId]; + + // safety belt. shouldn't happen. accountData is set in onConnection, + // we don't have a connectionId until it is set. + if (!data) + return; + + if (value === undefined) + delete data[field]; + else + data[field] = value; + }; + + /// + /// RECONNECT TOKENS + /// + /// support reconnecting using a meteor login token + + _hashLoginToken(loginToken) { + const hash = crypto.createHash('sha256'); + hash.update(loginToken); + return hash.digest('base64'); + }; + + // {token, when} => {hashedToken, when} + _hashStampedToken(stampedToken) { + const hashedStampedToken = Object.keys(stampedToken).reduce( + (prev, key) => key === 'token' ? + prev : + { ...prev, [key]: stampedToken[key] }, + {}, + ) + return { + ...hashedStampedToken, + hashedToken: this._hashLoginToken(stampedToken.token) + }; + }; + + // Using $addToSet avoids getting an index error if another client + // logging in simultaneously has already inserted the new hashed + // token. + _insertHashedLoginToken(userId, hashedToken, query) { + query = query ? { ...query } : {}; + query._id = userId; + this.users.update(query, { + $addToSet: { + "services.resume.loginTokens": hashedToken + } + }); + }; + + // Exported for tests. + _insertLoginToken(userId, stampedToken, query) { + this._insertHashedLoginToken( + userId, + this._hashStampedToken(stampedToken), + query + ); + }; + + _clearAllLoginTokens(userId) { + this.users.update(userId, { + $set: { + 'services.resume.loginTokens': [] + } + }); + }; + + // test hook + _getUserObserve(connectionId) { + return this._userObservesForConnections[connectionId]; + }; + + // Clean up this connection's association with the token: that is, stop + // the observe that we started when we associated the connection with + // this token. + _removeTokenFromConnection(connectionId) { + if (hasOwn.call(this._userObservesForConnections, connectionId)) { + const observe = this._userObservesForConnections[connectionId]; + if (typeof observe === 'number') { + // We're in the process of setting up an observe for this connection. We + // can't clean up that observe yet, but if we delete the placeholder for + // this connection, then the observe will get cleaned up as soon as it has + // been set up. + delete this._userObservesForConnections[connectionId]; + } else { + delete this._userObservesForConnections[connectionId]; + observe.stop(); + } + } + }; + + _getLoginToken(connectionId) { + return this._getAccountData(connectionId, 'loginToken'); + }; + + // newToken is a hashed token. + _setLoginToken(userId, connection, newToken) { + this._removeTokenFromConnection(connection.id); + this._setAccountData(connection.id, 'loginToken', newToken); + + if (newToken) { + // Set up an observe for this token. If the token goes away, we need + // to close the connection. We defer the observe because there's + // no need for it to be on the critical path for login; we just need + // to ensure that the connection will get closed at some point if + // the token gets deleted. + // + // Initially, we set the observe for this connection to a number; this + // signifies to other code (which might run while we yield) that we are in + // the process of setting up an observe for this connection. Once the + // observe is ready to go, we replace the number with the real observe + // handle (unless the placeholder has been deleted or replaced by a + // different placehold number, signifying that the connection was closed + // already -- in this case we just clean up the observe that we started). + const myObserveNumber = ++this._nextUserObserveNumber; + this._userObservesForConnections[connection.id] = myObserveNumber; + Meteor.defer(() => { + // If something else happened on this connection in the meantime (it got + // closed, or another call to _setLoginToken happened), just do + // nothing. We don't need to start an observe for an old connection or old + // token. + if (this._userObservesForConnections[connection.id] !== myObserveNumber) { + return; + } + + let foundMatchingUser; + // Because we upgrade unhashed login tokens to hashed tokens at + // login time, sessions will only be logged in with a hashed + // token. Thus we only need to observe hashed tokens here. + const observe = this.users.find({ + _id: userId, + 'services.resume.loginTokens.hashedToken': newToken + }, { fields: { _id: 1 } }).observeChanges({ + added: () => { + foundMatchingUser = true; + }, + removed: connection.close, + // The onClose callback for the connection takes care of + // cleaning up the observe handle and any other state we have + // lying around. + }); + + // If the user ran another login or logout command we were waiting for the + // defer or added to fire (ie, another call to _setLoginToken occurred), + // then we let the later one win (start an observe, etc) and just stop our + // observe now. + // + // Similarly, if the connection was already closed, then the onClose + // callback would have called _removeTokenFromConnection and there won't + // be an entry in _userObservesForConnections. We can stop the observe. + if (this._userObservesForConnections[connection.id] !== myObserveNumber) { + observe.stop(); + return; + } + + this._userObservesForConnections[connection.id] = observe; + + if (! foundMatchingUser) { + // We've set up an observe on the user associated with `newToken`, + // so if the new token is removed from the database, we'll close + // the connection. But the token might have already been deleted + // before we set up the observe, which wouldn't have closed the + // connection because the observe wasn't running yet. + connection.close(); + } + }); + } + }; + + // (Also used by Meteor Accounts server and tests). + // + _generateStampedLoginToken() { + return { + token: Random.secret(), + when: new Date + }; + }; + + /// + /// TOKEN EXPIRATION + /// + + // Deletes expired password reset tokens from the database. + // + // Exported for tests. Also, the arguments are only used by + // tests. oldestValidDate is simulate expiring tokens without waiting + // for them to actually expire. userId is used by tests to only expire + // tokens for the test user. + _expirePasswordResetTokens(oldestValidDate, userId) { + const tokenLifetimeMs = this._getPasswordResetTokenLifetimeMs(); + + // when calling from a test with extra arguments, you must specify both! + if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { + throw new Error("Bad test. Must specify both oldestValidDate and userId."); + } + + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); + + const tokenFilter = { + $or: [ + { "services.password.reset.reason": "reset"}, + { "services.password.reset.reason": {$exists: false}} + ] + }; + + expirePasswordToken(this, oldestValidDate, tokenFilter, userId); + } + + // Deletes expired password enroll tokens from the database. + // + // Exported for tests. Also, the arguments are only used by + // tests. oldestValidDate is simulate expiring tokens without waiting + // for them to actually expire. userId is used by tests to only expire + // tokens for the test user. + _expirePasswordEnrollTokens(oldestValidDate, userId) { + const tokenLifetimeMs = this._getPasswordEnrollTokenLifetimeMs(); + + // when calling from a test with extra arguments, you must specify both! + if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { + throw new Error("Bad test. Must specify both oldestValidDate and userId."); + } + + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); + + const tokenFilter = { + "services.password.reset.reason": "enroll" + }; + + expirePasswordToken(this, oldestValidDate, tokenFilter, userId); + } + + // Deletes expired tokens from the database and closes all open connections + // associated with these tokens. + // + // Exported for tests. Also, the arguments are only used by + // tests. oldestValidDate is simulate expiring tokens without waiting + // for them to actually expire. userId is used by tests to only expire + // tokens for the test user. + _expireTokens(oldestValidDate, userId) { + const tokenLifetimeMs = this._getTokenLifetimeMs(); + + // when calling from a test with extra arguments, you must specify both! + if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { + throw new Error("Bad test. Must specify both oldestValidDate and userId."); + } + + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); + const userFilter = userId ? {_id: userId} : {}; + + + // Backwards compatible with older versions of meteor that stored login token + // timestamps as numbers. + this.users.update({ ...userFilter, + $or: [ + { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, + { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } + ] + }, { + $pull: { + "services.resume.loginTokens": { + $or: [ + { when: { $lt: oldestValidDate } }, + { when: { $lt: +oldestValidDate } } + ] + } + } + }, { multi: true }); + // The observe on Meteor.users will take care of closing connections for + // expired tokens. + }; + + // @override from accounts_common.js + config(options) { + // Call the overridden implementation of the method. + const superResult = AccountsCommon.prototype.config.apply(this, arguments); + + // If the user set loginExpirationInDays to null, then we need to clear the + // timer that periodically expires tokens. + if (hasOwn.call(this._options, 'loginExpirationInDays') && + this._options.loginExpirationInDays === null && + this.expireTokenInterval) { + Meteor.clearInterval(this.expireTokenInterval); + this.expireTokenInterval = null; + } + + return superResult; + }; + + // Called by accounts-password + insertUserDoc(options, user) { + // - clone user document, to protect from modification + // - add createdAt timestamp + // - prepare an _id, so that you can modify other collections (eg + // create a first task for every new user) + // + // XXX If the onCreateUser or validateNewUser hooks fail, we might + // end up having modified some other collection + // inappropriately. The solution is probably to have onCreateUser + // accept two callbacks - one that gets called before inserting + // the user document (in which you can modify its contents), and + // one that gets called after (in which you should change other + // collections) + user = { + createdAt: new Date(), + _id: Random.id(), + ...user, + }; + + if (user.services) { + Object.keys(user.services).forEach(service => + pinEncryptedFieldsToUser(user.services[service], user._id) + ); + } + + let fullUser; + if (this._onCreateUserHook) { + fullUser = this._onCreateUserHook(options, user); + + // This is *not* part of the API. We need this because we can't isolate + // the global server environment between tests, meaning we can't test + // both having a create user hook set and not having one set. + if (fullUser === 'TEST DEFAULT HOOK') + fullUser = defaultCreateUserHook(options, user); + } else { + fullUser = defaultCreateUserHook(options, user); + } + + this._validateNewUserHooks.forEach(hook => { + if (! hook(fullUser)) + throw new Meteor.Error(403, "User validation failed"); + }); + + let userId; + try { + userId = this.users.insert(fullUser); + } catch (e) { + // XXX string parsing sucks, maybe + // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day + if (!e.errmsg) throw e; + if (e.errmsg.includes('emails.address')) + throw new Meteor.Error(403, "Email already exists."); + if (e.errmsg.includes('username')) + throw new Meteor.Error(403, "Username already exists."); + throw e; + } + return userId; + }; + + // Helper function: returns false if email does not match company domain from + // the configuration. + _testEmailDomain(email) { + const domain = this._options.restrictCreationByEmailDomain; + + return !domain || + (typeof domain === 'function' && domain(email)) || + (typeof domain === 'string' && + (new RegExp(`@${Meteor._escapeRegExp(domain)}$`, 'i')).test(email)); + }; + + /// + /// CLEAN UP FOR `logoutOtherClients` + /// + + _deleteSavedTokensForUser(userId, tokensToDelete) { + if (tokensToDelete) { + this.users.update(userId, { + $unset: { + "services.resume.haveLoginTokensToDelete": 1, + "services.resume.loginTokensToDelete": 1 + }, + $pullAll: { + "services.resume.loginTokens": tokensToDelete + } + }); + } + }; + + _deleteSavedTokensForAllUsersOnStartup() { + // If we find users who have saved tokens to delete on startup, delete + // them now. It's possible that the server could have crashed and come + // back up before new tokens are found in localStorage, but this + // shouldn't happen very often. We shouldn't put a delay here because + // that would give a lot of power to an attacker with a stolen login + // token and the ability to crash the server. + Meteor.startup(() => { + this.users.find({ + "services.resume.haveLoginTokensToDelete": true + }, { + "services.resume.loginTokensToDelete": 1 + }).forEach(user => { + this._deleteSavedTokensForUser( + user._id, + user.services.resume.loginTokensToDelete + ); + }); + }); + }; + + /// + /// MANAGING USER OBJECTS + /// + + // Updates or creates a user after we authenticate with a 3rd party. + // + // @param serviceName {String} Service name (eg, twitter). + // @param serviceData {Object} Data to store in the user's record + // under services[serviceName]. Must include an "id" field + // which is a unique identifier for the user in the service. + // @param options {Object, optional} Other options to pass to insertUserDoc + // (eg, profile) + // @returns {Object} Object with token and id keys, like the result + // of the "login" method. + // + updateOrCreateUserFromExternalService( + serviceName, + serviceData, + options + ) { + options = { ...options }; + + if (serviceName === "password" || serviceName === "resume") { + throw new Error( + "Can't use updateOrCreateUserFromExternalService with internal service " + + serviceName); + } + if (!hasOwn.call(serviceData, 'id')) { + throw new Error( + `Service data for service ${serviceName} must include id`); + } + + // Look for a user with the appropriate service user id. + const selector = {}; + const serviceIdKey = `services.${serviceName}.id`; + + // XXX Temporary special case for Twitter. (Issue #629) + // The serviceData.id will be a string representation of an integer. + // We want it to match either a stored string or int representation. + // This is to cater to earlier versions of Meteor storing twitter + // user IDs in number form, and recent versions storing them as strings. + // This can be removed once migration technology is in place, and twitter + // users stored with integer IDs have been migrated to string IDs. + if (serviceName === "twitter" && !isNaN(serviceData.id)) { + selector["$or"] = [{},{}]; + selector["$or"][0][serviceIdKey] = serviceData.id; + selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); + } else { + selector[serviceIdKey] = serviceData.id; + } + + let user = this.users.findOne(selector); + + // When creating a new user we pass through all options. When updating an + // existing user, by default we only process/pass through the serviceData + // (eg, so that we keep an unexpired access token and don't cache old email + // addresses in serviceData.email). The onExternalLogin hook can be used when + // creating or updating a user, to modify or pass through more options as + // needed. + let opts = user ? {} : options; + if (this._onExternalLoginHook) { + opts = this._onExternalLoginHook(options, user); + } + + if (user) { + pinEncryptedFieldsToUser(serviceData, user._id); + + let setAttrs = {}; + Object.keys(serviceData).forEach(key => + setAttrs[`services.${serviceName}.${key}`] = serviceData[key] + ); + + // XXX Maybe we should re-use the selector above and notice if the update + // touches nothing? + setAttrs = { ...setAttrs, ...opts }; + this.users.update(user._id, { + $set: setAttrs + }); + + return { + type: serviceName, + userId: user._id + }; + } else { + // Create a new user with the service data. + user = {services: {}}; + user.services[serviceName] = serviceData; + return { + type: serviceName, + userId: this.insertUserDoc(opts, user) + }; + } + }; + + // Removes default rate limiting rule + removeDefaultRateLimit() { + const resp = DDPRateLimiter.removeRule(this.defaultRateLimiterRuleId); + this.defaultRateLimiterRuleId = null; + return resp; + }; + + // Add a default rule of limiting logins, creating new users and password reset + // to 5 times every 10 seconds per connection. + addDefaultRateLimit() { + if (!this.defaultRateLimiterRuleId) { + this.defaultRateLimiterRuleId = DDPRateLimiter.addRule({ + userId: null, + clientAddress: null, + type: 'method', + name: name => ['login', 'createUser', 'resetPassword', 'forgotPassword'] + .includes(name), + connectionId: (connectionId) => true, + }, 5, 10000); + } + }; + +} // Give each login hook callback a fresh cloned copy of the attempt // object, but don't clone the connection. // -function cloneAttemptWithConnection(connection, attempt) { - var clonedAttempt = EJSON.clone(attempt); +const cloneAttemptWithConnection = (connection, attempt) => { + const clonedAttempt = EJSON.clone(attempt); clonedAttempt.connection = connection; return clonedAttempt; -} - -Ap._validateLogin = function (connection, attempt) { - this._validateLoginHook.each(function (callback) { - var ret; - try { - ret = callback(cloneAttemptWithConnection(connection, attempt)); - } - catch (e) { - attempt.allowed = false; - // XXX this means the last thrown error overrides previous error - // messages. Maybe this is surprising to users and we should make - // overriding errors more explicit. (see - // https://github.com/meteor/meteor/issues/1960) - attempt.error = e; - return true; - } - if (! ret) { - attempt.allowed = false; - // don't override a specific error provided by a previous - // validator or the initial attempt (eg "incorrect password"). - if (!attempt.error) - attempt.error = new Meteor.Error(403, "Login forbidden"); - } - return true; - }); }; - -Ap._successfulLogin = function (connection, attempt) { - this._onLoginHook.each(function (callback) { - callback(cloneAttemptWithConnection(connection, attempt)); - return true; - }); -}; - -Ap._failedLogin = function (connection, attempt) { - this._onLoginFailureHook.each(function (callback) { - callback(cloneAttemptWithConnection(connection, attempt)); - return true; - }); -}; - -Ap._successfulLogout = function (connection, userId) { - const user = userId && this.users.findOne(userId); - this._onLogoutHook.each(function (callback) { - callback({ user, connection }); - return true; - }); -}; - -/// -/// LOGIN METHODS -/// - -// Login methods return to the client an object containing these -// fields when the user was logged in successfully: -// -// id: userId -// token: * -// tokenExpires: * -// -// tokenExpires is optional and intends to provide a hint to the -// client as to when the token will expire. If not provided, the -// client will call Accounts._tokenExpiration, passing it the date -// that it received the token. -// -// The login method will throw an error back to the client if the user -// failed to log in. -// -// -// Login handlers and service specific login methods such as -// `createUser` internally return a `result` object containing these -// fields: -// -// type: -// optional string; the service name, overrides the handler -// default if present. -// -// error: -// exception; if the user is not allowed to login, the reason why. -// -// userId: -// string; the user id of the user attempting to login (if -// known), required for an allowed login. -// -// options: -// optional object merged into the result returned by the login -// method; used by HAMK from SRP. -// -// stampedLoginToken: -// optional object with `token` and `when` indicating the login -// token is already present in the database, returned by the -// "resume" login handler. -// -// For convenience, login methods can also throw an exception, which -// is converted into an {error} result. However, if the id of the -// user attempting the login is known, a {userId, error} result should -// be returned instead since the user id is not captured when an -// exception is thrown. -// -// This internal `result` object is automatically converted into the -// public {id, token, tokenExpires} object returned to the client. - - -// Try a login method, converting thrown exceptions into an {error} -// result. The `type` argument is a default, inserted into the result -// object if not explicitly returned. -// -var tryLoginMethod = function (type, fn) { - var result; +const tryLoginMethod = (type, fn) => { + let result; try { result = fn(); } @@ -268,733 +1303,25 @@ var tryLoginMethod = function (type, fn) { return result; }; - -// Log in a user on a connection. -// -// We use the method invocation to set the user id on the connection, -// not the connection object directly. setUserId is tied to methods to -// enforce clear ordering of method application (using wait methods on -// the client, and a no setUserId after unblock restriction on the -// server) -// -// The `stampedLoginToken` parameter is optional. When present, it -// indicates that the login token has already been inserted into the -// database and doesn't need to be inserted again. (It's used by the -// "resume" login handler). -Ap._loginUser = function (methodInvocation, userId, stampedLoginToken) { - var self = this; - - if (! stampedLoginToken) { - stampedLoginToken = self._generateStampedLoginToken(); - self._insertLoginToken(userId, stampedLoginToken); - } - - // This order (and the avoidance of yields) is important to make - // sure that when publish functions are rerun, they see a - // consistent view of the world: the userId is set and matches - // the login token on the connection (not that there is - // currently a public API for reading the login token on a - // connection). - Meteor._noYieldsAllowed(function () { - self._setLoginToken( - userId, - methodInvocation.connection, - self._hashLoginToken(stampedLoginToken.token) - ); - }); - - methodInvocation.setUserId(userId); - - return { - id: userId, - token: stampedLoginToken.token, - tokenExpires: self._tokenExpiration(stampedLoginToken.when) - }; -}; - - -// After a login method has completed, call the login hooks. Note -// that `attemptLogin` is called for *all* login attempts, even ones -// which aren't successful (such as an invalid password, etc). -// -// If the login is allowed and isn't aborted by a validate login hook -// callback, log in the user. -// -Ap._attemptLogin = function ( - methodInvocation, - methodName, - methodArgs, - result -) { - if (!result) - throw new Error("result is required"); - - // XXX A programming error in a login handler can lead to this occuring, and - // then we don't call onLogin or onLoginFailure callbacks. Should - // tryLoginMethod catch this case and turn it into an error? - if (!result.userId && !result.error) - throw new Error("A login method must specify a userId or an error"); - - var user; - if (result.userId) - user = this.users.findOne(result.userId); - - var attempt = { - type: result.type || "unknown", - allowed: !! (result.userId && !result.error), - methodName: methodName, - methodArguments: _.toArray(methodArgs) - }; - if (result.error) - attempt.error = result.error; - if (user) - attempt.user = user; - - // _validateLogin may mutate `attempt` by adding an error and changing allowed - // to false, but that's the only change it can make (and the user's callbacks - // only get a clone of `attempt`). - this._validateLogin(methodInvocation.connection, attempt); - - if (attempt.allowed) { - var ret = _.extend( - this._loginUser( - methodInvocation, - result.userId, - result.stampedLoginToken - ), - result.options || {} - ); - ret.type = attempt.type; - this._successfulLogin(methodInvocation.connection, attempt); - return ret; - } - else { - this._failedLogin(methodInvocation.connection, attempt); - throw attempt.error; - } -}; - - -// All service specific login methods should go through this function. -// Ensure that thrown exceptions are caught and that login hook -// callbacks are still called. -// -Ap._loginMethod = function ( - methodInvocation, - methodName, - methodArgs, - type, - fn -) { - return this._attemptLogin( - methodInvocation, - methodName, - methodArgs, - tryLoginMethod(type, fn) - ); -}; - - -// Report a login attempt failed outside the context of a normal login -// method. This is for use in the case where there is a multi-step login -// procedure (eg SRP based password login). If a method early in the -// chain fails, it should call this function to report a failure. There -// is no corresponding method for a successful login; methods that can -// succeed at logging a user in should always be actual login methods -// (using either Accounts._loginMethod or Accounts.registerLoginHandler). -Ap._reportLoginFailure = function ( - methodInvocation, - methodName, - methodArgs, - result -) { - var attempt = { - type: result.type || "unknown", - allowed: false, - error: result.error, - methodName: methodName, - methodArguments: _.toArray(methodArgs) - }; - - if (result.userId) { - attempt.user = this.users.findOne(result.userId); - } - - this._validateLogin(methodInvocation.connection, attempt); - this._failedLogin(methodInvocation.connection, attempt); - - // _validateLogin may mutate attempt to set a new error message. Return - // the modified version. - return attempt; -}; - - -/// -/// LOGIN HANDLERS -/// - -// The main entry point for auth packages to hook in to login. -// -// A login handler is a login method which can return `undefined` to -// indicate that the login request is not handled by this handler. -// -// @param name {String} Optional. The service name, used by default -// if a specific service name isn't returned in the result. -// -// @param handler {Function} A function that receives an options object -// (as passed as an argument to the `login` method) and returns one of: -// - `undefined`, meaning don't handle; -// - a login method result object - -Ap.registerLoginHandler = function (name, handler) { - if (! handler) { - handler = name; - name = null; - } - - this._loginHandlers.push({ - name: name, - handler: handler - }); -}; - - -// Checks a user's credentials against all the registered login -// handlers, and returns a login token if the credentials are valid. It -// is like the login method, except that it doesn't set the logged-in -// user on the connection. Throws a Meteor.Error if logging in fails, -// including the case where none of the login handlers handled the login -// request. Otherwise, returns {id: userId, token: *, tokenExpires: *}. -// -// For example, if you want to login with a plaintext password, `options` could be -// { user: { username: }, password: }, or -// { user: { email: }, password: }. - -// Try all of the registered login handlers until one of them doesn't -// return `undefined`, meaning it handled this call to `login`. Return -// that return value. -Ap._runLoginHandlers = function (methodInvocation, options) { - for (var i = 0; i < this._loginHandlers.length; ++i) { - var handler = this._loginHandlers[i]; - - var result = tryLoginMethod( - handler.name, - function () { - return handler.handler.call(methodInvocation, options); - } - ); - - if (result) { - return result; - } - - if (result !== undefined) { - throw new Meteor.Error(400, "A login handler should return a result or undefined"); - } - } - - return { - type: null, - error: new Meteor.Error(400, "Unrecognized options for login request") - }; -}; - -// Deletes the given loginToken from the database. -// -// For new-style hashed token, this will cause all connections -// associated with the token to be closed. -// -// Any connections associated with old-style unhashed tokens will be -// in the process of becoming associated with hashed tokens and then -// they'll get closed. -Ap.destroyToken = function (userId, loginToken) { - this.users.update(userId, { - $pull: { - "services.resume.loginTokens": { - $or: [ - { hashedToken: loginToken }, - { token: loginToken } - ] - } - } - }); -}; - -Ap._initServerMethods = function () { - // The methods created in this function need to be created here so that - // this variable is available in their scope. - var accounts = this; - - // This object will be populated with methods and then passed to - // accounts._server.methods further below. - var methods = {}; - - // @returns {Object|null} - // If successful, returns {token: reconnectToken, id: userId} - // If unsuccessful (for example, if the user closed the oauth login popup), - // throws an error describing the reason - methods.login = function (options) { - var self = this; - - // Login handlers should really also check whatever field they look at in - // options, but we don't enforce it. - check(options, Object); - - var result = accounts._runLoginHandlers(self, options); - - return accounts._attemptLogin(self, "login", arguments, result); - }; - - methods.logout = function () { - var token = accounts._getLoginToken(this.connection.id); - accounts._setLoginToken(this.userId, this.connection, null); - if (token && this.userId) - accounts.destroyToken(this.userId, token); - accounts._successfulLogout(this.connection, this.userId); - this.setUserId(null); - }; - - // Delete all the current user's tokens and close all open connections logged - // in as this user. Returns a fresh new login token that this client can - // use. Tests can override the default connection close delay - // (stored in CONNECTION_CLOSE_DELAY_MS) by setting - // Accounts._connectionCloseDelayMsForTests. - // - // XXX COMPAT WITH 0.7.2 - // This single `logoutOtherClients` method has been replaced with two - // methods, one that you call to get a new token, and another that you - // call to remove all tokens except your own. The new design allows - // clients to know when other clients have actually been logged - // out. (The `logoutOtherClients` method guarantees the caller that - // the other clients will be logged out at some point, but makes no - // guarantees about when.) This method is left in for backwards - // compatibility, especially since application code might be calling - // this method directly. - // - // @returns {Object} Object with token and tokenExpires keys. - methods.logoutOtherClients = function () { - var self = this; - var user = accounts.users.findOne(self.userId, { - fields: { - "services.resume.loginTokens": true - } - }); - if (user) { - // Save the current tokens in the database to be deleted in - // CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the - // caller's browser time to find the fresh token in localStorage. We save - // the tokens in the database in case we crash before actually deleting - // them. - var tokens = user.services.resume.loginTokens; - var newToken = accounts._generateStampedLoginToken(); - var userId = self.userId; - accounts.users.update(userId, { - $set: { - "services.resume.loginTokensToDelete": tokens, - "services.resume.haveLoginTokensToDelete": true - }, - $push: { "services.resume.loginTokens": accounts._hashStampedToken(newToken) } - }); - const connectionCloseDelay = - accounts._connectionCloseDelayMsForTests - ? accounts._connectionCloseDelayMsForTests - : CONNECTION_CLOSE_DELAY_MS; - Meteor.setTimeout(function () { - // The observe on Meteor.users will take care of closing the connections - // associated with `tokens`. - accounts._deleteSavedTokensForUser(userId, tokens); - }, connectionCloseDelay); - // We do not set the login token on this connection, but instead the - // observe closes the connection and the client will reconnect with the - // new token. - return { - token: newToken.token, - tokenExpires: accounts._tokenExpiration(newToken.when) - }; - } else { - throw new Meteor.Error("You are not logged in."); - } - }; - - // Generates a new login token with the same expiration as the - // connection's current token and saves it to the database. Associates - // the connection with this new token and returns it. Throws an error - // if called on a connection that isn't logged in. - // - // @returns Object - // If successful, returns { token: , id: , - // tokenExpires: }. - methods.getNewToken = function () { - var self = this; - var user = accounts.users.findOne(self.userId, { - fields: { "services.resume.loginTokens": 1 } - }); - if (! self.userId || ! user) { - throw new Meteor.Error("You are not logged in."); - } - // Be careful not to generate a new token that has a later - // expiration than the curren token. Otherwise, a bad guy with a - // stolen token could use this method to stop his stolen token from - // ever expiring. - var currentHashedToken = accounts._getLoginToken(self.connection.id); - var currentStampedToken = _.find( - user.services.resume.loginTokens, - function (stampedToken) { - return stampedToken.hashedToken === currentHashedToken; - } - ); - if (! currentStampedToken) { // safety belt: this should never happen - throw new Meteor.Error("Invalid login token"); - } - var newStampedToken = accounts._generateStampedLoginToken(); - newStampedToken.when = currentStampedToken.when; - accounts._insertLoginToken(self.userId, newStampedToken); - return accounts._loginUser(self, self.userId, newStampedToken); - }; - - // Removes all tokens except the token associated with the current - // connection. Throws an error if the connection is not logged - // in. Returns nothing on success. - methods.removeOtherTokens = function () { - var self = this; - if (! self.userId) { - throw new Meteor.Error("You are not logged in."); - } - var currentToken = accounts._getLoginToken(self.connection.id); - accounts.users.update(self.userId, { - $pull: { - "services.resume.loginTokens": { hashedToken: { $ne: currentToken } } - } - }); - }; - - // Allow a one-time configuration for a login service. Modifications - // to this collection are also allowed in insecure mode. - methods.configureLoginService = function (options) { - check(options, Match.ObjectIncluding({service: String})); - // Don't let random users configure a service we haven't added yet (so - // that when we do later add it, it's set up with their configuration - // instead of ours). - // XXX if service configuration is oauth-specific then this code should - // be in accounts-oauth; if it's not then the registry should be - // in this package - if (!(accounts.oauth - && _.contains(accounts.oauth.serviceNames(), options.service))) { - throw new Meteor.Error(403, "Service unknown"); - } - - var ServiceConfiguration = - Package['service-configuration'].ServiceConfiguration; - if (ServiceConfiguration.configurations.findOne({service: options.service})) - throw new Meteor.Error(403, "Service " + options.service + " already configured"); - - if (_.has(options, "secret") && usingOAuthEncryption()) - options.secret = OAuthEncryption.seal(options.secret); - - ServiceConfiguration.configurations.insert(options); - }; - - accounts._server.methods(methods); -}; - -Ap._initAccountDataHooks = function () { - var accounts = this; - - accounts._server.onConnection(function (connection) { - accounts._accountData[connection.id] = { - connection: connection - }; - - connection.onClose(function () { - accounts._removeTokenFromConnection(connection.id); - delete accounts._accountData[connection.id]; - }); - }); -}; - -Ap._initServerPublications = function () { - var accounts = this; - - // Publish all login service configuration fields other than secret. - accounts._server.publish("meteor.loginServiceConfiguration", function () { - var ServiceConfiguration = - Package['service-configuration'].ServiceConfiguration; - return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); - }, {is_auto: true}); // not techincally autopublish, but stops the warning. - - // Publish the current user's record to the client. - accounts._server.publish(null, function () { - if (this.userId) { - return accounts.users.find({ - _id: this.userId - }, { - fields: { - profile: 1, - username: 1, - emails: 1 - } - }); - } else { - return null; - } - }, /*suppress autopublish warning*/{is_auto: true}); - - // Use Meteor.startup to give other packages a chance to call - // addAutopublishFields. - Package.autopublish && Meteor.startup(function () { - // ['profile', 'username'] -> {profile: 1, username: 1} - var toFieldSelector = function (fields) { - return _.object(_.map(fields, function (field) { - return [field, 1]; - })); - }; - - accounts._server.publish(null, function () { - if (this.userId) { - return accounts.users.find({ - _id: this.userId - }, { - fields: toFieldSelector(accounts._autopublishFields.loggedInUser) - }); - } else { - return null; - } - }, /*suppress autopublish warning*/{is_auto: true}); - - // XXX this publish is neither dedup-able nor is it optimized by our special - // treatment of queries on a specific _id. Therefore this will have O(n^2) - // run-time performance every time a user document is changed (eg someone - // logging in). If this is a problem, we can instead write a manual publish - // function which filters out fields based on 'this.userId'. - accounts._server.publish(null, function () { - var selector = this.userId ? { - _id: { $ne: this.userId } - } : {}; - - return accounts.users.find(selector, { - fields: toFieldSelector(accounts._autopublishFields.otherUsers) - }); - }, /*suppress autopublish warning*/{is_auto: true}); - }); -}; - -// Add to the list of fields or subfields to be automatically -// published if autopublish is on. Must be called from top-level -// code (ie, before Meteor.startup hooks run). -// -// @param opts {Object} with: -// - forLoggedInUser {Array} Array of fields published to the logged-in user -// - forOtherUsers {Array} Array of fields published to users that aren't logged in -Ap.addAutopublishFields = function (opts) { - this._autopublishFields.loggedInUser.push.apply( - this._autopublishFields.loggedInUser, opts.forLoggedInUser); - this._autopublishFields.otherUsers.push.apply( - this._autopublishFields.otherUsers, opts.forOtherUsers); -}; - -/// -/// ACCOUNT DATA -/// - -// HACK: This is used by 'meteor-accounts' to get the loginToken for a -// connection. Maybe there should be a public way to do that. -Ap._getAccountData = function (connectionId, field) { - var data = this._accountData[connectionId]; - return data && data[field]; -}; - -Ap._setAccountData = function (connectionId, field, value) { - var data = this._accountData[connectionId]; - - // safety belt. shouldn't happen. accountData is set in onConnection, - // we don't have a connectionId until it is set. - if (!data) - return; - - if (value === undefined) - delete data[field]; - else - data[field] = value; -}; - - -/// -/// RECONNECT TOKENS -/// -/// support reconnecting using a meteor login token - -Ap._hashLoginToken = function (loginToken) { - var hash = crypto.createHash('sha256'); - hash.update(loginToken); - return hash.digest('base64'); -}; - - -// {token, when} => {hashedToken, when} -Ap._hashStampedToken = function (stampedToken) { - return _.extend(_.omit(stampedToken, 'token'), { - hashedToken: this._hashLoginToken(stampedToken.token) - }); -}; - - -// Using $addToSet avoids getting an index error if another client -// logging in simultaneously has already inserted the new hashed -// token. -Ap._insertHashedLoginToken = function (userId, hashedToken, query) { - query = query ? _.clone(query) : {}; - query._id = userId; - this.users.update(query, { - $addToSet: { - "services.resume.loginTokens": hashedToken - } - }); -}; - - -// Exported for tests. -Ap._insertLoginToken = function (userId, stampedToken, query) { - this._insertHashedLoginToken( - userId, - this._hashStampedToken(stampedToken), - query - ); -}; - - -Ap._clearAllLoginTokens = function (userId) { - this.users.update(userId, { - $set: { - 'services.resume.loginTokens': [] - } - }); -}; - -// test hook -Ap._getUserObserve = function (connectionId) { - return this._userObservesForConnections[connectionId]; -}; - -// Clean up this connection's association with the token: that is, stop -// the observe that we started when we associated the connection with -// this token. -Ap._removeTokenFromConnection = function (connectionId) { - if (_.has(this._userObservesForConnections, connectionId)) { - var observe = this._userObservesForConnections[connectionId]; - if (typeof observe === 'number') { - // We're in the process of setting up an observe for this connection. We - // can't clean up that observe yet, but if we delete the placeholder for - // this connection, then the observe will get cleaned up as soon as it has - // been set up. - delete this._userObservesForConnections[connectionId]; - } else { - delete this._userObservesForConnections[connectionId]; - observe.stop(); - } - } -}; - -Ap._getLoginToken = function (connectionId) { - return this._getAccountData(connectionId, 'loginToken'); -}; - -// newToken is a hashed token. -Ap._setLoginToken = function (userId, connection, newToken) { - var self = this; - - self._removeTokenFromConnection(connection.id); - self._setAccountData(connection.id, 'loginToken', newToken); - - if (newToken) { - // Set up an observe for this token. If the token goes away, we need - // to close the connection. We defer the observe because there's - // no need for it to be on the critical path for login; we just need - // to ensure that the connection will get closed at some point if - // the token gets deleted. - // - // Initially, we set the observe for this connection to a number; this - // signifies to other code (which might run while we yield) that we are in - // the process of setting up an observe for this connection. Once the - // observe is ready to go, we replace the number with the real observe - // handle (unless the placeholder has been deleted or replaced by a - // different placehold number, signifying that the connection was closed - // already -- in this case we just clean up the observe that we started). - var myObserveNumber = ++self._nextUserObserveNumber; - self._userObservesForConnections[connection.id] = myObserveNumber; - Meteor.defer(function () { - // If something else happened on this connection in the meantime (it got - // closed, or another call to _setLoginToken happened), just do - // nothing. We don't need to start an observe for an old connection or old - // token. - if (self._userObservesForConnections[connection.id] !== myObserveNumber) { - return; - } - - var foundMatchingUser; - // Because we upgrade unhashed login tokens to hashed tokens at - // login time, sessions will only be logged in with a hashed - // token. Thus we only need to observe hashed tokens here. - var observe = self.users.find({ - _id: userId, - 'services.resume.loginTokens.hashedToken': newToken - }, { fields: { _id: 1 } }).observeChanges({ - added: function () { - foundMatchingUser = true; - }, - removed: function () { - connection.close(); - // The onClose callback for the connection takes care of - // cleaning up the observe handle and any other state we have - // lying around. - } - }); - - // If the user ran another login or logout command we were waiting for the - // defer or added to fire (ie, another call to _setLoginToken occurred), - // then we let the later one win (start an observe, etc) and just stop our - // observe now. - // - // Similarly, if the connection was already closed, then the onClose - // callback would have called _removeTokenFromConnection and there won't - // be an entry in _userObservesForConnections. We can stop the observe. - if (self._userObservesForConnections[connection.id] !== myObserveNumber) { - observe.stop(); - return; - } - - self._userObservesForConnections[connection.id] = observe; - - if (! foundMatchingUser) { - // We've set up an observe on the user associated with `newToken`, - // so if the new token is removed from the database, we'll close - // the connection. But the token might have already been deleted - // before we set up the observe, which wouldn't have closed the - // connection because the observe wasn't running yet. - connection.close(); - } - }); - } -}; - -function setupDefaultLoginHandlers(accounts) { +const setupDefaultLoginHandlers = accounts => { accounts.registerLoginHandler("resume", function (options) { return defaultResumeLoginHandler.call(this, accounts, options); }); -} +}; // Login handler for resume tokens. -function defaultResumeLoginHandler(accounts, options) { +const defaultResumeLoginHandler = (accounts, options) => { if (!options.resume) return undefined; check(options.resume, String); - var hashedToken = accounts._hashLoginToken(options.resume); + const hashedToken = accounts._hashLoginToken(options.resume); // First look for just the new-style hashed login token, to avoid // sending the unhashed token to the database in a query if we don't // need to. - var user = accounts.users.findOne( + let user = accounts.users.findOne( {"services.resume.loginTokens.hashedToken": hashedToken}); if (! user) { @@ -1019,20 +1346,20 @@ function defaultResumeLoginHandler(accounts, options) { // Find the token, which will either be an object with fields // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. - var oldUnhashedStyleToken; - var token = _.find(user.services.resume.loginTokens, function (token) { - return token.hashedToken === hashedToken; - }); + let oldUnhashedStyleToken; + let token = user.services.resume.loginTokens.find(token => + token.hashedToken === hashedToken + ); if (token) { oldUnhashedStyleToken = false; } else { - token = _.find(user.services.resume.loginTokens, function (token) { - return token.token === options.resume; - }); + token = user.services.resume.loginTokens.find(token => + token.token === options.resume + ); oldUnhashedStyleToken = true; } - var tokenExpires = accounts._tokenExpiration(token.when); + const tokenExpires = accounts._tokenExpiration(token.when); if (new Date() >= tokenExpires) return { userId: user._id, @@ -1052,11 +1379,11 @@ function defaultResumeLoginHandler(accounts, options) { "services.resume.loginTokens.token": options.resume }, {$addToSet: { - "services.resume.loginTokens": { - "hashedToken": hashedToken, - "when": token.when - } - }} + "services.resume.loginTokens": { + "hashedToken": hashedToken, + "when": token.when + } + }} ); // Remove the old token *after* adding the new, since otherwise @@ -1076,22 +1403,14 @@ function defaultResumeLoginHandler(accounts, options) { when: token.when } }; -} - -// (Also used by Meteor Accounts server and tests). -// -Ap._generateStampedLoginToken = function () { - return { - token: Random.secret(), - when: new Date - }; }; -/// -/// TOKEN EXPIRATION -/// - -function expirePasswordToken(accounts, oldestValidDate, tokenFilter, userId) { +const expirePasswordToken = ( + accounts, + oldestValidDate, + tokenFilter, + userId +) => { const userFilter = userId ? {_id: userId} : {}; const resetRangeOr = { $or: [ @@ -1106,138 +1425,27 @@ function expirePasswordToken(accounts, oldestValidDate, tokenFilter, userId) { "services.password.reset": "" } }, { multi: true }); -} - -// Deletes expired tokens from the database and closes all open connections -// associated with these tokens. -// -// Exported for tests. Also, the arguments are only used by -// tests. oldestValidDate is simulate expiring tokens without waiting -// for them to actually expire. userId is used by tests to only expire -// tokens for the test user. -Ap._expireTokens = function (oldestValidDate, userId) { - var tokenLifetimeMs = this._getTokenLifetimeMs(); - - // when calling from a test with extra arguments, you must specify both! - if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); - } - - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); - var userFilter = userId ? {_id: userId} : {}; - - - // Backwards compatible with older versions of meteor that stored login token - // timestamps as numbers. - this.users.update(_.extend(userFilter, { - $or: [ - { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, - { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } - ] - }), { - $pull: { - "services.resume.loginTokens": { - $or: [ - { when: { $lt: oldestValidDate } }, - { when: { $lt: +oldestValidDate } } - ] - } - } - }, { multi: true }); - // The observe on Meteor.users will take care of closing connections for - // expired tokens. }; -// Deletes expired password reset tokens from the database. -// -// Exported for tests. Also, the arguments are only used by -// tests. oldestValidDate is simulate expiring tokens without waiting -// for them to actually expire. userId is used by tests to only expire -// tokens for the test user. -Ap._expirePasswordResetTokens = function (oldestValidDate, userId) { - var tokenLifetimeMs = this._getPasswordResetTokenLifetimeMs(); - - // when calling from a test with extra arguments, you must specify both! - if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); - } - - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); - - var tokenFilter = { - $or: [ - { "services.password.reset.reason": "reset"}, - { "services.password.reset.reason": {$exists: false}} - ] - }; - - expirePasswordToken(this, oldestValidDate, tokenFilter, userId); -} - -// Deletes expired password enroll tokens from the database. -// -// Exported for tests. Also, the arguments are only used by -// tests. oldestValidDate is simulate expiring tokens without waiting -// for them to actually expire. userId is used by tests to only expire -// tokens for the test user. -Ap._expirePasswordEnrollTokens = function (oldestValidDate, userId) { - var tokenLifetimeMs = this._getPasswordEnrollTokenLifetimeMs(); - - // when calling from a test with extra arguments, you must specify both! - if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); - } - - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); - - var tokenFilter = { - "services.password.reset.reason": "enroll" - }; - - expirePasswordToken(this, oldestValidDate, tokenFilter, userId); -} - -// @override from accounts_common.js -Ap.config = function (options) { - // Call the overridden implementation of the method. - var superResult = AccountsCommon.prototype.config.apply(this, arguments); - - // If the user set loginExpirationInDays to null, then we need to clear the - // timer that periodically expires tokens. - if (_.has(this._options, "loginExpirationInDays") && - this._options.loginExpirationInDays === null && - this.expireTokenInterval) { - Meteor.clearInterval(this.expireTokenInterval); - this.expireTokenInterval = null; - } - - return superResult; -}; - -function setExpireTokensInterval(accounts) { - accounts.expireTokenInterval = Meteor.setInterval(function () { +const setExpireTokensInterval = accounts => { + accounts.expireTokenInterval = Meteor.setInterval(() => { accounts._expireTokens(); accounts._expirePasswordResetTokens(); accounts._expirePasswordEnrollTokens(); }, EXPIRE_TOKENS_INTERVAL_MS); -} - +}; /// /// OAuth Encryption Support /// -var OAuthEncryption = +const OAuthEncryption = Package["oauth-encryption"] && Package["oauth-encryption"].OAuthEncryption; -function usingOAuthEncryption() { +const usingOAuthEncryption = () => { return OAuthEncryption && OAuthEncryption.keyIsLoaded(); -} - +}; // OAuth service data is temporarily stored in the pending credentials // collection during the oauth authentication process. Sensitive data @@ -1246,14 +1454,14 @@ function usingOAuthEncryption() { // user id included when storing the service data permanently in // the users collection. // -function pinEncryptedFieldsToUser(serviceData, userId) { - _.each(_.keys(serviceData), function (key) { - var value = serviceData[key]; +const pinEncryptedFieldsToUser = (serviceData, userId) => { + Object.keys(serviceData).forEach(key => { + let value = serviceData[key]; if (OAuthEncryption && OAuthEncryption.isSealed(value)) value = OAuthEncryption.seal(OAuthEncryption.open(value), userId); serviceData[key] = value; }); -} +}; // Encrypt unencrypted login service secrets when oauth-encryption is @@ -1265,13 +1473,12 @@ function pinEncryptedFieldsToUser(serviceData, userId) { // block in the app code will run after this accounts-base startup // block. Perhaps we need a post-startup callback? -Meteor.startup(function () { +Meteor.startup(() => { if (! usingOAuthEncryption()) { return; } - var ServiceConfiguration = - Package['service-configuration'].ServiceConfiguration; + const { ServiceConfiguration } = Package['service-configuration']; ServiceConfiguration.configurations.find({ $and: [{ @@ -1279,7 +1486,7 @@ Meteor.startup(function () { }, { "secret.algorithm": { $exists: false } }] - }).forEach(function (config) { + }).forEach(config => { ServiceConfiguration.configurations.update(config._id, { $set: { secret: OAuthEncryption.seal(config.secret) @@ -1290,220 +1497,62 @@ Meteor.startup(function () { // XXX see comment on Accounts.createUser in passwords_server about adding a // second "server options" argument. -function defaultCreateUserHook(options, user) { +const defaultCreateUserHook = (options, user) => { if (options.profile) user.profile = options.profile; return user; -} - -// Called by accounts-password -Ap.insertUserDoc = function (options, user) { - // - clone user document, to protect from modification - // - add createdAt timestamp - // - prepare an _id, so that you can modify other collections (eg - // create a first task for every new user) - // - // XXX If the onCreateUser or validateNewUser hooks fail, we might - // end up having modified some other collection - // inappropriately. The solution is probably to have onCreateUser - // accept two callbacks - one that gets called before inserting - // the user document (in which you can modify its contents), and - // one that gets called after (in which you should change other - // collections) - user = _.extend({ - createdAt: new Date(), - _id: Random.id() - }, user); - - if (user.services) { - _.each(user.services, function (serviceData) { - pinEncryptedFieldsToUser(serviceData, user._id); - }); - } - - var fullUser; - if (this._onCreateUserHook) { - fullUser = this._onCreateUserHook(options, user); - - // This is *not* part of the API. We need this because we can't isolate - // the global server environment between tests, meaning we can't test - // both having a create user hook set and not having one set. - if (fullUser === 'TEST DEFAULT HOOK') - fullUser = defaultCreateUserHook(options, user); - } else { - fullUser = defaultCreateUserHook(options, user); - } - - _.each(this._validateNewUserHooks, function (hook) { - if (! hook(fullUser)) - throw new Meteor.Error(403, "User validation failed"); - }); - - var userId; - try { - userId = this.users.insert(fullUser); - } catch (e) { - // XXX string parsing sucks, maybe - // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day - if (e.name !== 'MongoError' && e.name !== 'BulkWriteError') throw e; - if (e.code !== 11000) throw e; - if (e.errmsg.indexOf('emails.address') !== -1) - throw new Meteor.Error(403, "Email already exists."); - if (e.errmsg.indexOf('username') !== -1) - throw new Meteor.Error(403, "Username already exists."); - // XXX better error reporting for services.facebook.id duplicate, etc - throw e; - } - return userId; -}; - -// Helper function: returns false if email does not match company domain from -// the configuration. -Ap._testEmailDomain = function (email) { - var domain = this._options.restrictCreationByEmailDomain; - return !domain || - (_.isFunction(domain) && domain(email)) || - (_.isString(domain) && - (new RegExp('@' + Meteor._escapeRegExp(domain) + '$', 'i')).test(email)); }; // Validate new user's email or Google/Facebook/GitHub account's email function defaultValidateNewUserHook(user) { - var self = this; - var domain = self._options.restrictCreationByEmailDomain; - if (!domain) + const domain = this._options.restrictCreationByEmailDomain; + if (!domain) { return true; - - var emailIsGood = false; - if (!_.isEmpty(user.emails)) { - emailIsGood = _.any(user.emails, function (email) { - return self._testEmailDomain(email.address); - }); - } else if (!_.isEmpty(user.services)) { - // Find any email of any service and check it - emailIsGood = _.any(user.services, function (service) { - return service.email && self._testEmailDomain(service.email); - }); } - if (emailIsGood) - return true; + let emailIsGood = false; + if (user.emails && user.emails.length > 0) { + emailIsGood = user.emails.reduce( + (prev, email) => prev || this._testEmailDomain(email.address), false + ); + } else if (user.services && user.services.length > 0) { + // Find any email of any service and check it + emailIsGood = user.services.reduce( + (prev, service) => service.email && this._testEmailDomain(service.email), + false, + ); + } - if (_.isString(domain)) - throw new Meteor.Error(403, "@" + domain + " email required"); - else + if (emailIsGood) { + return true; + } + + if (typeof domain === 'string') { + throw new Meteor.Error(403, `@${domain} email required`); + } else { throw new Meteor.Error(403, "Email doesn't match the criteria."); + } } -/// -/// MANAGING USER OBJECTS -/// - -// Updates or creates a user after we authenticate with a 3rd party. -// -// @param serviceName {String} Service name (eg, twitter). -// @param serviceData {Object} Data to store in the user's record -// under services[serviceName]. Must include an "id" field -// which is a unique identifier for the user in the service. -// @param options {Object, optional} Other options to pass to insertUserDoc -// (eg, profile) -// @returns {Object} Object with token and id keys, like the result -// of the "login" method. -// -Ap.updateOrCreateUserFromExternalService = function ( - serviceName, - serviceData, - options -) { - options = _.clone(options || {}); - - if (serviceName === "password" || serviceName === "resume") - throw new Error( - "Can't use updateOrCreateUserFromExternalService with internal service " - + serviceName); - if (!_.has(serviceData, 'id')) - throw new Error( - "Service data for service " + serviceName + " must include id"); - - // Look for a user with the appropriate service user id. - var selector = {}; - var serviceIdKey = "services." + serviceName + ".id"; - - // XXX Temporary special case for Twitter. (Issue #629) - // The serviceData.id will be a string representation of an integer. - // We want it to match either a stored string or int representation. - // This is to cater to earlier versions of Meteor storing twitter - // user IDs in number form, and recent versions storing them as strings. - // This can be removed once migration technology is in place, and twitter - // users stored with integer IDs have been migrated to string IDs. - if (serviceName === "twitter" && !isNaN(serviceData.id)) { - selector["$or"] = [{},{}]; - selector["$or"][0][serviceIdKey] = serviceData.id; - selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); - } else { - selector[serviceIdKey] = serviceData.id; - } - - var user = this.users.findOne(selector); - - // When creating a new user we pass through all options. When updating an - // existing user, by default we only process/pass through the serviceData - // (eg, so that we keep an unexpired access token and don't cache old email - // addresses in serviceData.email). The onExternalLogin hook can be used when - // creating or updating a user, to modify or pass through more options as - // needed. - var opts = user ? {} : options; - if (this._onExternalLoginHook) { - opts = this._onExternalLoginHook(options, user); - } - - if (user) { - pinEncryptedFieldsToUser(serviceData, user._id); - - var setAttrs = {}; - _.each(serviceData, function (value, key) { - setAttrs["services." + serviceName + "." + key] = value; - }); - - // XXX Maybe we should re-use the selector above and notice if the update - // touches nothing? - setAttrs = _.extend({}, setAttrs, opts); - this.users.update(user._id, { - $set: setAttrs - }); - - return { - type: serviceName, - userId: user._id - }; - } else { - // Create a new user with the service data. - user = {services: {}}; - user.services[serviceName] = serviceData; - return { - type: serviceName, - userId: this.insertUserDoc(opts, user) - }; - } -}; - -function setupUsersCollection(users) { +const setupUsersCollection = users => { /// /// RESTRICTING WRITES TO USER OBJECTS /// users.allow({ // clients can modify the profile field of their own document, and // nothing else. - update: function (userId, user, fields, modifier) { + update: (userId, user, fields, modifier) => { // make sure it is our record - if (user._id !== userId) + if (user._id !== userId) { return false; + } // user can only modify the 'profile' field. sets to multiple // sub-keys (eg profile.foo and profile.bar) are merged into entry // in the fields list. - if (fields.length !== 1 || fields[0] !== 'profile') + if (fields.length !== 1 || fields[0] !== 'profile') { return false; + } return true; }, @@ -1514,56 +1563,15 @@ function setupUsersCollection(users) { users._ensureIndex('username', {unique: 1, sparse: 1}); users._ensureIndex('emails.address', {unique: 1, sparse: 1}); users._ensureIndex('services.resume.loginTokens.hashedToken', - {unique: 1, sparse: 1}); + {unique: 1, sparse: 1}); users._ensureIndex('services.resume.loginTokens.token', - {unique: 1, sparse: 1}); + {unique: 1, sparse: 1}); // For taking care of logoutOtherClients calls that crashed before the // tokens were deleted. users._ensureIndex('services.resume.haveLoginTokensToDelete', - { sparse: 1 }); + { sparse: 1 }); // For expiring login tokens users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 }); // For expiring password tokens users._ensureIndex('services.password.reset.when', { sparse: 1 }); -} - -/// -/// CLEAN UP FOR `logoutOtherClients` -/// - -Ap._deleteSavedTokensForUser = function (userId, tokensToDelete) { - if (tokensToDelete) { - this.users.update(userId, { - $unset: { - "services.resume.haveLoginTokensToDelete": 1, - "services.resume.loginTokensToDelete": 1 - }, - $pullAll: { - "services.resume.loginTokens": tokensToDelete - } - }); - } -}; - -Ap._deleteSavedTokensForAllUsersOnStartup = function () { - var self = this; - - // If we find users who have saved tokens to delete on startup, delete - // them now. It's possible that the server could have crashed and come - // back up before new tokens are found in localStorage, but this - // shouldn't happen very often. We shouldn't put a delay here because - // that would give a lot of power to an attacker with a stolen login - // token and the ability to crash the server. - Meteor.startup(function () { - self.users.find({ - "services.resume.haveLoginTokensToDelete": true - }, { - "services.resume.loginTokensToDelete": 1 - }).forEach(function (user) { - self._deleteSavedTokensForUser( - user._id, - user.services.resume.loginTokensToDelete - ); - }); - }); }; diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 5eb77b4121..fb4165a96c 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -7,21 +7,20 @@ Meteor.methods({ // XXX it'd be cool to also test that the right thing happens if options // *are* validated, but Accounts._options is global state which makes this hard // (impossible?) -Tinytest.add('accounts - config validates keys', function (test) { - test.throws(function () { - Accounts.config({foo: "bar"}); - }); -}); +Tinytest.add( + 'accounts - config validates keys', + test => test.throws(() => Accounts.config({foo: "bar"})) +); -Tinytest.add('accounts - config - token lifetime', function (test) { - const loginExpirationInDays = Accounts._options.loginExpirationInDays; +Tinytest.add('accounts - config - token lifetime', test => { + const { loginExpirationInDays } = Accounts._options; Accounts._options.loginExpirationInDays = 2; test.equal(Accounts._getTokenLifetimeMs(), 2 * 24 * 60 * 60 * 1000); Accounts._options.loginExpirationInDays = loginExpirationInDays; }); -Tinytest.add('accounts - config - unexpiring tokens', function (test) { - const loginExpirationInDays = Accounts._options.loginExpirationInDays; +Tinytest.add('accounts - config - unexpiring tokens', test => { + const { loginExpirationInDays } = Accounts._options; // When setting loginExpirationInDays to null in the global Accounts // config object, make sure the returned token lifetime represents an @@ -48,7 +47,7 @@ Tinytest.add('accounts - config - unexpiring tokens', function (test) { Accounts._options.loginExpirationInDays = loginExpirationInDays; }); -Tinytest.add('accounts - config - default token lifetime', function (test) { +Tinytest.add('accounts - config - default token lifetime', test => { const options = Accounts._options; Accounts._options = {}; test.equal( @@ -58,55 +57,55 @@ Tinytest.add('accounts - config - default token lifetime', function (test) { Accounts._options = options; }); -var idsInValidateNewUser = {}; -Accounts.validateNewUser(function (user) { +const idsInValidateNewUser = {}; +Accounts.validateNewUser(user => { idsInValidateNewUser[user._id] = true; return true; }); -Tinytest.add('accounts - validateNewUser gets passed user with _id', function (test) { - var newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId; +Tinytest.add('accounts - validateNewUser gets passed user with _id', test => { + const newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId; test.isTrue(newUserId in idsInValidateNewUser); }); -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', function (test) { - var facebookId = Random.id(); +Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', test => { + const facebookId = Random.id(); // create an account with facebook - var uid1 = Accounts.updateOrCreateUserFromExternalService( + const uid1 = Accounts.updateOrCreateUserFromExternalService( 'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id; - var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); - test.length(users, 1); - test.equal(users[0].profile.foo, 1); - test.equal(users[0].services.facebook.monkey, 42); + const users1 = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); + test.length(users1, 1); + test.equal(users1[0].profile.foo, 1); + test.equal(users1[0].services.facebook.monkey, 42); // create again with the same id, see that we get the same user. // it should update services.facebook but not profile. - var uid2 = Accounts.updateOrCreateUserFromExternalService( + const uid2 = Accounts.updateOrCreateUserFromExternalService( 'facebook', {id: facebookId, llama: 50}, {profile: {foo: 1000, bar: 2}}).id; test.equal(uid1, uid2); - users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); - test.length(users, 1); - test.equal(users[0].profile.foo, 1); - test.equal(users[0].profile.bar, undefined); - test.equal(users[0].services.facebook.llama, 50); + const users2 = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); + test.length(users2, 1); + test.equal(users2[0].profile.foo, 1); + test.equal(users2[0].profile.bar, undefined); + test.equal(users2[0].services.facebook.llama, 50); // make sure we *don't* lose values not passed this call to // updateOrCreateUserFromExternalService - test.equal(users[0].services.facebook.monkey, 42); + test.equal(users2[0].services.facebook.monkey, 42); // cleanup Meteor.users.remove(uid1); }); -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', function (test) { - var weiboId1 = Random.id(); - var weiboId2 = Random.id(); +Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', test => { + const weiboId1 = Random.id(); + const weiboId2 = Random.id(); // users that have different service ids get different users - var uid1 = Accounts.updateOrCreateUserFromExternalService( + const uid1 = Accounts.updateOrCreateUserFromExternalService( 'weibo', {id: weiboId1}, {profile: {foo: 1}}).id; - var uid2 = Accounts.updateOrCreateUserFromExternalService( + const uid2 = Accounts.updateOrCreateUserFromExternalService( 'weibo', {id: weiboId2}, {profile: {bar: 2}}).id; test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).profile.foo, 1); @@ -119,75 +118,73 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', functio Meteor.users.remove(uid2); }); -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', function (test) { - var twitterIdOld = parseInt(Random.hexString(4), 16); - var twitterIdNew = ''+twitterIdOld; +Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', test => { + const twitterIdOld = parseInt(Random.hexString(4), 16); + const twitterIdNew = ''+twitterIdOld; // create an account with twitter using the old ID format of integer - var uid1 = Accounts.updateOrCreateUserFromExternalService( + const uid1 = Accounts.updateOrCreateUserFromExternalService( 'twitter', {id: twitterIdOld, monkey: 42}, {profile: {foo: 1}}).id; - var users = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch(); - test.length(users, 1); - test.equal(users[0].profile.foo, 1); - test.equal(users[0].services.twitter.monkey, 42); + const users1 = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch(); + test.length(users1, 1); + test.equal(users1[0].profile.foo, 1); + test.equal(users1[0].services.twitter.monkey, 42); // Update the account with the new ID format of string // test that the existing user is found, and that the ID // gets updated to a string value - var uid2 = Accounts.updateOrCreateUserFromExternalService( + const uid2 = Accounts.updateOrCreateUserFromExternalService( 'twitter', {id: twitterIdNew, monkey: 42}, {profile: {foo: 1}}).id; test.equal(uid1, uid2); - users = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch(); - test.length(users, 1); + const users2 = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch(); + test.length(users2, 1); // cleanup Meteor.users.remove(uid1); }); -Tinytest.add('accounts - insertUserDoc username', function (test) { - var userIn = { +Tinytest.add('accounts - insertUserDoc username', test => { + const userIn = { username: Random.id() }; // user does not already exist. create a user object with fields set. - var userId = Accounts.insertUserDoc( + const userId = Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); - var userOut = Meteor.users.findOne(userId); + const userOut = Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); test.equal(userOut.username, userIn.username); // run the hook again. now the user exists, so it throws an error. - test.throws(function () { - Accounts.insertUserDoc( - {profile: {name: 'Foo Bar'}}, - userIn - ); - }, 'Username already exists.'); + test.throws( + () => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn), + 'Username already exists.' + ); // cleanup Meteor.users.remove(userId); }); -Tinytest.add('accounts - insertUserDoc email', function (test) { - var email1 = Random.id(); - var email2 = Random.id(); - var email3 = Random.id(); - var userIn = { +Tinytest.add('accounts - insertUserDoc email', test => { + const email1 = Random.id(); + const email2 = Random.id(); + const email3 = Random.id(); + const userIn = { emails: [{address: email1, verified: false}, {address: email2, verified: true}] }; // user does not already exist. create a user object with fields set. - var userId = Accounts.insertUserDoc( + const userId = Accounts.insertUserDoc( {profile: {name: 'Foo Bar'}}, userIn ); - var userOut = Meteor.users.findOne(userId); + const userOut = Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); @@ -195,32 +192,28 @@ Tinytest.add('accounts - insertUserDoc email', function (test) { // run the hook again with the exact same emails. // run the hook again. now the user exists, so it throws an error. - test.throws(function () { - Accounts.insertUserDoc( - {profile: {name: 'Foo Bar'}}, - userIn - ); - }, 'Email already exists.'); + test.throws( + () => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn), + 'Email already exists.' + ); // now with only one of them. - test.throws(function () { - Accounts.insertUserDoc( - {}, {emails: [{address: email1}]} - ); - }, 'Email already exists.'); + test.throws(() => + Accounts.insertUserDoc({}, {emails: [{address: email1}]}), + 'Email already exists.' + ); - test.throws(function () { - Accounts.insertUserDoc( - {}, {emails: [{address: email2}]} - ); - }, 'Email already exists.'); + test.throws(() => + Accounts.insertUserDoc({}, {emails: [{address: email2}]}), + 'Email already exists.' + ); // a third email works. - var userId3 = Accounts.insertUserDoc( + const userId3 = Accounts.insertUserDoc( {}, {emails: [{address: email3}]} ); - var user3 = Meteor.users.findOne(userId3); + const user3 = Meteor.users.findOne(userId3); test.equal(typeof user3.createdAt, 'object'); // cleanup @@ -229,12 +222,12 @@ Tinytest.add('accounts - insertUserDoc email', function (test) { }); // More token expiration tests are in accounts-password -Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) { - var userIn = { username: Random.id() }; - var userId = Accounts.insertUserDoc({ profile: { +Tinytest.addAsync('accounts - expire numeric token', (test, onComplete) => { + const userIn = { username: Random.id() }; + const userId = Accounts.insertUserDoc({ profile: { name: 'Foo Bar' } }, userIn); - var date = new Date(new Date() - 5000); + const date = new Date(new Date() - 5000); Meteor.users.update(userId, { $set: { "services.resume.loginTokens": [{ @@ -246,10 +239,11 @@ Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) }] } }); - var observe = Meteor.users.find(userId).observe({ - changed: function (newUser) { + const observe = Meteor.users.find(userId).observe({ + changed: newUser => { if (newUser.services && newUser.services.resume && - _.isEmpty(newUser.services.resume.loginTokens)) { + (!newUser.services.resume.loginTokens || + newUser.services.resume.loginTokens.length === 0)) { observe.stop(); onComplete(); } @@ -261,45 +255,43 @@ Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) // Login tokens used to be stored unhashed in the database. We want // to make sure users can still login after upgrading. -var insertUnhashedLoginToken = function (userId, stampedToken) { +const insertUnhashedLoginToken = (userId, stampedToken) => { Meteor.users.update( userId, {$push: {'services.resume.loginTokens': stampedToken}} ); }; -Tinytest.addAsync('accounts - login token', function (test, onComplete) { +Tinytest.addAsync('accounts - login token', (test, onComplete) => { // Test that we can login when the database contains a leftover // old style unhashed login token. - var userId1 = Accounts.insertUserDoc({}, {username: Random.id()}); - var stampedToken = Accounts._generateStampedLoginToken(); - insertUnhashedLoginToken(userId1, stampedToken); - var connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken.token}); + const userId1 = Accounts.insertUserDoc({}, {username: Random.id()}); + const stampedToken1 = Accounts._generateStampedLoginToken(); + insertUnhashedLoginToken(userId1, stampedToken1); + let connection = DDP.connect(Meteor.absoluteUrl()); + connection.call('login', {resume: stampedToken1.token}); connection.disconnect(); // Steal the unhashed token from the database and use it to login. // This is a sanity check so that when we *can't* login with a // stolen *hashed* token, we know it's not a problem with the test. - var userId2 = Accounts.insertUserDoc({}, {username: Random.id()}); + const userId2 = Accounts.insertUserDoc({}, {username: Random.id()}); insertUnhashedLoginToken(userId2, Accounts._generateStampedLoginToken()); - var stolenToken = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token; - test.isTrue(stolenToken); + const stolenToken1 = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token; + test.isTrue(stolenToken1); connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stolenToken}); + connection.call('login', {resume: stolenToken1}); connection.disconnect(); // Now do the same thing, this time with a stolen hashed token. - var userId3 = Accounts.insertUserDoc({}, {username: Random.id()}); + const userId3 = Accounts.insertUserDoc({}, {username: Random.id()}); Accounts._insertLoginToken(userId3, Accounts._generateStampedLoginToken()); - stolenToken = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken; - test.isTrue(stolenToken); + const stolenToken2 = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken; + test.isTrue(stolenToken2); connection = DDP.connect(Meteor.absoluteUrl()); // evil plan foiled test.throws( - function () { - connection.call('login', {resume: stolenToken}); - }, + () => connection.call('login', {resume: stolenToken2}), /You\'ve been logged out by the server/ ); connection.disconnect(); @@ -307,21 +299,21 @@ Tinytest.addAsync('accounts - login token', function (test, onComplete) { // Old style unhashed tokens are replaced by hashed tokens when // encountered. This means that after someone logins once, the // old unhashed token is no longer available to be stolen. - var userId4 = Accounts.insertUserDoc({}, {username: Random.id()}); - var stampedToken = Accounts._generateStampedLoginToken(); - insertUnhashedLoginToken(userId4, stampedToken); + const userId4 = Accounts.insertUserDoc({}, {username: Random.id()}); + const stampedToken2 = Accounts._generateStampedLoginToken(); + insertUnhashedLoginToken(userId4, stampedToken2); connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken.token}); + connection.call('login', {resume: stampedToken2.token}); connection.disconnect(); // The token is no longer available to be stolen. - stolenToken = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token; - test.isFalse(stolenToken); + const stolenToken3 = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token; + test.isFalse(stolenToken3); // After the upgrade, the client can still login with their original // unhashed login token. connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken.token}); + connection.call('login', {resume: stampedToken2.token}); connection.disconnect(); onComplete(); @@ -329,13 +321,13 @@ Tinytest.addAsync('accounts - login token', function (test, onComplete) { Tinytest.addAsync( 'accounts - connection data cleaned up', - function (test, onComplete) { + (test, onComplete) => { makeTestConnection( test, - function (clientConn, serverConn) { + (clientConn, serverConn) => { // onClose callbacks are called in order, so we run after the // close callback in accounts. - serverConn.onClose(function () { + serverConn.onClose(() => { test.isFalse(Accounts._getAccountData(serverConn.id, 'connection')); onComplete(); }); @@ -348,20 +340,18 @@ Tinytest.addAsync( } ); -Tinytest.add( - 'accounts - get new token', - function (test) { +Tinytest.add('accounts - get new token', test => { // Test that the `getNewToken` method returns us a valid token, with // the same expiration as our original token. - var userId = Accounts.insertUserDoc({}, { username: Random.id() }); - var stampedToken = Accounts._generateStampedLoginToken(); + const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + const stampedToken = Accounts._generateStampedLoginToken(); Accounts._insertLoginToken(userId, stampedToken); - var conn = DDP.connect(Meteor.absoluteUrl()); + const conn = DDP.connect(Meteor.absoluteUrl()); conn.call('login', { resume: stampedToken.token }); test.equal(conn.call('getCurrentLoginToken'), Accounts._hashLoginToken(stampedToken.token)); - var newTokenResult = conn.call('getNewToken'); + const newTokenResult = conn.call('getNewToken'); test.equal(newTokenResult.tokenExpires, Accounts._tokenExpiration(stampedToken.when)); test.equal(conn.call('getCurrentLoginToken'), @@ -370,48 +360,41 @@ Tinytest.add( // A second connection should be able to log in with the new token // we got. - var secondConn = DDP.connect(Meteor.absoluteUrl()); + const secondConn = DDP.connect(Meteor.absoluteUrl()); secondConn.call('login', { resume: newTokenResult.token }); secondConn.disconnect(); } ); -Tinytest.addAsync( - 'accounts - remove other tokens', - function (test, onComplete) { +Tinytest.addAsync('accounts - remove other tokens', (test, onComplete) => { // Test that the `removeOtherTokens` method removes all tokens other // than the caller's token, thereby logging out and closing other // connections. - var userId = Accounts.insertUserDoc({}, { username: Random.id() }); - var stampedTokens = []; - var conns = []; + const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + const stampedTokens = []; + const conns = []; - _.times(2, function (i) { + for(let i = 0; i < 2; i++) { stampedTokens.push(Accounts._generateStampedLoginToken()); Accounts._insertLoginToken(userId, stampedTokens[i]); - var conn = DDP.connect(Meteor.absoluteUrl()); + const conn = DDP.connect(Meteor.absoluteUrl()); conn.call('login', { resume: stampedTokens[i].token }); test.equal(conn.call('getCurrentLoginToken'), Accounts._hashLoginToken(stampedTokens[i].token)); conns.push(conn); - }); + }; conns[0].call('removeOtherTokens'); - simplePoll( - function () { - var tokens = _.map(conns, function (conn) { - return conn.call('getCurrentLoginToken'); - }); + simplePoll(() => { + const tokens = conns.map(conn => conn.call('getCurrentLoginToken')); return ! tokens[1] && tokens[0] === Accounts._hashLoginToken(stampedTokens[0].token); }, - function () { // success - _.each(conns, function (conn) { - conn.disconnect(); - }); + () => { // success + conns.forEach(conn => conn.disconnect()); onComplete(); }, - function () { // timed out + () => { // timed out throw new Error("accounts - remove other tokens timed out"); } ); @@ -420,31 +403,31 @@ Tinytest.addAsync( Tinytest.add( 'accounts - hook callbacks can access Meteor.userId()', - function (test) { - var userId = Accounts.insertUserDoc({}, { username: Random.id() }); - var stampedToken = Accounts._generateStampedLoginToken(); + test => { + const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + const stampedToken = Accounts._generateStampedLoginToken(); Accounts._insertLoginToken(userId, stampedToken); - var validateStopper = Accounts.validateLoginAttempt(function(attempt) { + const validateStopper = Accounts.validateLoginAttempt(attempt => { test.equal(Meteor.userId(), validateAttemptExpectedUserId, "validateLoginAttempt"); return true; }); - var onLoginStopper = Accounts.onLogin(function(attempt) { - test.equal(Meteor.userId(), onLoginExpectedUserId, "onLogin"); - }); - var onLogoutStopper = Accounts.onLogout(function(logoutContext) { + const onLoginStopper = Accounts.onLogin(attempt => + test.equal(Meteor.userId(), onLoginExpectedUserId, "onLogin") + ); + const onLogoutStopper = Accounts.onLogout(logoutContext => { test.equal(logoutContext.user._id, onLogoutExpectedUserId, "onLogout"); test.instanceOf(logoutContext.connection, Object); }); - var onLoginFailureStopper = Accounts.onLoginFailure(function(attempt) { - test.equal(Meteor.userId(), onLoginFailureExpectedUserId, "onLoginFailure"); - }); + const onLoginFailureStopper = Accounts.onLoginFailure(attempt => + test.equal(Meteor.userId(), onLoginFailureExpectedUserId, "onLoginFailure") + ); - var conn = DDP.connect(Meteor.absoluteUrl()); + const conn = DDP.connect(Meteor.absoluteUrl()); // On a new connection, Meteor.userId() should be null until logged in. - var validateAttemptExpectedUserId = null; - var onLoginExpectedUserId = userId; + let validateAttemptExpectedUserId = null; + const onLoginExpectedUserId = userId; conn.call('login', { resume: stampedToken.token }); // Now that the user is logged in on the connection, Meteor.userId() should @@ -453,11 +436,11 @@ Tinytest.add( conn.call('login', { resume: stampedToken.token }); // Trigger onLoginFailure callbacks - var onLoginFailureExpectedUserId = userId; - test.throws(function() { conn.call('login', { resume: "bogus" }) }, '403'); + const onLoginFailureExpectedUserId = userId; + test.throws(() => conn.call('login', { resume: "bogus" }), '403'); // Trigger onLogout callbacks - var onLogoutExpectedUserId = userId; + const onLogoutExpectedUserId = userId; conn.call('logout'); conn.disconnect(); @@ -470,7 +453,7 @@ Tinytest.add( Tinytest.add( 'accounts - verify onExternalLogin hook can update oauth user profiles', - function (test) { + test => { // Verify user profile data is saved properly when not using the // onExternalLogin hook. let facebookId = Random.id(); diff --git a/packages/accounts-base/accounts_url_tests.js b/packages/accounts-base/accounts_url_tests.js index 3e9b4fd43c..cab232cd17 100644 --- a/packages/accounts-base/accounts_url_tests.js +++ b/packages/accounts-base/accounts_url_tests.js @@ -1,17 +1,16 @@ -import {AccountsTest} from "meteor/accounts-base"; +import { AccountsTest } from "./accounts_client.js"; -Tinytest.add("accounts - parse urls for accounts-password", - function (test) { - var actions = ["reset-password", "verify-email", "enroll-account"]; +Tinytest.add("accounts - parse urls for accounts-password", test => { + const actions = ["reset-password", "verify-email", "enroll-account"]; // make sure the callback was called the right number of times - var actionsParsed = []; + const actionsParsed = []; - _.each(actions, function (hashPart) { - var fakeToken = "asdf"; + actions.forEach(hashPart => { + const fakeToken = "asdf"; - var hashTokenOnly = "#/" + hashPart + "/" + fakeToken; - AccountsTest.attemptToMatchHash(hashTokenOnly, function (token, action) { + const hashTokenOnly = `#/${hashPart}/${fakeToken}`; + AccountsTest.attemptToMatchHash(hashTokenOnly, (token, action) => { test.equal(token, fakeToken); test.equal(action, hashPart); diff --git a/packages/accounts-base/client_main.js b/packages/accounts-base/client_main.js index 8070357424..4fa98ef532 100644 --- a/packages/accounts-base/client_main.js +++ b/packages/accounts-base/client_main.js @@ -1,6 +1,4 @@ -import {AccountsClient} from "./accounts_client.js"; -import {AccountsTest} from "./url_client.js"; -import "./localstorage_token.js"; +import { AccountsClient, AccountsTest } from "./accounts_client.js"; /** * @namespace Accounts @@ -16,11 +14,14 @@ Accounts = new AccountsClient(); */ Meteor.users = Accounts.users; -export { +const exp = { AccountsClient }; + +if (Meteor.isPackageTest) { // Since this file is the main module for the client version of the // accounts-base package, properties of non-entry-point modules need to // be re-exported in order to be accessible to modules that import the // accounts-base package. - AccountsClient, - AccountsTest, -}; + exp.AccountsTest = AccountsTest; +} + +export default exp; diff --git a/packages/accounts-base/localstorage_token.js b/packages/accounts-base/localstorage_token.js deleted file mode 100644 index 2b78b0ad56..0000000000 --- a/packages/accounts-base/localstorage_token.js +++ /dev/null @@ -1,185 +0,0 @@ -import {AccountsClient} from "./accounts_client.js"; -var Ap = AccountsClient.prototype; - -// This file deals with storing a login token and user id in the -// browser's localStorage facility. It polls local storage every few -// seconds to synchronize login state between multiple tabs in the same -// browser. - -// Login with a Meteor access token. This is the only public function -// here. -Meteor.loginWithToken = function (token, callback) { - return Accounts.loginWithToken(token, callback); -}; - -Ap.loginWithToken = function (token, callback) { - this.callLoginMethod({ - methodArguments: [{ - resume: token - }], - userCallback: callback - }); -}; - -// Semi-internal API. Call this function to re-enable auto login after -// if it was disabled at startup. -Ap._enableAutoLogin = function () { - this._autoLoginEnabled = true; - this._pollStoredLoginToken(); -}; - - -/// -/// STORING -/// - -// Call this from the top level of the test file for any test that does -// logging in and out, to protect multiple tabs running the same tests -// simultaneously from interfering with each others' localStorage. -Ap._isolateLoginTokenForTest = function () { - this.LOGIN_TOKEN_KEY = this.LOGIN_TOKEN_KEY + Random.id(); - this.USER_ID_KEY = this.USER_ID_KEY + Random.id(); -}; - -Ap._storeLoginToken = function (userId, token, tokenExpires) { - Meteor._localStorage.setItem(this.USER_ID_KEY, userId); - Meteor._localStorage.setItem(this.LOGIN_TOKEN_KEY, token); - if (! tokenExpires) - tokenExpires = this._tokenExpiration(new Date()); - Meteor._localStorage.setItem(this.LOGIN_TOKEN_EXPIRES_KEY, tokenExpires); - - // to ensure that the localstorage poller doesn't end up trying to - // connect a second time - this._lastLoginTokenWhenPolled = token; -}; - -Ap._unstoreLoginToken = function () { - Meteor._localStorage.removeItem(this.USER_ID_KEY); - Meteor._localStorage.removeItem(this.LOGIN_TOKEN_KEY); - Meteor._localStorage.removeItem(this.LOGIN_TOKEN_EXPIRES_KEY); - - // to ensure that the localstorage poller doesn't end up trying to - // connect a second time - this._lastLoginTokenWhenPolled = null; -}; - -// This is private, but it is exported for now because it is used by a -// test in accounts-password. -// -Ap._storedLoginToken = function () { - return Meteor._localStorage.getItem(this.LOGIN_TOKEN_KEY); -}; - -Ap._storedLoginTokenExpires = function () { - return Meteor._localStorage.getItem(this.LOGIN_TOKEN_EXPIRES_KEY); -}; - -Ap._storedUserId = function () { - return Meteor._localStorage.getItem(this.USER_ID_KEY); -}; - -Ap._unstoreLoginTokenIfExpiresSoon = function () { - var tokenExpires = this._storedLoginTokenExpires(); - if (tokenExpires && this._tokenExpiresSoon(new Date(tokenExpires))) { - this._unstoreLoginToken(); - } -}; - -/// -/// AUTO-LOGIN -/// - -Ap._initLocalStorage = function () { - var self = this; - - // Key names to use in localStorage - self.LOGIN_TOKEN_KEY = "Meteor.loginToken"; - self.LOGIN_TOKEN_EXPIRES_KEY = "Meteor.loginTokenExpires"; - self.USER_ID_KEY = "Meteor.userId"; - - var rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; - if (rootUrlPathPrefix || this.connection !== Meteor.connection) { - // We want to keep using the same keys for existing apps that do not - // set a custom ROOT_URL_PATH_PREFIX, so that most users will not have - // to log in again after an app updates to a version of Meteor that - // contains this code, but it's generally preferable to namespace the - // keys so that connections from distinct apps to distinct DDP URLs - // will be distinct in Meteor._localStorage. - var namespace = ":" + this.connection._stream.rawUrl; - if (rootUrlPathPrefix) { - namespace += ":" + rootUrlPathPrefix; - } - self.LOGIN_TOKEN_KEY += namespace; - self.LOGIN_TOKEN_EXPIRES_KEY += namespace; - self.USER_ID_KEY += namespace; - } - - if (self._autoLoginEnabled) { - // Immediately try to log in via local storage, so that any DDP - // messages are sent after we have established our user account - self._unstoreLoginTokenIfExpiresSoon(); - var token = self._storedLoginToken(); - if (token) { - // On startup, optimistically present us as logged in while the - // request is in flight. This reduces page flicker on startup. - var userId = self._storedUserId(); - userId && self.connection.setUserId(userId); - self.loginWithToken(token, function (err) { - if (err) { - Meteor._debug("Error logging in with token", err); - self.makeClientLoggedOut(); - } - - self._pageLoadLogin({ - type: "resume", - allowed: !err, - error: err, - methodName: "login", - // XXX This is duplicate code with loginWithToken, but - // loginWithToken can also be called at other times besides - // page load. - methodArguments: [{resume: token}] - }); - }); - } - } - - // Poll local storage every 3 seconds to login if someone logged in in - // another tab - self._lastLoginTokenWhenPolled = token; - - if (self._pollIntervalTimer) { - // Unlikely that _initLocalStorage will be called more than once for - // the same AccountsClient instance, but just in case... - clearInterval(self._pollIntervalTimer); - } - - self._pollIntervalTimer = setInterval(function () { - self._pollStoredLoginToken(); - }, 3000); -}; - -Ap._pollStoredLoginToken = function () { - var self = this; - - if (! self._autoLoginEnabled) { - return; - } - - var currentLoginToken = self._storedLoginToken(); - - // != instead of !== just to make sure undefined and null are treated the same - if (self._lastLoginTokenWhenPolled != currentLoginToken) { - if (currentLoginToken) { - self.loginWithToken(currentLoginToken, function (err) { - if (err) { - self.makeClientLoggedOut(); - } - }); - } else { - self.logout(); - } - } - - self._lastLoginTokenWhenPolled = currentLoginToken; -}; diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 3942fbd469..687cb8154c 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,10 +1,9 @@ Package.describe({ summary: "A user account system", - version: "1.4.2" + version: "1.4.3", }); -Package.onUse(function (api) { - api.use('underscore', ['client', 'server']); +Package.onUse(api => { api.use('ecmascript', ['client', 'server']); api.use('ddp-rate-limiter'); api.use('localstorage', 'client'); @@ -50,7 +49,7 @@ Package.onUse(function (api) { api.mainModule('client_main.js', 'client'); }); -Package.onTest(function (api) { +Package.onTest(api => { api.use([ 'accounts-base', 'ecmascript', @@ -58,7 +57,6 @@ Package.onTest(function (api) { 'random', 'test-helpers', 'oauth-encryption', - 'underscore', 'ddp', 'accounts-password' ]); diff --git a/packages/accounts-base/server_main.js b/packages/accounts-base/server_main.js index cf2063017d..bfb12ef956 100644 --- a/packages/accounts-base/server_main.js +++ b/packages/accounts-base/server_main.js @@ -1,6 +1,4 @@ import {AccountsServer} from "./accounts_server.js"; -import "./accounts_rate_limit.js"; -import "./url_server.js"; /** * @namespace Accounts diff --git a/packages/accounts-base/url_client.js b/packages/accounts-base/url_client.js deleted file mode 100644 index 4bd55d53a4..0000000000 --- a/packages/accounts-base/url_client.js +++ /dev/null @@ -1,169 +0,0 @@ -import {AccountsClient} from "./accounts_client.js"; - -var Ap = AccountsClient.prototype; - -// All of the special hash URLs we support for accounts interactions -var accountsPaths = ["reset-password", "verify-email", "enroll-account"]; - -var savedHash = window.location.hash; - -Ap._initUrlMatching = function () { - // By default, allow the autologin process to happen. - this._autoLoginEnabled = true; - - // We only support one callback per URL. - this._accountsCallbacks = {}; - - // Try to match the saved value of window.location.hash. - this._attemptToMatchHash(); -}; - -// Separate out this functionality for testing - -Ap._attemptToMatchHash = function () { - attemptToMatchHash(this, savedHash, defaultSuccessHandler); -}; - -// Note that both arguments are optional and are currently only passed by -// accounts_url_tests.js. -function attemptToMatchHash(accounts, hash, success) { - _.each(accountsPaths, function (urlPart) { - var token; - - var tokenRegex = new RegExp("^\\#\\/" + urlPart + "\\/(.*)$"); - var match = hash.match(tokenRegex); - - if (match) { - token = match[1]; - - // XXX COMPAT WITH 0.9.3 - if (urlPart === "reset-password") { - accounts._resetPasswordToken = token; - } else if (urlPart === "verify-email") { - accounts._verifyEmailToken = token; - } else if (urlPart === "enroll-account") { - accounts._enrollAccountToken = token; - } - } else { - return; - } - - // If no handlers match the hash, then maybe it's meant to be consumed - // by some entirely different code, so we only clear it the first time - // a handler successfully matches. Note that later handlers reuse the - // savedHash, so clearing window.location.hash here will not interfere - // with their needs. - window.location.hash = ""; - - // Do some stuff with the token we matched - success.call(accounts, token, urlPart); - }); -} - -function defaultSuccessHandler(token, urlPart) { - var self = this; - - // put login in a suspended state to wait for the interaction to finish - self._autoLoginEnabled = false; - - // wait for other packages to register callbacks - Meteor.startup(function () { - // if a callback has been registered for this kind of token, call it - if (self._accountsCallbacks[urlPart]) { - self._accountsCallbacks[urlPart](token, function () { - self._enableAutoLogin(); - }); - } - }); -} - -// Export for testing -export var AccountsTest = { - attemptToMatchHash: function (hash, success) { - return attemptToMatchHash(Accounts, hash, success); - } -}; - -// XXX these should be moved to accounts-password eventually. Right now -// this is prevented by the need to set autoLoginEnabled=false, but in -// some bright future we won't need to do that anymore. - -/** - * @summary Register a function to call when a reset password link is clicked - * in an email sent by - * [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail). - * This function should be called in top-level code, not inside - * `Meteor.startup()`. - * @memberof! Accounts - * @name onResetPasswordLink - * @param {Function} callback The function to call. It is given two arguments: - * - * 1. `token`: A password reset token that can be passed to - * [`Accounts.resetPassword`](#accounts_resetpassword). - * 2. `done`: A function to call when the password reset UI flow is complete. The normal - * login process is suspended until this function is called, so that the - * password for user A can be reset even if user B was logged in. - * @locus Client - */ -Ap.onResetPasswordLink = function (callback) { - if (this._accountsCallbacks["reset-password"]) { - Meteor._debug("Accounts.onResetPasswordLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["reset-password"] = callback; -}; - -/** - * @summary Register a function to call when an email verification link is - * clicked in an email sent by - * [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail). - * This function should be called in top-level code, not inside - * `Meteor.startup()`. - * @memberof! Accounts - * @name onEmailVerificationLink - * @param {Function} callback The function to call. It is given two arguments: - * - * 1. `token`: An email verification token that can be passed to - * [`Accounts.verifyEmail`](#accounts_verifyemail). - * 2. `done`: A function to call when the email verification UI flow is complete. - * The normal login process is suspended until this function is called, so - * that the user can be notified that they are verifying their email before - * being logged in. - * @locus Client - */ -Ap.onEmailVerificationLink = function (callback) { - if (this._accountsCallbacks["verify-email"]) { - Meteor._debug("Accounts.onEmailVerificationLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["verify-email"] = callback; -}; - -/** - * @summary Register a function to call when an account enrollment link is - * clicked in an email sent by - * [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). - * This function should be called in top-level code, not inside - * `Meteor.startup()`. - * @memberof! Accounts - * @name onEnrollmentLink - * @param {Function} callback The function to call. It is given two arguments: - * - * 1. `token`: A password reset token that can be passed to - * [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly - * enrolled account a password. - * 2. `done`: A function to call when the enrollment UI flow is complete. - * The normal login process is suspended until this function is called, so that - * user A can be enrolled even if user B was logged in. - * @locus Client - */ -Ap.onEnrollmentLink = function (callback) { - if (this._accountsCallbacks["enroll-account"]) { - Meteor._debug("Accounts.onEnrollmentLink was called more than once. " + - "Only one callback added will be executed."); - } - - this._accountsCallbacks["enroll-account"] = callback; -}; diff --git a/packages/accounts-base/url_server.js b/packages/accounts-base/url_server.js deleted file mode 100644 index 9725e0b82a..0000000000 --- a/packages/accounts-base/url_server.js +++ /dev/null @@ -1,17 +0,0 @@ -import {AccountsServer} from "./accounts_server.js"; - -// XXX These should probably not actually be public? - -AccountsServer.prototype.urls = { - resetPassword: function (token) { - return Meteor.absoluteUrl('#/reset-password/' + token); - }, - - verifyEmail: function (token) { - return Meteor.absoluteUrl('#/verify-email/' + token); - }, - - enrollAccount: function (token) { - return Meteor.absoluteUrl('#/enroll-account/' + token); - } -}; diff --git a/packages/accounts-facebook/facebook.js b/packages/accounts-facebook/facebook.js index 4179610e4b..e48b58a88b 100644 --- a/packages/accounts-facebook/facebook.js +++ b/packages/accounts-facebook/facebook.js @@ -1,20 +1,19 @@ Accounts.oauth.registerService('facebook'); if (Meteor.isClient) { - const loginWithFacebook = function(options, callback) { + const loginWithFacebook = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Facebook.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('facebook', loginWithFacebook); - Meteor.loginWithFacebook = function () { - return Accounts.applyLoginFunction('facebook', arguments); - }; + Meteor.loginWithFacebook = + (...args) => Accounts.applyLoginFunction('facebook', args); } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately diff --git a/packages/accounts-facebook/notice.js b/packages/accounts-facebook/notice.js index 4be0d87c7b..5e58a88423 100644 --- a/packages/accounts-facebook/notice.js +++ b/packages/accounts-facebook/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('facebook-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'facebook-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-facebook,\n" + "but didn't install the configuration UI for the Facebook\n" + diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index 6682ed36db..6240e43bf7 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -1,9 +1,9 @@ Package.describe({ summary: "Login service for Facebook accounts", - version: "1.3.1" + version: "1.3.2", }); -Package.onUse(function(api) { +Package.onUse(api => { api.use('ecmascript'); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. diff --git a/packages/accounts-github/github.js b/packages/accounts-github/github.js index b2478518e5..b15fc0a9bc 100644 --- a/packages/accounts-github/github.js +++ b/packages/accounts-github/github.js @@ -1,20 +1,19 @@ Accounts.oauth.registerService('github'); if (Meteor.isClient) { - const loginWithGithub = function(options, callback) { + const loginWithGithub = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Github.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('github', loginWithGithub); - Meteor.loginWithGithub = function () { - return Accounts.applyLoginFunction('github', arguments); - }; + Meteor.loginWithGithub = + (...args) => Accounts.applyLoginFunction('github', args); } else { Accounts.addAutopublishFields({ // not sure whether the github api can be used from the browser, diff --git a/packages/accounts-github/notice.js b/packages/accounts-github/notice.js index 72be765556..533d504e91 100644 --- a/packages/accounts-github/notice.js +++ b/packages/accounts-github/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('github-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'github-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-github,\n" + "but didn't install the configuration UI for the GitHub\n" + diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js index 61a4c370f2..d2a25333a5 100644 --- a/packages/accounts-github/package.js +++ b/packages/accounts-github/package.js @@ -1,9 +1,9 @@ Package.describe({ summary: 'Login service for Github accounts', - version: '1.4.1' + version: '1.4.2', }); -Package.onUse(function (api) { +Package.onUse(api => { api.use('ecmascript'); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. diff --git a/packages/accounts-google/google.js b/packages/accounts-google/google.js index 306ef7882c..7f2b0b0c51 100644 --- a/packages/accounts-google/google.js +++ b/packages/accounts-google/google.js @@ -1,7 +1,7 @@ Accounts.oauth.registerService('google'); if (Meteor.isClient) { - const loginWithGoogle = function(options, callback) { + const loginWithGoogle = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; @@ -23,31 +23,34 @@ if (Meteor.isClient) { // accounts-base/accounts_server.js still checks server-side that the server // has the proper email address after the OAuth conversation. if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') { - options = _.extend({}, options || {}); - options.loginUrlParameters = _.extend({}, options.loginUrlParameters || {}); + options = { ...options }; + options.loginUrlParameters = { ...options.loginUrlParameters }; options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain; } - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Google.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('google', loginWithGoogle); - Meteor.loginWithGoogle = function () { - return Accounts.applyLoginFunction('google', arguments); - }; + Meteor.loginWithGoogle = + (...args) => Accounts.applyLoginFunction('google', args); } else { Accounts.addAutopublishFields({ - forLoggedInUser: _.map( + forLoggedInUser: // publish access token since it can be used from the client (if // transmitted over ssl or on // localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent // refresh token probably shouldn't be sent down. - Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token - function (subfield) { return 'services.google.' + subfield; }), + Google.whitelistedFields.concat(['accessToken', 'expiresAt']).map( + subfield => `services.google.${subfield}` // don't publish refresh token + ), - forOtherUsers: _.map( + forOtherUsers: // even with autopublish, no legitimate web app should be // publishing all users' emails - _.without(Google.whitelistedFields, 'email', 'verified_email'), - function (subfield) { return 'services.google.' + subfield; }) + Google.whitelistedFields.filter( + field => field !== 'email' && field !== 'verified_email' + ).map( + subfield => `services.google${subfield}` + ), }); } diff --git a/packages/accounts-google/notice.js b/packages/accounts-google/notice.js index 16409b385c..f1073910df 100644 --- a/packages/accounts-google/notice.js +++ b/packages/accounts-google/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('google-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'google-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-google,\n" + "but didn't install the configuration UI for the Google\n" + diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 188b32d4b2..31a383a3cb 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -1,10 +1,10 @@ Package.describe({ summary: "Login service for Google accounts", - version: "1.3.1" + version: "1.3.2", }); -Package.onUse(function(api) { - api.use(['ecmascript', 'underscore', 'random']); +Package.onUse(api => { + api.use(['ecmascript']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); diff --git a/packages/accounts-meetup/meetup.js b/packages/accounts-meetup/meetup.js index 5a25dde195..8b48549d07 100644 --- a/packages/accounts-meetup/meetup.js +++ b/packages/accounts-meetup/meetup.js @@ -1,20 +1,19 @@ Accounts.oauth.registerService('meetup'); if (Meteor.isClient) { - const loginWithMeetup = function(options, callback) { + const loginWithMeetup = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Meetup.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('meetup', loginWithMeetup); - Meteor.loginWithMeetup = function () { - return Accounts.applyLoginFunction('meetup', arguments); - }; + Meteor.loginWithMeetup = + (...args) => Accounts.applyLoginFunction('meetup', args); } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately diff --git a/packages/accounts-meetup/notice.js b/packages/accounts-meetup/notice.js index 2be49da263..a2810d743d 100644 --- a/packages/accounts-meetup/notice.js +++ b/packages/accounts-meetup/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('meetup-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'meetup-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-meetup,\n" + "but didn't install the configuration UI for the Meetup\n" + diff --git a/packages/accounts-meetup/package.js b/packages/accounts-meetup/package.js index 829eace0b0..9cc3b51cc1 100644 --- a/packages/accounts-meetup/package.js +++ b/packages/accounts-meetup/package.js @@ -1,9 +1,9 @@ Package.describe({ summary: 'Login service for Meetup accounts', - version: '1.4.1' + version: '1.4.2', }); -Package.onUse(function (api) { +Package.onUse(api => { api.use('ecmascript'); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. diff --git a/packages/accounts-meteor-developer/meteor-developer.js b/packages/accounts-meteor-developer/meteor-developer.js index fed49ba759..54f61f5565 100644 --- a/packages/accounts-meteor-developer/meteor-developer.js +++ b/packages/accounts-meteor-developer/meteor-developer.js @@ -1,21 +1,20 @@ Accounts.oauth.registerService("meteor-developer"); if (Meteor.isClient) { - const loginWithMeteorDeveloperAccount = function (options, callback) { + const loginWithMeteorDeveloperAccount = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } - var credentialRequestCompleteCallback = + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('meteor-developer', loginWithMeteorDeveloperAccount); - Meteor.loginWithMeteorDeveloperAccount = function () { - return Accounts.applyLoginFunction('meteor-developer', arguments); - }; + Meteor.loginWithMeteorDeveloperAccount = (...args) => + Accounts.applyLoginFunction('meteor-developer', args); } else { Accounts.addAutopublishFields({ // publish all fields including access token, which can legitimately be used diff --git a/packages/accounts-meteor-developer/notice.js b/packages/accounts-meteor-developer/notice.js index 86c29290b7..3cb368e563 100644 --- a/packages/accounts-meteor-developer/notice.js +++ b/packages/accounts-meteor-developer/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('meteor-developer-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'meteor-developer-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-meteor-developer,\n" + "but didn't install the configuration UI for the Meteor Developer\n" + diff --git a/packages/accounts-meteor-developer/package.js b/packages/accounts-meteor-developer/package.js index fa5b659c36..886d1b5c99 100644 --- a/packages/accounts-meteor-developer/package.js +++ b/packages/accounts-meteor-developer/package.js @@ -1,10 +1,10 @@ Package.describe({ summary: 'Login service for Meteor developer accounts', - version: '1.4.1' + version: '1.4.2', }); -Package.onUse(function (api) { - api.use(['ecmascript', 'underscore', 'random']); +Package.onUse(api => { + api.use(['ecmascript']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); diff --git a/packages/accounts-oauth/oauth_client.js b/packages/accounts-oauth/oauth_client.js index 7290158083..c04327e172 100644 --- a/packages/accounts-oauth/oauth_client.js +++ b/packages/accounts-oauth/oauth_client.js @@ -19,7 +19,7 @@ // Allow server to specify a specify subclass of errors. We should come // up with a more generic way to do this! -var convertError = function (err) { +const convertError = err => { if (err && err instanceof Meteor.Error && err.error === Accounts.LoginCancelledError.numericError) return new Accounts.LoginCancelledError(err.reason); @@ -34,8 +34,8 @@ var convertError = function (err) { // credentialSecret for a successful login is stored in session // storage. -Meteor.startup(function () { - var oauth = OAuth.getDataAfterRedirect(); +Meteor.startup(() => { + const oauth = OAuth.getDataAfterRedirect(); if (! oauth) return; @@ -43,12 +43,13 @@ Meteor.startup(function () { // successfully. However we still call the login method anyway to // retrieve the error if the login was unsuccessful. - var methodName = 'login'; - var methodArguments = [{oauth: _.pick(oauth, 'credentialToken', 'credentialSecret')}]; + const methodName = 'login'; + const { credentialToken, credentialSecret } = oauth; + const methodArguments = [{ oauth: { credentialToken, credentialSecret } }]; Accounts.callLoginMethod({ - methodArguments: methodArguments, - userCallback: function (err) { + methodArguments, + userCallback: err => { // The redirect login flow is complete. Construct an // `attemptInfo` object with the login result, and report back // to the code which initiated the login attempt @@ -58,8 +59,8 @@ Meteor.startup(function () { type: oauth.loginService, allowed: !err, error: err, - methodName: methodName, - methodArguments: methodArguments + methodName, + methodArguments, }); } }); @@ -69,24 +70,20 @@ Meteor.startup(function () { // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. -Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) { - var credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null; +Accounts.oauth.tryLoginAfterPopupClosed = (credentialToken, callback) => { + const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null; Accounts.callLoginMethod({ - methodArguments: [{oauth: { - credentialToken: credentialToken, - credentialSecret: credentialSecret - }}], - userCallback: callback && function (err) { - callback(convertError(err)); - }}); + methodArguments: [{oauth: { credentialToken, credentialSecret }}], + userCallback: callback && (err => callback(convertError(err))), + }); }; -Accounts.oauth.credentialRequestCompleteHandler = function(callback) { - return function (credentialTokenOrError) { +Accounts.oauth.credentialRequestCompleteHandler = callback => + credentialTokenOrError => { if(credentialTokenOrError && credentialTokenOrError instanceof Error) { callback && callback(credentialTokenOrError); } else { Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback); } - }; -}; + } + diff --git a/packages/accounts-oauth/oauth_common.js b/packages/accounts-oauth/oauth_common.js index eecebfca1c..05f6307331 100644 --- a/packages/accounts-oauth/oauth_common.js +++ b/packages/accounts-oauth/oauth_common.js @@ -1,12 +1,13 @@ Accounts.oauth = {}; -var services = {}; +const services = {}; +const hasOwn = Object.prototype.hasOwnProperty; // Helper for registering OAuth based accounts packages. // On the server, adds an index to the user collection. -Accounts.oauth.registerService = function (name) { - if (_.has(services, name)) - throw new Error("Duplicate service: " + name); +Accounts.oauth.registerService = name => { + if (hasOwn.call(services, name)) + throw new Error(`Duplicate service: ${name}`); services[name] = true; if (Meteor.server) { @@ -14,8 +15,7 @@ Accounts.oauth.registerService = function (name) { // so this should be a unique index. You might want to add indexes for other // fields returned by your service (eg services.github.login) but you can do // that in your app. - Meteor.users._ensureIndex('services.' + name + '.id', - {unique: 1, sparse: 1}); + Meteor.users._ensureIndex(`services.${name}.id`, {unique: 1, sparse: 1}); } }; @@ -24,12 +24,10 @@ Accounts.oauth.registerService = function (name) { // contain it. // It's worth noting that already logged in users will remain logged in unless // you manually expire their sessions. -Accounts.oauth.unregisterService = function (name) { - if (!_.has(services, name)) - throw new Error("Service not found: " + name); +Accounts.oauth.unregisterService = name => { + if (!hasOwn.call(services, name)) + throw new Error(`Service not found: ${name}`); delete services[name]; }; -Accounts.oauth.serviceNames = function () { - return _.keys(services); -}; +Accounts.oauth.serviceNames = () => Object.keys(services); diff --git a/packages/accounts-oauth/oauth_server.js b/packages/accounts-oauth/oauth_server.js index 74090435b2..c76b2e439b 100644 --- a/packages/accounts-oauth/oauth_server.js +++ b/packages/accounts-oauth/oauth_server.js @@ -1,6 +1,6 @@ // Listen to calls to `login` with an oauth option set. This is where // users actually get logged in to meteor via oauth. -Accounts.registerLoginHandler(function (options) { +Accounts.registerLoginHandler(options => { if (!options.oauth) return undefined; // don't handle @@ -13,7 +13,7 @@ Accounts.registerLoginHandler(function (options) { credentialSecret: Match.OneOf(null, String) }); - var result = OAuth.retrieveCredential(options.oauth.credentialToken, + const result = OAuth.retrieveCredential(options.oauth.credentialToken, options.oauth.credentialSecret); if (!result) { @@ -42,14 +42,14 @@ Accounts.registerLoginHandler(function (options) { // to the user. throw result; else { - if (!_.contains(Accounts.oauth.serviceNames(), result.serviceName)) { + if (! Accounts.oauth.serviceNames().includes(result.serviceName)) { // serviceName was not found in the registered services list. // This could happen because the service never registered itself or // unregisterService was called on it. return { type: "oauth", error: new Meteor.Error( Accounts.LoginCancelledError.numericError, - "No registered oauth service found for: " + result.serviceName) }; + `No registered oauth service found for: ${result.serviceName}`) }; } return Accounts.updateOrCreateUserFromExternalService(result.serviceName, result.serviceData, result.options); diff --git a/packages/accounts-oauth/package.js b/packages/accounts-oauth/package.js index 41e5f9d9e4..c294020d38 100644 --- a/packages/accounts-oauth/package.js +++ b/packages/accounts-oauth/package.js @@ -1,12 +1,10 @@ Package.describe({ summary: "Common code for OAuth-based login services", - version: "1.1.15" + version: "1.1.16", }); -Package.onUse(function (api) { - api.use('underscore', ['client', 'server']); - api.use('random', ['client', 'server']); - api.use('check', ['client', 'server']); +Package.onUse(api => { + api.use('check', 'server'); api.use('webapp', 'server'); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. @@ -19,6 +17,6 @@ Package.onUse(function (api) { }); -Package.onTest(function (api) { +Package.onTest(api => { api.addFiles("oauth_tests.js", 'server'); }); diff --git a/packages/accounts-password/email_templates.js b/packages/accounts-password/email_templates.js index badf763583..b5731465a1 100644 --- a/packages/accounts-password/email_templates.js +++ b/packages/accounts-password/email_templates.js @@ -1,7 +1,6 @@ -function greet(welcomeMsg) { - return function(user, url) { - var greeting = (user.profile && user.profile.name) ? - ("Hello " + user.profile.name + ",") : "Hello,"; +const greet = welcomeMsg => (user, url) => { + const greeting = (user.profile && user.profile.name) ? + (`Hello ${user.profile.name},`) : "Hello,"; return `${greeting} ${welcomeMsg}, simply click the link below. @@ -10,8 +9,7 @@ ${url} Thanks. `; - }; -} +}; /** * @summary Options to customize emails sent from the Accounts system. @@ -23,21 +21,15 @@ Accounts.emailTemplates = { siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), resetPassword: { - subject: function(user) { - return "How to reset your password on " + Accounts.emailTemplates.siteName; - }, - text: greet("To reset your password") + subject: () => `How to reset your password on ${Accounts.emailTemplates.siteName}`, + text: greet("To reset your password"), }, verifyEmail: { - subject: function(user) { - return "How to verify email address on " + Accounts.emailTemplates.siteName; - }, - text: greet("To verify your account email") + subject: () => `How to verify email address on ${Accounts.emailTemplates.siteName}`, + text: greet("To verify your account email"), }, enrollAccount: { - subject: function(user) { - return "An account has been created for you on " + Accounts.emailTemplates.siteName; - }, - text: greet("To start using the service") - } + subject: () => `An account has been created for you on ${Accounts.emailTemplates.siteName}`, + text: greet("To start using the service"), + }, }; diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index 62d2dfbedf..afcdef3905 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -1,6 +1,6 @@ -var resetPasswordToken; -var verifyEmailToken; -var enrollAccountToken; +let resetPasswordToken; +let verifyEmailToken; +let enrollAccountToken; Accounts._isolateLoginTokenForTest(); @@ -11,10 +11,10 @@ if (Meteor.isServer) { testAsyncMulti("accounts emails - reset password flow", [ function (test, expect) { this.randomSuffix = Random.id(); - this.email = "Ada-intercept@example.com" + this.randomSuffix; + this.email = `Ada-intercept@example.com${this.randomSuffix}`; // Create the user with another email and add the tested for email later, // so we can test whether forgotPassword respects the passed in email - Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'}, + Accounts.createUser({email: `another@example.com${this.randomSuffix}`, password: 'foobar'}, expect((error) => { test.equal(error, undefined); Meteor.call("addEmailForTestAndVerify", this.email); @@ -31,10 +31,10 @@ testAsyncMulti("accounts emails - reset password flow", [ test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 2); // the first is the email verification - var options = result[1]; + const options = result[1]; - var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); - var match = options.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const match = options.text.match(re); test.isTrue(match); resetPasswordToken = match[1]; test.isTrue(options.html.match(re)); @@ -73,17 +73,17 @@ testAsyncMulti(`accounts emails - \ reset password flow with case insensitive email`, [ function (test, expect) { this.randomSuffix = Random.id(); - this.email = "Ada-intercept@example.com" + this.randomSuffix; + this.email = `Ada-intercept@example.com${this.randomSuffix}`; // Create the user with another email and add the tested for email later, // so we can test whether forgotPassword respects the passed in email - Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'}, + Accounts.createUser({email: `another@example.com${this.randomSuffix}`, password: 'foobar'}, expect((error) => { test.equal(error, undefined); Meteor.call("addEmailForTestAndVerify", this.email); })); }, function (test, expect) { - Accounts.forgotPassword({email: "ada-intercept@example.com" + this.randomSuffix}, expect((error) => { + Accounts.forgotPassword({email: `ada-intercept@example.com${this.randomSuffix}`}, expect(error => { test.equal(error, undefined); })); }, @@ -93,10 +93,10 @@ reset password flow with case insensitive email`, [ test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 2); // the first is the email verification - var options = result[1]; + const options = result[1]; - var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); - var match = options.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const match = options.text.match(re); test.isTrue(match); resetPasswordToken = match[1]; test.isTrue(options.html.match(re)); @@ -131,16 +131,16 @@ reset password flow with case insensitive email`, [ } ]); -var getVerifyEmailToken = function (email, test, expect) { +const getVerifyEmailToken = (email, test, expect) => { Accounts.connection.call( "getInterceptedEmails", email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 1); - var options = result[0]; + const options = result[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/verify-email/(\\S*)"); - var match = options.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`); + const match = options.text.match(re); test.isTrue(match); verifyEmailToken = match[1]; test.isTrue(options.html.match(re)); @@ -150,22 +150,20 @@ var getVerifyEmailToken = function (email, test, expect) { })); }; -var loggedIn = function (test, expect) { - return expect((error) => { +const loggedIn = (test, expect) => expect((error) => { test.equal(error, undefined); test.isTrue(Meteor.user()); }); -}; testAsyncMulti("accounts emails - verify email flow", [ function (test, expect) { - this.email = Random.id() + "-intercept@example.com"; + this.email = `${Random.id()}-intercept@example.com`; const emailId = Random.id(); - this.anotherEmail = emailId.toLowerCase() + "-intercept@example.com"; + this.anotherEmail = `${emailId.toLowerCase()}-intercept@example.com`; // Add the same email as 'anotherEmail' but in upper case in order to check if // the verification token will be removed for the email in upperCase and in // lowerCase. - this.anotherEmailCaps = emailId.toUpperCase() +"-INTERCEPT@example.com"; + this.anotherEmailCaps = `${emailId.toUpperCase()}-INTERCEPT@example.com`; Accounts.createUser( {email: this.email, password: 'foobar'}, loggedIn(test, expect)); @@ -175,7 +173,7 @@ testAsyncMulti("accounts emails - verify email flow", [ test.equal(Meteor.user().emails[0].address, this.email); test.isFalse(Meteor.user().emails[0].verified); // We should NOT be publishing things like verification tokens! - test.isFalse(_.has(Meteor.user(), 'services')); + test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user(), 'services')); }, function (test, expect) { getVerifyEmailToken(this.email, test, expect); @@ -262,32 +260,32 @@ testAsyncMulti("accounts emails - verify email flow", [ } ]); -var getEnrollAccountToken = function (email, test, expect) { +const getEnrollAccountToken = (email, test, expect) => Accounts.connection.call( "getInterceptedEmails", email, expect((error, result) => { test.equal(error, undefined); test.notEqual(result, undefined); test.equal(result.length, 1); - var options = result[0]; + const options = result[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)") - var match = options.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`) + const match = options.text.match(re); test.isTrue(match); enrollAccountToken = match[1]; test.isTrue(options.html.match(re)); test.equal(options.from, 'test@meteor.com'); test.equal(options.headers['My-Custom-Header'], 'Cool'); - })); -}; + }) + ); testAsyncMulti("accounts emails - enroll account flow", [ function (test, expect) { - this.email = Random.id() + "-intercept@example.com"; + this.email = `${Random.id()}-intercept@example.com`; Accounts.connection.call("createUserOnServer", this.email, expect((error, result) => { test.isFalse(error); - var user = result; + const user = result; test.equal(user.emails.length, 1); test.equal(user.emails[0].address, this.email); test.isFalse(user.emails[0].verified); diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index ddd4c9c241..32c080a4a5 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -3,30 +3,26 @@ // the string "intercept", storing them in an array that can then // be retrieved using the getInterceptedEmails method // -var interceptedEmails = {}; // (email address) -> (array of options) +const interceptedEmails = {}; // (email address) -> (array of options) // add html email templates that just contain the url Accounts.emailTemplates.resetPassword.html = Accounts.emailTemplates.enrollAccount.html = - Accounts.emailTemplates.verifyEmail.html = function (user, url) { - return url; - }; + Accounts.emailTemplates.verifyEmail.html = (user, url) => url; // override the from address Accounts.emailTemplates.resetPassword.from = Accounts.emailTemplates.enrollAccount.from = - Accounts.emailTemplates.verifyEmail.from = function (user) { - return 'test@meteor.com'; - }; + Accounts.emailTemplates.verifyEmail.from = user => 'test@meteor.com'; // add a custom header to check against Accounts.emailTemplates.headers = { 'My-Custom-Header' : 'Cool' }; -EmailTest.hookSend(function (options) { - var to = options.to; - if (!to || to.toUpperCase().indexOf('INTERCEPT') === -1) { +EmailTest.hookSend(options => { + const { to } = options; + if (!to || !to.toUpperCase().includes('INTERCEPT')) { return true; // go ahead and send } else { if (!interceptedEmails[to]) @@ -38,22 +34,22 @@ EmailTest.hookSend(function (options) { }); Meteor.methods({ - getInterceptedEmails: function (email) { + getInterceptedEmails: email => { check(email, String); return interceptedEmails[email]; }, - addEmailForTestAndVerify: function (email) { + addEmailForTestAndVerify: email => { check(email, String); Meteor.users.update( - {_id: this.userId}, + {_id: Accounts.userId()}, {$push: {emails: {address: email, verified: false}}}); - Accounts.sendVerificationEmail(this.userId, email); + Accounts.sendVerificationEmail(Accounts.userId(), email); }, - createUserOnServer: function (email) { + createUserOnServer: email => { check(email, String); - var userId = Accounts.createUser({email: email}); + const userId = Accounts.createUser({ email }); Accounts.sendEnrollmentEmail(userId); return Meteor.users.findOne(userId); } diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index e5db3b2bfc..5d614113d5 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -8,7 +8,7 @@ Package.describe({ version: "1.5.1" }); -Package.onUse(function(api) { +Package.onUse(api => { api.use('npm-bcrypt', 'server'); api.use([ @@ -22,10 +22,9 @@ Package.onUse(function(api) { // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); - api.use('email', ['server']); - api.use('random', ['server']); - api.use('check'); - api.use('underscore'); + api.use('email', 'server'); + api.use('random', 'server'); + api.use('check', 'server'); api.use('ecmascript'); api.addFiles('email_templates.js', 'server'); @@ -33,10 +32,9 @@ Package.onUse(function(api) { api.addFiles('password_client.js', 'client'); }); -Package.onTest(function(api) { +Package.onTest(api => { api.use(['accounts-password', 'tinytest', 'test-helpers', 'tracker', - 'accounts-base', 'random', 'email', 'underscore', 'check', - 'ddp', 'ecmascript']); + 'accounts-base', 'random', 'email', 'check', 'ddp', 'ecmascript']); api.addFiles('password_tests_setup.js', 'server'); api.addFiles('password_tests.js', ['client', 'server']); api.addFiles('email_tests_setup.js', 'server'); diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 28e2787c4f..022c1cdf65 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -1,5 +1,5 @@ // Used in the various functions below to handle errors consistently -function reportError(error, callback) { +const reportError = (error, callback) => { if (callback) { callback(error); } else { @@ -30,9 +30,9 @@ function reportError(error, callback) { * on failure. * @importFromPackage meteor */ -Meteor.loginWithPassword = function (selector, password, callback) { +Meteor.loginWithPassword = (selector, password, callback) => { if (typeof selector === 'string') - if (selector.indexOf('@') === -1) + if (!selector.includes('@')) selector = {username: selector}; else selector = {email: selector}; @@ -42,7 +42,7 @@ Meteor.loginWithPassword = function (selector, password, callback) { user: selector, password: Accounts._hashPassword(password) }], - userCallback: function (error, result) { + userCallback: (error, result) => { if (error && error.error === 400 && error.reason === 'old password format') { // The "reason" string should match the error thrown in the @@ -72,12 +72,11 @@ Meteor.loginWithPassword = function (selector, password, callback) { }); }; -Accounts._hashPassword = function (password) { - return { - digest: SHA256(password), - algorithm: "sha-256" - }; -}; +Accounts._hashPassword = password => ({ + digest: SHA256(password), + algorithm: "sha-256" +}); + // XXX COMPAT WITH 0.8.1.3 // The server requested an upgrade from the old SRP password format, @@ -86,8 +85,8 @@ Accounts._hashPassword = function (password) { // us to upgrade from SRP to bcrypt. // - userSelector: selector to retrieve the user object // - plaintextPassword: the password as a string -var srpUpgradePath = function (options, callback) { - var details; +const srpUpgradePath = (options, callback) => { + let details; try { details = EJSON.parse(options.upgradeError.details); } catch (e) {} @@ -99,7 +98,7 @@ var srpUpgradePath = function (options, callback) { Accounts.callLoginMethod({ methodArguments: [{ user: options.userSelector, - srp: SHA256(details.identity + ":" + options.plaintextPassword), + srp: SHA256(`${details.identity}:${options.plaintextPassword}`), password: Accounts._hashPassword(options.plaintextPassword) }], userCallback: callback @@ -120,8 +119,8 @@ var srpUpgradePath = function (options, callback) { * @param {Function} [callback] Client only, optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.createUser = function (options, callback) { - options = _.clone(options); // we'll be modifying options +Accounts.createUser = (options, callback) => { + options = { ...options }; // we'll be modifying options if (typeof options.password !== 'string') throw new Error("options.password must be a string"); @@ -155,12 +154,15 @@ Accounts.createUser = function (options, callback) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.changePassword = function (oldPassword, newPassword, callback) { +Accounts.changePassword = (oldPassword, newPassword, callback) => { if (!Meteor.user()) { return reportError(new Error("Must be logged in to change password."), callback); } - check(newPassword, String); + if (!newPassword instanceof String) { + return reportError(new Meteor.Error(400, "Password must be a string"), callback); + } + if (!newPassword) { return reportError(new Meteor.Error(400, "Password may not be empty"), callback); } @@ -169,7 +171,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { 'changePassword', [oldPassword ? Accounts._hashPassword(oldPassword) : null, Accounts._hashPassword(newPassword)], - function (error, result) { + (error, result) => { if (error || !result) { if (error && error.error === 400 && error.reason === 'old password format') { @@ -180,7 +182,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { upgradeError: error, userSelector: { id: Meteor.userId() }, plaintextPassword: oldPassword - }, function (err) { + }, err => { if (err) { reportError(err, callback); } else { @@ -216,7 +218,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.forgotPassword = function(options, callback) { +Accounts.forgotPassword = (options, callback) => { if (!options.email) { return reportError(new Meteor.Error(400, "Must pass options.email"), callback); } @@ -243,9 +245,14 @@ Accounts.forgotPassword = function(options, callback) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.resetPassword = function(token, newPassword, callback) { - check(token, String); - check(newPassword, String); +Accounts.resetPassword = (token, newPassword, callback) => { + if (!token instanceof String) { + return reportError(new Meteor.Error(400, "Token must be a string"), callback); + } + + if (!newPassword instanceof String) { + return reportError(new Meteor.Error(400, "Password must be a string"), callback); + } if (!newPassword) { return reportError(new Meteor.Error(400, "Password may not be empty"), callback); @@ -270,7 +277,7 @@ Accounts.resetPassword = function(token, newPassword, callback) { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.verifyEmail = function(token, callback) { +Accounts.verifyEmail = (token, callback) => { if (!token) { return reportError(new Meteor.Error(400, "Need to pass token"), callback); } diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 3566b87c91..dcb8258dc1 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1,8 +1,11 @@ /// BCRYPT -var bcrypt = NpmModuleBcrypt; -var bcryptHash = Meteor.wrapAsync(bcrypt.hash); -var bcryptCompare = Meteor.wrapAsync(bcrypt.compare); +const bcrypt = NpmModuleBcrypt; +const bcryptHash = Meteor.wrapAsync(bcrypt.hash); +const bcryptCompare = Meteor.wrapAsync(bcrypt.compare); + +// Utility for grabbing user +const getUserById = id => Meteor.users.findOne(id); // User records have a 'services.password.bcrypt' field on them to hold // their hashed passwords (unless they have a 'services.password.srp' @@ -29,7 +32,7 @@ Accounts._bcryptRounds = () => Accounts._options.bcryptRounds || 10; // - String (the plaintext password) // - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256". // -var getPasswordString = function (password) { +const getPasswordString = password => { if (typeof password === "string") { password = SHA256(password); } else { // 'password' is an object @@ -47,7 +50,7 @@ var getPasswordString = function (password) { // SHA256 before bcrypt) or an object with properties `digest` and // `algorithm` (in which case we bcrypt `password.digest`). // -var hashPassword = function (password) { +const hashPassword = password => { password = getPasswordString(password); return bcryptHash(password, Accounts._bcryptRounds()); }; @@ -70,8 +73,8 @@ const getRoundsFromBcryptHash = hash => { // properties `digest` and `algorithm` (in which case we bcrypt // `password.digest`). // -Accounts._checkPassword = function (user, password) { - var result = { +Accounts._checkPassword = (user, password) => { + const result = { userId: user._id }; @@ -95,7 +98,7 @@ Accounts._checkPassword = function (user, password) { return result; }; -var checkPassword = Accounts._checkPassword; +const checkPassword = Accounts._checkPassword; /// /// ERROR HANDLER @@ -117,14 +120,14 @@ const handleError = (msg, throwError = true) => { /// LOGIN /// -Accounts._findUserByQuery = function (query) { - var user = null; +Accounts._findUserByQuery = query => { + let user = null; if (query.id) { - user = Meteor.users.findOne({ _id: query.id }); + user = getUserById(query.id); } else { - var fieldName; - var fieldValue; + let fieldName; + let fieldValue; if (query.username) { fieldName = 'username'; fieldValue = query.username; @@ -134,13 +137,13 @@ Accounts._findUserByQuery = function (query) { } else { throw new Error("shouldn't happen (validation missed something)"); } - var selector = {}; + let selector = {}; selector[fieldName] = fieldValue; user = Meteor.users.findOne(selector); // If user is not found, try a case insensitive lookup if (!user) { selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue); - var candidateUsers = Meteor.users.find(selector).fetch(); + const candidateUsers = Meteor.users.find(selector).fetch(); // No match if multiple candidates are found if (candidateUsers.length === 1) { user = candidateUsers[0]; @@ -161,11 +164,8 @@ Accounts._findUserByQuery = function (query) { * @returns {Object} A user if found, else null * @importFromPackage accounts-base */ -Accounts.findUserByUsername = function (username) { - return Accounts._findUserByQuery({ - username: username - }); -}; +Accounts.findUserByUsername = + username => Accounts._findUserByQuery({ username }); /** * @summary Finds the user with the specified email. @@ -177,11 +177,7 @@ Accounts.findUserByUsername = function (username) { * @returns {Object} A user if found, else null * @importFromPackage accounts-base */ -Accounts.findUserByEmail = function (email) { - return Accounts._findUserByQuery({ - email: email - }); -}; +Accounts.findUserByEmail = email => Accounts._findUserByQuery({ email }); // Generates a MongoDB selector that can be used to perform a fast case // insensitive lookup for the given fieldName and string. Since MongoDB does @@ -192,48 +188,48 @@ Accounts.findUserByEmail = function (email) { // http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use), // this has been found to greatly improve performance (from 1200ms to 5ms in a // test with 1.000.000 users). -var selectorForFastCaseInsensitiveLookup = function (fieldName, string) { +const selectorForFastCaseInsensitiveLookup = (fieldName, string) => { // Performance seems to improve up to 4 prefix characters - var prefix = string.substring(0, Math.min(string.length, 4)); - var orClause = _.map(generateCasePermutationsForString(prefix), - function (prefixPermutation) { - var selector = {}; + const prefix = string.substring(0, Math.min(string.length, 4)); + const orClause = generateCasePermutationsForString(prefix).map( + prefixPermutation => { + const selector = {}; selector[fieldName] = - new RegExp('^' + Meteor._escapeRegExp(prefixPermutation)); + new RegExp(`^${Meteor._escapeRegExp(prefixPermutation)}`); return selector; }); - var caseInsensitiveClause = {}; + const caseInsensitiveClause = {}; caseInsensitiveClause[fieldName] = - new RegExp('^' + Meteor._escapeRegExp(string) + '$', 'i') + new RegExp(`^${Meteor._escapeRegExp(string)}$`, 'i') return {$and: [{$or: orClause}, caseInsensitiveClause]}; } // Generates permutations of all case variations of a given string. -var generateCasePermutationsForString = function (string) { - var permutations = ['']; - for (var i = 0; i < string.length; i++) { - var ch = string.charAt(i); - permutations = _.flatten(_.map(permutations, function (prefix) { - var lowerCaseChar = ch.toLowerCase(); - var upperCaseChar = ch.toUpperCase(); +const generateCasePermutationsForString = string => { + let permutations = ['']; + for (let i = 0; i < string.length; i++) { + const ch = string.charAt(i); + permutations = [].concat(...(permutations.map(prefix => { + const lowerCaseChar = ch.toLowerCase(); + const upperCaseChar = ch.toUpperCase(); // Don't add unneccesary permutations when ch is not a letter if (lowerCaseChar === upperCaseChar) { return [prefix + ch]; } else { return [prefix + lowerCaseChar, prefix + upperCaseChar]; } - })); + }))); } return permutations; } -var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) { +const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => { // Some tests need the ability to add users with the same case insensitive // value, hence the _skipCaseInsensitiveChecksForTest check - var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); + const skipCheck = Object.prototype.hasOwnProperty.call(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); if (fieldValue && !skipCheck) { - var matchedUsers = Meteor.users.find( + const matchedUsers = Meteor.users.find( selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch(); if (matchedUsers.length > 0 && @@ -242,29 +238,29 @@ var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldV // Otherwise, check to see if there are multiple matches or a match // that is not us (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { - handleError(displayName + " already exists."); + handleError(`${displayName} already exists.`); } } }; // XXX maybe this belongs in the check package -var NonEmptyString = Match.Where(function (x) { +const NonEmptyString = Match.Where(x => { check(x, String); return x.length > 0; }); -var userQueryValidator = Match.Where(function (user) { +const userQueryValidator = Match.Where(user => { check(user, { id: Match.Optional(NonEmptyString), username: Match.Optional(NonEmptyString), email: Match.Optional(NonEmptyString) }); - if (_.keys(user).length !== 1) + if (Object.keys(user).length !== 1) throw new Match.Error("User property must have exactly one field"); return true; }); -var passwordValidator = Match.OneOf( +const passwordValidator = Match.OneOf( String, { digest: String, algorithm: String } ); @@ -283,7 +279,7 @@ var passwordValidator = Match.OneOf( // // Note that neither password option is secure without SSL. // -Accounts.registerLoginHandler("password", function (options) { +Accounts.registerLoginHandler("password", options => { if (! options.password || options.srp) return undefined; // don't handle @@ -293,7 +289,7 @@ Accounts.registerLoginHandler("password", function (options) { }); - var user = Accounts._findUserByQuery(options.user); + const user = Accounts._findUserByQuery(options.user); if (!user) { handleError("User not found"); } @@ -309,8 +305,8 @@ Accounts.registerLoginHandler("password", function (options) { // not upgraded to bcrypt yet. We don't attempt to tell the client // to upgrade to bcrypt, because it might be a standalone DDP // client doesn't know how to do such a thing. - var verifier = user.services.password.srp; - var newVerifier = SRP.generateVerifier(options.password, { + const verifier = user.services.password.srp; + const newVerifier = SRP.generateVerifier(options.password, { identity: verifier.identity, salt: verifier.salt}); if (verifier.verifier !== newVerifier.verifier) { @@ -351,7 +347,7 @@ Accounts.registerLoginHandler("password", function (options) { // try the SRP upgrade path. // // XXX COMPAT WITH 0.8.1.3 -Accounts.registerLoginHandler("password", function (options) { +Accounts.registerLoginHandler("password", options => { if (!options.srp || !options.password) { return undefined; // don't handle } @@ -362,7 +358,7 @@ Accounts.registerLoginHandler("password", function (options) { password: passwordValidator }); - var user = Accounts._findUserByQuery(options.user); + const user = Accounts._findUserByQuery(options.user); if (!user) { handleError("User not found"); } @@ -377,8 +373,8 @@ Accounts.registerLoginHandler("password", function (options) { handleError("User has no password set"); } - var v1 = user.services.password.srp.verifier; - var v2 = SRP.generateVerifier( + const v1 = user.services.password.srp.verifier; + const v2 = SRP.generateVerifier( null, { hashedIdentityAndPassword: options.srp, @@ -393,7 +389,7 @@ Accounts.registerLoginHandler("password", function (options) { } // Upgrade to bcrypt on successful login. - var salted = hashPassword(options.password); + const salted = hashPassword(options.password); Meteor.users.update( user._id, { @@ -419,16 +415,16 @@ Accounts.registerLoginHandler("password", function (options) { * @param {String} newUsername A new username for the user. * @importFromPackage accounts-base */ -Accounts.setUsername = function (userId, newUsername) { +Accounts.setUsername = (userId, newUsername) => { check(userId, NonEmptyString); check(newUsername, NonEmptyString); - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) { handleError("User not found"); } - var oldUsername = user.username; + const oldUsername = user.username; // Perform a case insensitive check for duplicates before update checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); @@ -469,7 +465,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { throw new Meteor.Error(401, "Must be logged in"); } - var user = Meteor.users.findOne(this.userId); + const user = getUserById(this.userId); if (!user) { handleError("User not found"); } @@ -486,18 +482,18 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { })); } - var result = checkPassword(user, oldPassword); + const result = checkPassword(user, oldPassword); if (result.error) { throw result.error; } - var hashed = hashPassword(newPassword); + const hashed = hashPassword(newPassword); // It would be better if this removed ALL existing tokens and replaced // the token for the current connection with a new one, but that would // be tricky, so we'll settle for just replacing all tokens other than // the one for the current connection. - var currentToken = Accounts._getLoginToken(this.connection.id); + const currentToken = Accounts._getLoginToken(this.connection.id); Meteor.users.update( { _id: this.userId }, { @@ -524,15 +520,15 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { * @param {Object} options.logout Logout all current connections with this userId (default: true) * @importFromPackage accounts-base */ -Accounts.setPassword = function (userId, newPlaintextPassword, options) { - options = _.extend({logout: true}, options); +Accounts.setPassword = (userId, newPlaintextPassword, options) => { + options = { logout: true , ...options }; - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) { throw new Meteor.Error(403, "User not found"); } - var update = { + const update = { $unset: { 'services.password.srp': 1, // XXX COMPAT WITH 0.8.1.3 'services.password.reset': 1 @@ -552,20 +548,23 @@ Accounts.setPassword = function (userId, newPlaintextPassword, options) { /// RESETTING VIA EMAIL /// +// Utility for plucking addresses from emails +const pluckAddresses = (emails = []) => emails.map(email => email.address); + // Method called by a user to request a password reset email. This is // the start of the reset process. -Meteor.methods({forgotPassword: function (options) { +Meteor.methods({forgotPassword: options => { check(options, {email: String}); - var user = Accounts.findUserByEmail(options.email); + const user = Accounts.findUserByEmail(options.email); if (!user) { handleError("User not found"); } - const emails = _.pluck(user.emails || [], 'address'); - const caseSensitiveEmail = _.find(emails, email => { - return email.toLowerCase() === options.email.toLowerCase(); - }); + const emails = pluckAddresses(user.emails); + const caseSensitiveEmail = emails.find( + email => email.toLowerCase() === options.email.toLowerCase() + ); Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); }}); @@ -580,9 +579,9 @@ Meteor.methods({forgotPassword: function (options) { * @returns {Object} Object with {email, user, token} values. * @importFromPackage accounts-base */ -Accounts.generateResetToken = function (userId, email, reason, extraTokenData) { +Accounts.generateResetToken = (userId, email, reason, extraTokenData) => { // Make sure the user exists, and email is one of their addresses. - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) { handleError("Can't find user"); } @@ -593,14 +592,15 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) { } // make sure we have a valid email - if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) { + if (!email || + !(pluckAddresses(user.emails).includes(email))) { handleError("No such email for user."); } - var token = Random.secret(); - var tokenRecord = { - token: token, - email: email, + const token = Random.secret(); + const tokenRecord = { + token, + email, when: new Date() }; @@ -614,7 +614,7 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) { } if (extraTokenData) { - _.extend(tokenRecord, extraTokenData); + Object.assign(tokenRecord, extraTokenData); } Meteor.users.update({_id: user._id}, {$set: { @@ -636,16 +636,16 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) { * @returns {Object} Object with {email, user, token} values. * @importFromPackage accounts-base */ -Accounts.generateVerificationToken = function (userId, email, extraTokenData) { +Accounts.generateVerificationToken = (userId, email, extraTokenData) => { // Make sure the user exists, and email is one of their addresses. - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) { handleError("Can't find user"); } // pick the first unverified email if we weren't passed an email. if (!email) { - var emailRecord = _.find(user.emails || [], function (e) { return !e.verified; }); + const emailRecord = (user.emails || []).find(e => !e.verified); email = (emailRecord || {}).address; if (!email) { @@ -654,20 +654,21 @@ Accounts.generateVerificationToken = function (userId, email, extraTokenData) { } // make sure we have a valid email - if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) { + if (!email || + !(pluckAddresses(user.emails).includes(email))) { handleError("No such email for user."); } - var token = Random.secret(); - var tokenRecord = { - token: token, + const token = Random.secret(); + const tokenRecord = { + token, // TODO: This should probably be renamed to "email" to match reset token record. address: email, when: new Date() }; if (extraTokenData) { - _.extend(tokenRecord, extraTokenData); + Object.assign(tokenRecord, extraTokenData); } Meteor.users.update({_id: user._id}, {$push: { @@ -695,8 +696,8 @@ Accounts.generateVerificationToken = function (userId, email, extraTokenData) { * @returns {Object} Options which can be passed to `Email.send`. * @importFromPackage accounts-base */ -Accounts.generateOptionsForEmail = function (email, user, url, reason) { - var options = { +Accounts.generateOptionsForEmail = (email, user, url, reason) => { + const options = { to: email, from: Accounts.emailTemplates[reason].from ? Accounts.emailTemplates[reason].from(user) @@ -731,7 +732,7 @@ Accounts.generateOptionsForEmail = function (email, user, url, reason) { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendResetPasswordEmail = function (userId, email, extraTokenData) { +Accounts.sendResetPasswordEmail = (userId, email, extraTokenData) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData); const url = Accounts.urls.resetPassword(token); @@ -757,7 +758,7 @@ Accounts.sendResetPasswordEmail = function (userId, email, extraTokenData) { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendEnrollmentEmail = function (userId, email, extraTokenData) { +Accounts.sendEnrollmentEmail = (userId, email, extraTokenData) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData); const url = Accounts.urls.enrollAccount(token); @@ -769,56 +770,54 @@ Accounts.sendEnrollmentEmail = function (userId, email, extraTokenData) { // Take token from sendResetPasswordEmail or sendEnrollmentEmail, change // the users password, and log them in. -Meteor.methods({resetPassword: function (token, newPassword) { - var self = this; +Meteor.methods({resetPassword: function (...args) { + const token = args[0]; + const newPassword = args[1]; return Accounts._loginMethod( - self, + this, "resetPassword", - arguments, + args, "password", - function () { + () => { check(token, String); check(newPassword, passwordValidator); - var user = Meteor.users.findOne({ + const user = Meteor.users.findOne({ "services.password.reset.token": token}); if (!user) { throw new Meteor.Error(403, "Token expired"); } - var when = user.services.password.reset.when; - var reason = user.services.password.reset.reason; - var tokenLifetimeMs = Accounts._getPasswordResetTokenLifetimeMs(); + const { when, reason, email } = user.services.password.reset; + let tokenLifetimeMs = Accounts._getPasswordResetTokenLifetimeMs(); if (reason === "enroll") { tokenLifetimeMs = Accounts._getPasswordEnrollTokenLifetimeMs(); } - var currentTimeMs = Date.now(); + const currentTimeMs = Date.now(); if ((currentTimeMs - when) > tokenLifetimeMs) throw new Meteor.Error(403, "Token expired"); - var email = user.services.password.reset.email; - if (!_.include(_.pluck(user.emails || [], 'address'), email)) + if (!(pluckAddresses(user.emails).includes(email))) return { userId: user._id, error: new Meteor.Error(403, "Token has invalid email address") }; - var hashed = hashPassword(newPassword); + const hashed = hashPassword(newPassword); // NOTE: We're about to invalidate tokens on the user, who we might be // logged in as. Make sure to avoid logging ourselves out if this // happens. But also make sure not to leave the connection in a state // of having a bad token set if things fail. - var oldToken = Accounts._getLoginToken(self.connection.id); - Accounts._setLoginToken(user._id, self.connection, null); - var resetToOldToken = function () { - Accounts._setLoginToken(user._id, self.connection, oldToken); - }; + const oldToken = Accounts._getLoginToken(this.connection.id); + Accounts._setLoginToken(user._id, this.connection, null); + const resetToOldToken = () => + Accounts._setLoginToken(user._id, this.connection, oldToken); try { // Update the user record by: // - Changing the password to the new one // - Forgetting about the reset token that was just used // - Verifying their email, since they got the password reset via email. - var affectedRecords = Meteor.users.update( + const affectedRecords = Meteor.users.update( { _id: user._id, 'emails.address': email, @@ -864,7 +863,7 @@ Meteor.methods({resetPassword: function (token, newPassword) { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendVerificationEmail = function (userId, email, extraTokenData) { +Accounts.sendVerificationEmail = (userId, email, extraTokenData) => { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created // this account. @@ -879,34 +878,33 @@ Accounts.sendVerificationEmail = function (userId, email, extraTokenData) { // Take token from sendVerificationEmail, mark the email as verified, // and log them in. -Meteor.methods({verifyEmail: function (token) { - var self = this; +Meteor.methods({verifyEmail: function (...args) { + const token = args[0]; return Accounts._loginMethod( - self, + this, "verifyEmail", - arguments, + args, "password", - function () { + () => { check(token, String); - var user = Meteor.users.findOne( + const user = Meteor.users.findOne( {'services.email.verificationTokens.token': token}); if (!user) throw new Meteor.Error(403, "Verify email link expired"); - var tokenRecord = _.find(user.services.email.verificationTokens, - function (t) { - return t.token == token; - }); + const tokenRecord = user.services.email.verificationTokens.find( + t => t.token == token + ); if (!tokenRecord) return { userId: user._id, error: new Meteor.Error(403, "Verify email link expired") }; - var emailsRecord = _.find(user.emails, function (e) { - return e.address == tokenRecord.address; - }); + const emailsRecord = user.emails.find( + e => e.address == tokenRecord.address + ); if (!emailsRecord) return { userId: user._id, @@ -941,16 +939,16 @@ Meteor.methods({verifyEmail: function (token) { * be marked as verified. Defaults to false. * @importFromPackage accounts-base */ -Accounts.addEmail = function (userId, newEmail, verified) { +Accounts.addEmail = (userId, newEmail, verified) => { check(userId, NonEmptyString); check(newEmail, NonEmptyString); check(verified, Match.Optional(Boolean)); - if (_.isUndefined(verified)) { + if (verified === void 0) { verified = false; } - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) throw new Meteor.Error(403, "User not found"); @@ -962,23 +960,26 @@ Accounts.addEmail = function (userId, newEmail, verified) { // then we are OK and (2) if this would create a conflict with other users // then there would already be a case-insensitive duplicate and we can't fix // that in this code anyway. - var caseInsensitiveRegExp = - new RegExp('^' + Meteor._escapeRegExp(newEmail) + '$', 'i'); + const caseInsensitiveRegExp = + new RegExp(`^${Meteor._escapeRegExp(newEmail)}$`, 'i'); - var didUpdateOwnEmail = _.any(user.emails, function(email, index) { - if (caseInsensitiveRegExp.test(email.address)) { - Meteor.users.update({ - _id: user._id, - 'emails.address': email.address - }, {$set: { - 'emails.$.address': newEmail, - 'emails.$.verified': verified - }}); - return true; - } - - return false; - }); + const didUpdateOwnEmail = user.emails.reduce( + (prev, email) => { + if (caseInsensitiveRegExp.test(email.address)) { + Meteor.users.update({ + _id: user._id, + 'emails.address': email.address + }, {$set: { + 'emails.$.address': newEmail, + 'emails.$.verified': verified + }}); + return true; + } else { + return prev; + } + }, + false + ); // In the other updates below, we have to do another call to // checkForCaseInsensitiveDuplicates to make sure that no conflicting values @@ -1025,11 +1026,11 @@ Accounts.addEmail = function (userId, newEmail, verified) { * @param {String} email The email address to remove. * @importFromPackage accounts-base */ -Accounts.removeEmail = function (userId, email) { +Accounts.removeEmail = (userId, email) => { check(userId, NonEmptyString); check(email, NonEmptyString); - var user = Meteor.users.findOne(userId); + const user = getUserById(userId); if (!user) throw new Meteor.Error(403, "User not found"); @@ -1046,7 +1047,7 @@ Accounts.removeEmail = function (userId, email) { // does the actual user insertion. // // returns the user id -var createUser = function (options) { +const createUser = options => { // Unknown keys allowed, because a onCreateUserHook can take arbitrary // options. check(options, Match.ObjectIncluding({ @@ -1055,14 +1056,13 @@ var createUser = function (options) { password: Match.Optional(passwordValidator) })); - var username = options.username; - var email = options.email; + const { username, email, password } = options; if (!username && !email) throw new Meteor.Error(400, "Need to set a username or email"); - var user = {services: {}}; - if (options.password) { - var hashed = hashPassword(options.password); + const user = {services: {}}; + if (password) { + const hashed = hashPassword(password); user.services.password = { bcrypt: hashed }; } @@ -1075,7 +1075,7 @@ var createUser = function (options) { checkForCaseInsensitiveDuplicates('username', 'Username', username); checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); - var userId = Accounts.insertUserDoc(options, user); + const userId = Accounts.insertUserDoc(options, user); // Perform another check after insert, in case a matching user has been // inserted in the meantime try { @@ -1090,14 +1090,14 @@ var createUser = function (options) { }; // method for create user. Requests come from the client. -Meteor.methods({createUser: function (options) { - var self = this; +Meteor.methods({createUser: function (...args) { + const options = args[0]; return Accounts._loginMethod( - self, + this, "createUser", - arguments, + args, "password", - function () { + () => { // createUser() above does more checking. check(options, Object); if (Accounts._options.forbidClientAccountCreation) @@ -1106,7 +1106,7 @@ Meteor.methods({createUser: function (options) { }; // Create user. result contains id and token. - var userId = createUser(options); + const userId = createUser(options); // safety belt. createUser is supposed to throw on error. send 500 error // instead of sending a verification email with empty userid. if (! userId) @@ -1136,8 +1136,8 @@ Meteor.methods({createUser: function (options) { // true", which we want to prevent the client from setting, but which a custom // method calling Accounts.createUser could set? // -Accounts.createUser = function (options, callback) { - options = _.clone(options); +Accounts.createUser = (options, callback) => { + options = { ...options }; // XXX allow an optional callback? if (callback) { diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 636ee1d490..43ec4dc1a1 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -5,114 +5,105 @@ if (Meteor.isServer) { Meteor.methods({ getResetToken: function () { - var token = Meteor.users.findOne(this.userId).services.password.reset; + const token = Meteor.users.findOne(this.userId).services.password.reset; return token; }, - addSkipCaseInsensitiveChecksForTest: function (value) { + addSkipCaseInsensitiveChecksForTest: value => { Accounts._skipCaseInsensitiveChecksForTest[value] = true; }, - removeSkipCaseInsensitiveChecksForTest: function (value) { + removeSkipCaseInsensitiveChecksForTest: value => { delete Accounts._skipCaseInsensitiveChecksForTest[value]; }, - countUsersOnServer: function (query) { - return Meteor.users.find(query).count(); - } + countUsersOnServer: query => Meteor.users.find(query).count(), }); } -if (Meteor.isClient) (function () { +if (Meteor.isClient) (() => { // XXX note, only one test can do login/logout things at once! for // now, that is this test. Accounts._isolateLoginTokenForTest(); - var addSkipCaseInsensitiveChecksForTest = function (value, test, expect) { + const addSkipCaseInsensitiveChecksForTest = (value, test, expect) => Meteor.call('addSkipCaseInsensitiveChecksForTest', value); - }; - var removeSkipCaseInsensitiveChecksForTest = function (value, test, expect) { + const removeSkipCaseInsensitiveChecksForTest = (value, test, expect) => Meteor.call('removeSkipCaseInsensitiveChecksForTest', value); - }; - var createUserStep = function (test, expect) { + const createUserStep = function (test, expect) { // Hack because Tinytest does not clean the database between tests/runs this.randomSuffix = Random.id(10); - this.username = 'AdaLovelace' + this.randomSuffix; - this.email = "Ada-intercept@lovelace.com" + this.randomSuffix; + this.username = `AdaLovelace${this.randomSuffix}`; + this.email = `Ada-intercept@lovelace.com${this.randomSuffix}`; this.password = 'password'; Accounts.createUser( {username: this.username, email: this.email, password: this.password}, loggedInAs(this.username, test, expect)); }; - var logoutStep = function (test, expect) { - Meteor.logout(expect(function (error) { + const logoutStep = (test, expect) => + Meteor.logout(expect(error => { if (error) { test.fail(error.message); } test.equal(Meteor.user(), null); })); - }; - var loggedInAs = function (someUsername, test, expect) { - return expect(function (error) { + const loggedInAs = (someUsername, test, expect) => { + return expect(error => { if (error) { test.fail(error.message); } test.equal(Meteor.userId() && Meteor.user().username, someUsername); }); }; - var loggedInUserHasEmail = function (someEmail, test, expect) { - return expect(function (error) { + const loggedInUserHasEmail = (someEmail, test, expect) => { + return expect(error => { if (error) { test.fail(error.message); } - var user = Meteor.user(); - test.isTrue(user && _.some(user.emails, function(email) { - return email.address === someEmail; - })); + const user = Meteor.user(); + test.isTrue(user && user.emails.reduce( + (prev, email) => prev || email.address === someEmail, + false + )); }); }; - var expectError = function (expectedError, test, expect) { - return expect(function (actualError) { - test.equal(actualError && actualError.error, expectedError.error); - test.equal(actualError && actualError.reason, expectedError.reason); - }); - }; - var expectUserNotFound = function (test, expect) { - return expectError(new Meteor.Error(403, "User not found"), test, expect); - }; - var waitForLoggedOutStep = function (test, expect) { - pollUntil(expect, function () { - return Meteor.userId() === null; - }, 10 * 1000, 100); - }; - var invalidateLoginsStep = function (test, expect) { - Meteor.call("testInvalidateLogins", 'fail', expect(function (error) { + const expectError = (expectedError, test, expect) => expect(actualError => { + test.equal(actualError && actualError.error, expectedError.error); + test.equal(actualError && actualError.reason, expectedError.reason); + }); + const expectUserNotFound = (test, expect) => + expectError(new Meteor.Error(403, "User not found"), test, expect); + const waitForLoggedOutStep = (test, expect) => pollUntil( + expect, + () => Meteor.userId() === null, + 10 * 1000, + 100 + ); + const invalidateLoginsStep = (test, expect) => + Meteor.call("testInvalidateLogins", 'fail', expect(error => { if (error) { test.fail(error.message); } })); - }; - var hideActualLoginErrorStep = function (test, expect) { - Meteor.call("testInvalidateLogins", 'hide', expect(function (error) { + const hideActualLoginErrorStep = (test, expect) => + Meteor.call("testInvalidateLogins", 'hide', expect(error => { if (error) { test.fail(error.message); } })); - }; - var validateLoginsStep = function (test, expect) { - Meteor.call("testInvalidateLogins", false, expect(function (error) { + const validateLoginsStep = (test, expect) => + Meteor.call("testInvalidateLogins", false, expect(error => { if (error) { test.fail(error.message); } })); - }; testAsyncMulti("passwords - basic login with password", [ function (test, expect) { // setup this.username = Random.id(); - this.email = Random.id() + '-intercept@example.com'; + this.email = `${Random.id()}-intercept@example.com`; this.password = 'password'; Accounts.createUser( @@ -133,14 +124,14 @@ if (Meteor.isClient) (function () { function (test, expect) { // Set up a reactive context that only refreshes when Meteor.user() is // invalidated. - var loaded = false; - var handle = Tracker.autorun(function () { + let loaded = false; + const handle = Tracker.autorun(() => { if (Meteor.user() && Meteor.user().emails) loaded = true; }); // At the beginning, we're not logged in. test.isFalse(loaded); - Meteor.loginWithPassword(this.username, this.password, expect(function (error) { + Meteor.loginWithPassword(this.username, this.password, expect(error => { test.equal(error, undefined); test.notEqual(Meteor.userId(), null); // By the time of the login callback, the user should be loaded. @@ -174,7 +165,7 @@ if (Meteor.isClient) (function () { function (test, expect) { // setup this.username = Random.id(); - this.email = Random.id() + '-intercept@example.com'; + this.email = `${Random.id()}-intercept@example.com`; this.password = 'password'; // create user with raw password (no API, need to invoke callLoginMethod @@ -220,7 +211,7 @@ if (Meteor.isClient) (function () { // We should be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( - { username: "adalovelace" + this.randomSuffix }, + { username: `adalovelace${this.randomSuffix}` }, this.password, loggedInAs(this.username, test, expect)); } @@ -231,7 +222,7 @@ if (Meteor.isClient) (function () { function (test, expect) { // Hack because Tinytest does not clean the database between tests/runs this.randomSuffix = Random.id(10); - this.username = 'ÁdaLØvela😈e' + this.randomSuffix; + this.username = `ÁdaLØvela😈e${this.randomSuffix}`; this.password = 'password'; Accounts.createUser( {username: this.username, email: this.email, password: this.password}, @@ -241,7 +232,7 @@ if (Meteor.isClient) (function () { // We should be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( - { username: "ádaløvela😈e" + this.randomSuffix }, + { username: `ádaløvela😈e${this.randomSuffix}` }, this.password, loggedInAs(this.username, test, expect)); } @@ -254,7 +245,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with a regex expression for the username function (test, expect) { Meteor.loginWithPassword( - { username: ".+" + this.randomSuffix }, + { username: `.+${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); } @@ -267,7 +258,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with a partial match for the username function (test, expect) { Meteor.loginWithPassword( - { username: "lovelace" + this.randomSuffix }, + { username: `lovelace${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); } @@ -278,7 +269,7 @@ if (Meteor.isClient) (function () { createUserStep, logoutStep, function (test, expect) { - this.otherUsername = 'Adalovelace' + this.randomSuffix; + this.otherUsername = `Adalovelace${this.randomSuffix}`; addSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect); }, // Create another user with a username that only differs in case @@ -293,7 +284,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with the username in lower case function (test, expect) { Meteor.loginWithPassword( - { username: "adalovelace" + this.randomSuffix }, + { username: `adalovelace${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); }, @@ -313,7 +304,7 @@ if (Meteor.isClient) (function () { // Attempting to create another user with a username that only differs in // case should fail function (test, expect) { - this.newUsername = 'adalovelace' + this.randomSuffix; + this.newUsername = `adalovelace${this.randomSuffix}`; Accounts.createUser( { username: this.newUsername, password: this.password }, expectError( @@ -337,7 +328,7 @@ if (Meteor.isClient) (function () { // We should be able to log in with the email in lower case function (test, expect) { Meteor.loginWithPassword( - { email: "ada-intercept@lovelace.com" + this.randomSuffix }, + { email: `ada-intercept@lovelace.com${this.randomSuffix}` }, this.password, loggedInAs(this.username, test, expect)); } @@ -350,7 +341,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with a regex expression for the email function (test, expect) { Meteor.loginWithPassword( - { email: ".+" + this.randomSuffix }, + { email: `.+${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); } @@ -363,7 +354,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with a partial match for the email function (test, expect) { Meteor.loginWithPassword( - { email: "com" + this.randomSuffix }, + { email: `com${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); } @@ -374,8 +365,8 @@ if (Meteor.isClient) (function () { createUserStep, logoutStep, function (test, expect) { - this.otherUsername = 'AdaLovelace' + Random.id(10); - this.otherEmail = "ADA-intercept@lovelace.com" + this.randomSuffix; + this.otherUsername = `AdaLovelace${Random.id(10)}`; + this.otherEmail = `ADA-intercept@lovelace.com${this.randomSuffix}`; addSkipCaseInsensitiveChecksForTest(this.otherEmail, test, expect); }, // Create another user with an email that only differs in case @@ -393,7 +384,7 @@ if (Meteor.isClient) (function () { // We shouldn't be able to log in with the email in lower case function (test, expect) { Meteor.loginWithPassword( - { email: "ada-intercept@lovelace.com" + this.randomSuffix }, + { email: `ada-intercept@lovelace.com${this.randomSuffix}` }, this.password, expectUserNotFound(test, expect)); }, @@ -412,7 +403,7 @@ if (Meteor.isClient) (function () { logoutStep, // Create user error without callback should throw error function (test, expect) { - this.newUsername = 'adalovelace' + this.randomSuffix; + this.newUsername = `adalovelace${this.randomSuffix}`; test.throws(function(){ Accounts.createUser({ username: this.newUsername, password: '' }); }, /Password may not be empty/); @@ -420,7 +411,7 @@ if (Meteor.isClient) (function () { // Attempting to create another user with an email that only differs in // case should fail function (test, expect) { - this.newEmail = "ada-intercept@lovelace.com" + this.randomSuffix; + this.newEmail = `ada-intercept@lovelace.com${this.randomSuffix}`; Accounts.createUser( { email: this.newEmail, password: this.password }, expectError( @@ -443,7 +434,7 @@ if (Meteor.isClient) (function () { function (test, expect) { // setup this.username = Random.id(); - this.email = Random.id() + '-intercept@example.com'; + this.email = `${Random.id()}-intercept@example.com`; this.password = 'password'; this.password2 = 'password2'; @@ -455,31 +446,30 @@ if (Meteor.isClient) (function () { // reset tokens get deleted on password change. function (test, expect) { Meteor.call("forgotPassword", - { email: this.email }, expect(function (error) { + { email: this.email }, expect(error => { test.isFalse(error); })); }, function (test, expect) { - var self = this; - Meteor.call("getResetToken", expect(function (err, token) { + Meteor.call("getResetToken", expect((err, token) => { test.isFalse(err); test.isTrue(token); - self.token = token; + this.token = token; })); }, // change password with bad old password. we stay logged in. function (test, expect) { - var self = this; - Accounts.changePassword('wrong', 'doesntmatter', expect(function (error) { + Accounts.changePassword('wrong', 'doesntmatter', expect(error => { test.isTrue(error); - test.equal(Meteor.user().username, self.username); + test.equal(Meteor.user().username, this.username); })); }, // change password with blank new password function (test, expect) { - test.throws(function(){ - Accounts.changePassword(this.password, ''); - }, /Password may not be empty/); + test.throws( + () => Accounts.changePassword(this.password, ''), + /Password may not be empty/ + ); }, // change password with good old password. function (test, expect) { @@ -487,7 +477,7 @@ if (Meteor.isClient) (function () { loggedInAs(this.username, test, expect)); }, function (test, expect) { - Meteor.call("getResetToken", expect(function (err, token) { + Meteor.call("getResetToken", expect((err, token) => { test.isFalse(err); test.isFalse(token); })); @@ -495,7 +485,7 @@ if (Meteor.isClient) (function () { logoutStep, // old password, failed login function (test, expect) { - Meteor.loginWithPassword(this.email, this.password, expect(function (error) { + Meteor.loginWithPassword(this.email, this.password, expect(error => { test.isTrue(error); test.isFalse(Meteor.user()); })); @@ -511,50 +501,50 @@ if (Meteor.isClient) (function () { testAsyncMulti("passwords - changing password logs out other clients", [ function (test, expect) { this.username = Random.id(); - this.email = Random.id() + '-intercept@example.com'; + this.email = `${Random.id()}-intercept@example.com`; this.password = 'password'; this.password2 = 'password2'; Accounts.createUser( { username: this.username, email: this.email, password: this.password }, - loggedInAs(this.username, test, expect)); + loggedInAs(this.username, test, expect) + ); }, // Log in a second connection as this user. function (test, expect) { - var self = this; - - self.secondConn = DDP.connect(Meteor.absoluteUrl()); - self.secondConn.call('login', - { user: { username: self.username }, password: self.password }, - expect(function (err, result) { + this.secondConn = DDP.connect(Meteor.absoluteUrl()); + this.secondConn.call('login', + { user: { username: this.username }, password: this.password }, + expect((err, result) => { test.isFalse(err); - self.secondConn.setUserId(result.id); - test.isTrue(self.secondConn.userId()); + this.secondConn.setUserId(result.id); + test.isTrue(this.secondConn.userId()); - self.secondConn.onReconnect = function () { - self.secondConn.apply( + this.secondConn.onReconnect = () => + this.secondConn.apply( 'login', [{ resume: result.token }], { wait: true }, - function (err, result) { - self.secondConn.setUserId(result && result.id || null); - } + (err, result) => + this.secondConn.setUserId(result && result.id || null) ); - }; })); }, function (test, expect) { - var self = this; - Accounts.changePassword(self.password, self.password2, expect(function (err) { - test.isFalse(err); - })); + Accounts.changePassword( + this.password, + this.password2, + expect(err => test.isFalse(err)) + ); }, // Now that we've changed the password, wait until the second // connection gets logged out. function (test, expect) { - var self = this; - pollUntil(expect, function () { - return self.secondConn.userId() === null; - }, 10 * 1000, 100); + pollUntil( + expect, + () => this.secondConn.userId() === null, + 10 * 1000, + 100 + ); } ]); @@ -567,22 +557,23 @@ if (Meteor.isClient) (function () { // forgotPassword called on client with blank email function (test, expect) { Accounts.forgotPassword( - { email: this.email }, expect(function (error) { - test.isTrue(error); - })); + { email: this.email }, + expect(error => test.isTrue(error)) + ); }, // forgotPassword called on client with blank email and no callback. function (test, expect) { - test.throws(function(){ - Accounts.forgotPassword({ email: this.email }); - }, /Must pass options\.email/); + test.throws( + () => Accounts.forgotPassword({ email: this.email }), + /Must pass options\.email/ + ); }, ]); Tinytest.add( 'passwords - forgotPassword only passes callback value to forgotPassword ' + 'Method if callback is defined (to address issue #5676)', - function (test) { + test => { let methodCallArgumentCount = 0; const originalMethodCall = Accounts.connection.call; const stubMethodCall = (...args) => { @@ -616,15 +607,16 @@ if (Meteor.isClient) (function () { // verifyEmail called on client with blank token function (test, expect) { Accounts.verifyEmail( - this.token, expect(function (error) { - test.isTrue(error); - })); + this.token, + expect(error => test.isTrue(error)) + ); }, // verifyEmail called on client with blank token and no callback. function (test, expect) { - test.throws(function(){ - Accounts.verifyEmail(this.token); - }, /Need to pass token/); + test.throws( + () => Accounts.verifyEmail(this.token), + /Need to pass token/ + ); }, ]); @@ -637,9 +629,10 @@ if (Meteor.isClient) (function () { // resetPassword called on client with blank token function (test, expect) { Accounts.resetPassword( - this.token, this.newPassword, expect(function (error) { - test.isTrue(error); - })); + this.token, + this.newPassword, + expect(error => test.isTrue(error)) + ); }, function (test, expect) { // setup @@ -649,15 +642,17 @@ if (Meteor.isClient) (function () { // resetPassword called on client with blank password function (test, expect) { Accounts.resetPassword( - this.token, this.newPassword, expect(function (error) { - test.isTrue(error); - })); + this.token, + this.newPassword, + expect(error => test.isTrue(error)) + ); }, // resetPassword called on client with blank password and no callback. function (test, expect) { - test.throws(function(){ - Accounts.resetPassword(this.token, this.newPassword); - }, /Match error: Expected string, got undefined/); + test.throws( + () => Accounts.resetPassword(this.token, this.newPassword), + /Password may not be empty/ + ); }, ]); @@ -666,7 +661,7 @@ if (Meteor.isClient) (function () { function (test, expect) { // setup this.username = Random.id(); - this.email = Random.id() + '-intercept@example.com'; + this.email = `${Random.id()}-intercept@example.com`; this.password = 'password'; }, // test Accounts.validateNewUser @@ -675,7 +670,7 @@ if (Meteor.isClient) (function () { {username: this.username, password: this.password, // should fail the new user validators profile: {invalid: true}}, - expect(function (error) { + expect(error => { test.equal(error.error, 403); test.equal(error.reason, "User validation failed"); })); @@ -687,11 +682,13 @@ if (Meteor.isClient) (function () { // should fail the new user validator with a special // exception profile: {invalidAndThrowException: true}}, - expect(function (error) { + expect(error => test.equal( error.reason, - "An exception thrown within Accounts.validateNewUser"); - })); + "An exception thrown within Accounts.validateNewUser" + ) + ) + ); }, // test Accounts.onCreateUser function(test, expect) { @@ -722,30 +719,29 @@ if (Meteor.isClient) (function () { // accounts-base/accounts_tests.js, but this is where the tests that // actually log in are. function(test, expect) { - var self = this; - var clientUser = Meteor.user(); - Accounts.connection.call('testMeteorUser', expect(function (err, result) { + const clientUser = Meteor.user(); + Accounts.connection.call('testMeteorUser', expect((err, result) => { test.equal(result._id, clientUser._id); test.equal(result.username, clientUser.username); - test.equal(result.username, self.username); + test.equal(result.username, this.username); test.equal(result.profile.touchedByOnCreateUser, true); test.equal(err, undefined); })); }, function(test, expect) { // Test that even with no published fields, we still have a document. - Accounts.connection.call('clearUsernameAndProfile', expect(function() { + Accounts.connection.call('clearUsernameAndProfile', expect(() => { test.isTrue(Meteor.userId()); - var user = Meteor.user(); + const user = Meteor.user(); test.equal(user, {_id: Meteor.userId()}); })); }, logoutStep, function(test, expect) { - var clientUser = Meteor.user(); + const clientUser = Meteor.user(); test.equal(clientUser, null); test.equal(Meteor.userId(), null); - Accounts.connection.call('testMeteorUser', expect(function (err, result) { + Accounts.connection.call('testMeteorUser', expect((err, result) => { test.equal(err, undefined); test.equal(result, null); })); @@ -784,35 +780,34 @@ if (Meteor.isClient) (function () { // Can't update fields other than profile. Meteor.users.update( this.userId, {$set: {disallowed: true, 'profile.updated': 42}}, - expect(function (err) { + expect(err => { test.isTrue(err); test.equal(err.error, 403); - test.isFalse(_.has(Meteor.user(), 'disallowed')); - test.isFalse(_.has(Meteor.user().profile, 'updated')); + test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user(), 'disallowed')); + test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user().profile, 'updated')); })); }, function(test, expect) { // Can't update another user. Meteor.users.update( this.otherUserId, {$set: {'profile.updated': 42}}, - expect(function (err) { + expect(err => { test.isTrue(err); test.equal(err.error, 403); })); }, function(test, expect) { // Can't update using a non-ID selector. (This one is thrown client-side.) - test.throws(function () { - Meteor.users.update( - {username: this.username}, {$set: {'profile.updated': 42}}); - }); - test.isFalse(_.has(Meteor.user().profile, 'updated')); + test.throws(() => Meteor.users.update( + {username: this.username}, {$set: {'profile.updated': 42}} + )); + test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user().profile, 'updated')); }, function(test, expect) { // Can update own profile using ID. Meteor.users.update( this.userId, {$set: {'profile.updated': 42}}, - expect(function (err) { + expect(err => { test.isFalse(err); test.equal(42, Meteor.user().profile.updated); })); @@ -834,81 +829,69 @@ if (Meteor.isClient) (function () { function (test, expect) { // we can't login with an invalid token - var expectLoginError = expect(function (err) { - test.isTrue(err); - }); + const expectLoginError = expect(err => test.isTrue(err)); Meteor.loginWithToken('invalid', expectLoginError); }, function (test, expect) { // we can login with a valid token - var expectLoginOK = expect(function (err) { - test.isFalse(err); - }); + const expectLoginOK = expect(err => test.isFalse(err)); Meteor.loginWithToken(Accounts._storedLoginToken(), expectLoginOK); }, function (test, expect) { // test logging out invalidates our token - var expectLoginError = expect(function (err) { - test.isTrue(err); - }); - var token = Accounts._storedLoginToken(); + const expectLoginError = expect(err => test.isTrue(err)); + const token = Accounts._storedLoginToken(); test.isTrue(token); - Meteor.logout(function () { - Meteor.loginWithToken(token, expectLoginError); - }); + Meteor.logout(() => Meteor.loginWithToken(token, expectLoginError)); }, function (test, expect) { - var self = this; // Test that login tokens get expired. We should get logged out when a // token expires, and not be able to log in again with the same token. - var expectNoError = expect(function (err) { + const expectNoError = expect(err => { test.isFalse(err); }); - Meteor.loginWithPassword(this.username, this.password, function (error) { - self.token = Accounts._storedLoginToken(); - test.isTrue(self.token); + Meteor.loginWithPassword(this.username, this.password, error => { + this.token = Accounts._storedLoginToken(); + test.isTrue(this.token); expectNoError(error); Accounts.connection.call("expireTokens"); }); }, waitForLoggedOutStep, function (test, expect) { - var token = Accounts._storedLoginToken(); + const token = Accounts._storedLoginToken(); test.isFalse(token); }, function (test, expect) { // Test that once expireTokens is finished, we can't login again with our // previous token. - Meteor.loginWithToken(this.token, expect(function (err, result) { + Meteor.loginWithToken(this.token, expect((err, result) => { test.isTrue(err); test.equal(Meteor.userId(), null); })); }, logoutStep, function (test, expect) { - var self = this; // Test that Meteor.logoutOtherClients logs out a second // authentcated connection while leaving Accounts.connection // logged in. - var secondConn = DDP.connect(Meteor.absoluteUrl()); - var token; + const secondConn = DDP.connect(Meteor.absoluteUrl()); + let token; - var expectSecondConnLoggedOut = expect(function (err, result) { - test.isTrue(err); - }); + const expectSecondConnLoggedOut = + expect((err, result) => test.isTrue(err)); - var expectAccountsConnLoggedIn = expect(function (err, result) { - test.isFalse(err); - }); + const expectAccountsConnLoggedIn = + expect((err, result) => test.isFalse(err)); - var expectSecondConnLoggedIn = expect(function (err, result) { + const expectSecondConnLoggedIn = expect((err, result) => { test.equal(result.token, token); test.isFalse(err); - Meteor.logoutOtherClients(function (err) { + Meteor.logoutOtherClients(err => { test.isFalse(err); secondConn.call('login', { resume: token }, expectSecondConnLoggedOut); @@ -919,9 +902,9 @@ if (Meteor.isClient) (function () { }); Meteor.loginWithPassword( - self.username, - self.password, - expect(function (err) { + this.username, + this.password, + expect(err => { test.isFalse(err); token = Accounts._storedLoginToken(); test.isTrue(token); @@ -936,39 +919,35 @@ if (Meteor.isClient) (function () { // `logoutOtherClients` method. function (test, expect) { - var self = this; - // Test that Meteor.logoutOtherClients logs out a second authenticated // connection while leaving Accounts.connection logged in. - var token; - self.secondConn = DDP.connect(Meteor.absoluteUrl()); + let token; + this.secondConn = DDP.connect(Meteor.absoluteUrl()); - var expectLoginError = expect(function (err) { - test.isTrue(err); - }); - var expectValidToken = expect(function (err, result) { + const expectLoginError = expect(err => test.isTrue(err)); + const expectValidToken = expect((err, result) => { test.isFalse(err); test.isTrue(result); - self.tokenFromLogoutOthers = result.token; + this.tokenFromLogoutOthers = result.token; }); - var expectSecondConnLoggedIn = expect(function (err, result) { + const expectSecondConnLoggedIn = expect((err, result) => { test.equal(result.token, token); test.isFalse(err); // This test will fail if an unrelated reconnect triggers before the // connection is logged out. In general our tests aren't resilient to // mid-test reconnects. - self.secondConn.onReconnect = function () { - self.secondConn.call("login", { resume: token }, expectLoginError); + this.secondConn.onReconnect = () => { + this.secondConn.call("login", { resume: token }, expectLoginError); }; Accounts.connection.call("logoutOtherClients", expectValidToken); }); - Meteor.loginWithPassword(this.username, this.password, expect(function (err) { + Meteor.loginWithPassword(this.username, this.password, expect(err => { test.isFalse(err); token = Accounts._storedLoginToken(); - self.beforeLogoutOthersToken = token; + this.beforeLogoutOthersToken = token; test.isTrue(token); - self.secondConn.call("login", { resume: token }, + this.secondConn.call("login", { resume: token }, expectSecondConnLoggedIn); })); }, @@ -976,13 +955,12 @@ if (Meteor.isClient) (function () { // previous token is no longer valid. waitForLoggedOutStep, function (test, expect) { - var self = this; - var token = Accounts._storedLoginToken(); + const token = Accounts._storedLoginToken(); test.isFalse(token); this.secondConn.close(); Meteor.loginWithToken( - self.beforeLogoutOthersToken, - expect(function (err) { + this.beforeLogoutOthersToken, + expect(err => { test.isTrue(err); test.isFalse(Meteor.userId()); }) @@ -991,25 +969,20 @@ if (Meteor.isClient) (function () { // Test that logoutOtherClients returned a new token that we can use to // log in. function (test, expect) { - var self = this; Meteor.loginWithToken( - self.tokenFromLogoutOthers, - expect(function (err) { + this.tokenFromLogoutOthers, + expect(err => { test.isFalse(err); test.isTrue(Meteor.userId()); }) ); }, logoutStep, - - - function (test, expect) { - var self = this; // Test that deleting a user logs out that user's connections. - Meteor.loginWithPassword(this.username, this.password, expect(function (err) { + Meteor.loginWithPassword(this.username, this.password, expect(err => { test.isFalse(err); - Accounts.connection.call("removeUser", self.username); + Accounts.connection.call("removeUser", this.username); })); }, waitForLoggedOutStep @@ -1030,7 +1003,7 @@ if (Meteor.isClient) (function () { Meteor.loginWithPassword( this.username, this.password, - expect(function (error) { + expect(error => { test.isTrue(error); test.equal(error.reason, "Login forbidden"); }) @@ -1041,7 +1014,7 @@ if (Meteor.isClient) (function () { Meteor.loginWithPassword( "no such user", "some password", - expect(function (error) { + expect(error => { test.isTrue(error); test.equal(error.reason, 'User not found'); }) @@ -1052,7 +1025,7 @@ if (Meteor.isClient) (function () { Meteor.loginWithPassword( "no such user", "some password", - expect(function (error) { + expect(error => { test.isTrue(error); test.equal(error.reason, 'hide actual error'); }) @@ -1063,9 +1036,7 @@ if (Meteor.isClient) (function () { testAsyncMulti("passwords - server onLogin hook", [ function (test, expect) { - Meteor.call("testCaptureLogins", expect(function (error) { - test.isFalse(error); - })); + Meteor.call("testCaptureLogins", expect(error => test.isFalse(error))); }, function (test, expect) { this.username = Random.id(); @@ -1076,29 +1047,27 @@ if (Meteor.isClient) (function () { loggedInAs(this.username, test, expect)); }, function (test, expect) { - var self = this; - Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { + Meteor.call("testFetchCapturedLogins", expect((error, logins) => { test.isFalse(error); test.equal(logins.length, 1); - var login = logins[0]; + const login = logins[0]; test.isTrue(login.successful); - var attempt = login.attempt; + const { attempt } = login; test.equal(attempt.type, "password"); test.isTrue(attempt.allowed); - test.equal(attempt.methodArguments[0].username, self.username); + test.equal(attempt.methodArguments[0].username, this.username); })); } ]); testAsyncMulti("passwords - client onLogin hook", [ function (test, expect) { - var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; - this.onLogin = Accounts.onLogin(function (attempt) { - self.attempt = true; + this.onLogin = Accounts.onLogin(attempt => { + this.attempt = true; }); Accounts.createUser( @@ -1108,15 +1077,13 @@ if (Meteor.isClient) (function () { function (test, expect) { this.onLogin.stop(); test.isTrue(this.attempt); - expect(function () {})(); + expect(() => ({}))(); } ]); testAsyncMulti("passwords - server onLogout hook", [ function (test, expect) { - Meteor.call("testCaptureLogouts", expect(function (error) { - test.isFalse(error); - })); + Meteor.call("testCaptureLogouts", expect(error => test.isFalse(error))); }, function (test, expect) { this.username = Random.id(); @@ -1128,11 +1095,10 @@ if (Meteor.isClient) (function () { }, logoutStep, function (test, expect) { - var self = this; - Meteor.call("testFetchCapturedLogouts", expect(function (error, logouts) { + Meteor.call("testFetchCapturedLogouts", expect((error, logouts) => { test.isFalse(error); test.equal(logouts.length, 1); - var logout = logouts[0]; + const logout = logouts[0]; test.isTrue(logout.successful); })); } @@ -1140,14 +1106,11 @@ if (Meteor.isClient) (function () { testAsyncMulti("passwords - client onLogout hook", [ function (test, expect) { - var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; - this.onLogout = Accounts.onLogout(function () { - self.logoutSuccess = true; - }); + this.onLogout = Accounts.onLogout(() => this.logoutSuccess = true); Accounts.createUser( {username: this.username, password: this.password}, @@ -1171,44 +1134,44 @@ if (Meteor.isClient) (function () { }, logoutStep, function (test, expect) { - Meteor.call("testCaptureLogins", expect(function (error) { - test.isFalse(error); - })); + Meteor.call("testCaptureLogins", expect(error => test.isFalse(error))); }, function (test, expect) { - Meteor.loginWithPassword(this.username, "incorrect", expect(function (error) { - test.isTrue(error); - })); + Meteor.loginWithPassword( + this.username, + "incorrect", + expect(error => test.isTrue(error)) + ); }, function (test, expect) { - Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { + Meteor.call("testFetchCapturedLogins", expect((error, logins) => { test.isFalse(error); test.equal(logins.length, 1); - var login = logins[0]; + const login = logins[0]; test.isFalse(login.successful); - var attempt = login.attempt; + const { attempt } = login; test.equal(attempt.type, "password"); test.isFalse(attempt.allowed); test.equal(attempt.error.reason, "Incorrect password"); })); }, function (test, expect) { - Meteor.call("testCaptureLogins", expect(function (error) { - test.isFalse(error); - })); + Meteor.call("testCaptureLogins", expect(error => test.isFalse(error))); }, function (test, expect) { - Meteor.loginWithPassword("no such user", "incorrect", expect(function (error) { - test.isTrue(error); - })); + Meteor.loginWithPassword( + "no such user", + "incorrect", + expect(error => test.isTrue(error)) + ); }, function (test, expect) { - Meteor.call("testFetchCapturedLogins", expect(function (error, logins) { + Meteor.call("testFetchCapturedLogins", expect((error, logins) => { test.isFalse(error); test.equal(logins.length, 1); - var login = logins[0]; + const login = logins[0]; test.isFalse(login.successful); - var attempt = login.attempt; + const { attempt } = login; test.equal(attempt.type, "password"); test.isFalse(attempt.allowed); test.equal(attempt.error.reason, "User not found"); @@ -1218,14 +1181,11 @@ if (Meteor.isClient) (function () { testAsyncMulti("passwords - client onLoginFailure hook", [ function (test, expect) { - var self = this; this.username = Random.id(); this.password = "password"; this.attempt = false; - this.onLoginFailure = Accounts.onLoginFailure(function () { - self.attempt = true; - }) + this.onLoginFailure = Accounts.onLoginFailure(() => this.attempt = true); Accounts.createUser( {username: this.username, password: this.password}, @@ -1233,19 +1193,19 @@ if (Meteor.isClient) (function () { }, logoutStep, function (test, expect) { - Meteor.call("testCaptureLogins", expect(function (error) { - test.isFalse(error); - })); + Meteor.call("testCaptureLogins", expect(error => test.isFalse(error))); }, function (test, expect) { - Meteor.loginWithPassword(this.username, "incorrect", expect(function (error) { - test.isTrue(error); - })); + Meteor.loginWithPassword( + this.username, + "incorrect", + expect(error => test.isTrue(error)) + ); }, function (test, expect) { this.onLoginFailure.stop(); test.isTrue(this.attempt); - expect(function () {})(); + expect(() => ({}))(); } ]); @@ -1253,35 +1213,42 @@ if (Meteor.isClient) (function () { logoutStep, // Create user with old SRP credentials in the database. function (test, expect) { - var self = this; - Meteor.call("testCreateSRPUser", expect(function (error, result) { + Meteor.call("testCreateSRPUser", expect((error, result) => { test.isFalse(error); - self.username = result; + this.username = result; })); }, // We are able to login with the old style credentials in the database. function (test, expect) { - Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) { - test.isFalse(error); - })); + Meteor.loginWithPassword( + this.username, + 'abcdef', + expect(error => test.isFalse(error)) + ); }, function (test, expect) { - Meteor.call("testSRPUpgrade", this.username, expect(function (error) { - test.isFalse(error); - })); + Meteor.call( + "testSRPUpgrade", + this.username, + expect(error => test.isFalse(error)) + ); }, logoutStep, // After the upgrade to bcrypt we're still able to login. function (test, expect) { - Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) { - test.isFalse(error); - })); + Meteor.loginWithPassword( + this.username, + 'abcdef', + expect(error => test.isFalse(error)) + ); }, logoutStep, function (test, expect) { - Meteor.call("removeUser", this.username, expect(function (error) { - test.isFalse(error); - })); + Meteor.call( + "removeUser", + this.username, + expect(error => test.isFalse(error)) + ); } ]); @@ -1289,10 +1256,9 @@ if (Meteor.isClient) (function () { logoutStep, // Create user with old SRP credentials in the database. function (test, expect) { - var self = this; - Meteor.call("testCreateSRPUser", expect(function (error, result) { + Meteor.call("testCreateSRPUser", expect((error, result) => { test.isFalse(error); - self.username = result; + this.username = result; })); }, // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt. @@ -1300,83 +1266,83 @@ if (Meteor.isClient) (function () { Accounts.callLoginMethod({ methodName: "login", methodArguments: [ { user: { username: this.username }, password: "abcdef" } ], - userCallback: expect(function (err) { - test.isFalse(err); - }) + userCallback: expect(err => test.isFalse(err)) }); }, function (test, expect) { - Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) { - test.isFalse(error); - })); + Meteor.call( + "testNoSRPUpgrade", + this.username, + expect((error) => test.isFalse(error)) + ); }, // Changing our password should upgrade us to bcrypt. function (test, expect) { - Accounts.changePassword("abcdef", "abcdefg", expect(function (error) { - test.isFalse(error); - })); + Accounts.changePassword( + "abcdef", + "abcdefg", + expect(error => test.isFalse(error)) + ); }, function (test, expect) { - Meteor.call("testSRPUpgrade", this.username, expect(function (error) { - test.isFalse(error); - })); + Meteor.call( + "testSRPUpgrade", + this.username, + expect(error => test.isFalse(error)) + ); }, // And after the upgrade we should be able to change our password again. function (test, expect) { - Accounts.changePassword("abcdefg", "abcdef", expect(function (error) { - test.isFalse(error); - })); + Accounts.changePassword( + "abcdefg", + "abcdef", + expect(error => test.isFalse(error)) + ); }, logoutStep ]); }) (); -if (Meteor.isServer) (function () { +if (Meteor.isServer) (() => { - Tinytest.add( - 'passwords - setup more than one onCreateUserHook', - function (test) { - test.throws(function() { - Accounts.onCreateUser(function () {}); - }); - }); + Tinytest.add('passwords - setup more than one onCreateUserHook', test => { + test.throws(() => Accounts.onCreateUser(() => ({}))); + }); - Tinytest.add( - 'passwords - createUser hooks', - function (test) { - var username = Random.id(); - test.throws(function () { - // should fail the new user validators - Accounts.createUser({username: username, profile: {invalid: true}}); - }); + Tinytest.add('passwords - createUser hooks', test => { + const username = Random.id(); + // should fail the new user validators + test.throws(() => Accounts.createUser( + {username: username, profile: {invalid: true}} + )); - var userId = Accounts.createUser({username: username, + const userId = Accounts.createUser({username: username, testOnCreateUserHook: true}); test.isTrue(userId); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); test.equal(user.profile.touchedByOnCreateUser, true); }); Tinytest.add( 'passwords - setPassword', - function (test) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + test => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({username: username, email: email}); + const userId = Accounts.createUser({username: username, email: email}); - var user = Meteor.users.findOne(userId); + let user = Meteor.users.findOne(userId); // no services yet. test.equal(user.services.password, undefined); // set a new password. Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); - var oldSaltedHash = user.services.password.bcrypt; + const oldSaltedHash = user.services.password.bcrypt; test.isTrue(oldSaltedHash); // Send a reset password email (setting a reset token) and insert a login @@ -1389,7 +1355,7 @@ if (Meteor.isServer) (function () { // reset with the same password, see we get a different salted hash Accounts.setPassword(userId, 'new password', {logout: false}); user = Meteor.users.findOne(userId); - var newSaltedHash = user.services.password.bcrypt; + const newSaltedHash = user.services.password.bcrypt; test.isTrue(newSaltedHash); test.notEqual(oldSaltedHash, newSaltedHash); // No more reset token. @@ -1400,7 +1366,7 @@ if (Meteor.isServer) (function () { // reset again, see that the login tokens are gone. Accounts.setPassword(userId, 'new password'); user = Meteor.users.findOne(userId); - var newerSaltedHash = user.services.password.bcrypt; + const newerSaltedHash = user.services.password.bcrypt; test.isTrue(newerSaltedHash); test.notEqual(oldSaltedHash, newerSaltedHash); test.notEqual(newSaltedHash, newerSaltedHash); @@ -1415,11 +1381,9 @@ if (Meteor.isServer) (function () { // This test properly belongs in accounts-base/accounts_tests.js, but // this is where the tests that actually log in are. - Tinytest.add('accounts - user() out of context', function (test) { + Tinytest.add('accounts - user() out of context', test => { // basic server context, no method. - test.throws(function () { - Meteor.user(); - }); + test.throws(() => Meteor.user()); }); // XXX would be nice to test @@ -1427,8 +1391,8 @@ if (Meteor.isServer) (function () { Tinytest.addAsync( 'passwords - login token observes get cleaned up', - function (test, onComplete) { - var username = Random.id(); + (test, onComplete) => { + const username = Random.id(); Accounts.createUser({ username: username, password: 'password' @@ -1436,17 +1400,17 @@ if (Meteor.isServer) (function () { makeTestConnection( test, - function (clientConn, serverConn) { - serverConn.onClose(function () { + (clientConn, serverConn) => { + serverConn.onClose(() => { test.isFalse(Accounts._getUserObserve(serverConn.id)); onComplete(); }); - var result = clientConn.call('login', { + const result = clientConn.call('login', { user: {username: username}, password: 'password' }); test.isTrue(result); - var token = Accounts._getAccountData(serverConn.id, 'loginToken'); + const token = Accounts._getAccountData(serverConn.id, 'loginToken'); test.isTrue(token); // We poll here, instead of just checking `_getUserObserve` @@ -1454,16 +1418,15 @@ if (Meteor.isServer) (function () { // observe, and setting up the observe yields, so we could end // up here before the observe has been set up. simplePoll( - function () { - return !! Accounts._getUserObserve(serverConn.id); - }, - function () { + () => !! Accounts._getUserObserve(serverConn.id), + () => { test.isTrue(Accounts._getUserObserve(serverConn.id)); clientConn.disconnect(); }, - function () { - test.fail("timed out waiting for user observe for connection " + - serverConn.id); + () => { + test.fail( + `timed out waiting for user observe for connection ${serverConn.id}` + ); onComplete(); } ); @@ -1475,66 +1438,71 @@ if (Meteor.isServer) (function () { Tinytest.add( "passwords - reset password doesn't work if email changed after email sent", - function (test) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + test => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({ + const userId = Accounts.createUser({ username: username, email: email, password: "old-password" }); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); Accounts.sendResetPasswordEmail(userId, email); - var resetPasswordEmailOptions = + const resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); - var match = resetPasswordEmailOptions.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const match = resetPasswordEmailOptions.text.match(re); test.isTrue(match); - var resetPasswordToken = match[1]; + const resetPasswordToken = match[1]; - var newEmail = Random.id() + '-new@example.com'; + const newEmail = `${Random.id()}-new@example.com`; Meteor.users.update(userId, {$set: {"emails.0.address": newEmail}}); - test.throws(function () { - Meteor.call("resetPassword", resetPasswordToken, "new-password"); - }, /Token has invalid email address/); - test.throws(function () { - Meteor.call("login", {user: {username: username}, password: "new-password"}); - }, /Incorrect password/); + test.throws( + () => Meteor.call("resetPassword", resetPasswordToken, "new-password"), + /Token has invalid email address/ + ); + test.throws( + () => Meteor.call( + "login", + {user: {username: username}, + password: "new-password"} + ), + /Incorrect password/); }); Tinytest.addAsync( 'passwords - reset password should work when token is not expired', - function (test, onComplete) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + (test, onComplete) => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({ + const userId = Accounts.createUser({ username: username, email: email, password: "old-password" }); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); Accounts.sendResetPasswordEmail(userId, email); - var resetPasswordEmailOptions = + const resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); - var match = resetPasswordEmailOptions.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const match = resetPasswordEmailOptions.text.match(re); test.isTrue(match); - var resetPasswordToken = match[1]; + const resetPasswordToken = match[1]; makeTestConnection( test, - function (clientConn) { + clientConn => { test.isTrue(clientConn.call( "resetPassword", resetPasswordToken, @@ -1547,48 +1515,54 @@ if (Meteor.isServer) (function () { })); onComplete(); - }); + } + ); }); Tinytest.add( 'passwords - reset password should not work when token is expired', - function (test) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + test => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({ + const userId = Accounts.createUser({ username: username, email: email, password: "old-password" }); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); Accounts.sendResetPasswordEmail(userId, email); - var resetPasswordEmailOptions = + const resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"); - var match = resetPasswordEmailOptions.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`); + const match = resetPasswordEmailOptions.text.match(re); test.isTrue(match); - var resetPasswordToken = match[1]; + const resetPasswordToken = match[1]; Meteor.users.update(userId, {$set: {"services.password.reset.when": new Date(Date.now() + -5 * 24 * 3600 * 1000) }}); - test.throws(function () { - Meteor.call("resetPassword", resetPasswordToken, "new-password"); - }, /Token expired/); - test.throws(function () { - Meteor.call("login", {user: {username: username}, password: "new-password"}); - }, /Incorrect password/); + test.throws( + () => Meteor.call("resetPassword", resetPasswordToken, "new-password"), + /Token expired/ + ); + test.throws( + () => Meteor.call( + "login", + {user: {username: username}, + password: "new-password"} + ), + /Incorrect password/); }); Tinytest.add( 'passwords - reset tokens with reasons get cleaned up', - function (test) { - var email = test.id + '-intercept@example.com'; - var userId = Accounts.createUser({email: email, password: 'password'}); + test => { + const email = `${test.id}-intercept@example.com`; + const userId = Accounts.createUser({email: email, password: 'password'}); Accounts.sendResetPasswordEmail(userId, email); test.isTrue(!!Meteor.users.findOne(userId).services.password.reset); @@ -1599,9 +1573,9 @@ if (Meteor.isServer) (function () { Tinytest.add( 'passwords - reset tokens without reasons get cleaned up', - function (test) { - var email = test.id + '-intercept@example.com'; - var userId = Accounts.createUser({email: email, password: 'password'}); + test => { + const email = `${test.id}-intercept@example.com`; + const userId = Accounts.createUser({email: email, password: 'password'}); Accounts.sendResetPasswordEmail(userId, email); Meteor.users.update({_id: userId}, {$unset: {"services.password.reset.reason": 1}}); test.isTrue(!!Meteor.users.findOne(userId).services.password.reset); @@ -1614,30 +1588,30 @@ if (Meteor.isServer) (function () { Tinytest.addAsync( 'passwords - enroll password should work when token is not expired', - function (test, onComplete) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + (test, onComplete) => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({ + const userId = Accounts.createUser({ username: username, email: email }); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); Accounts.sendEnrollmentEmail(userId, email); - var enrollPasswordEmailOptions = + const enrollPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)"); - var match = enrollPasswordEmailOptions.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`); + const match = enrollPasswordEmailOptions.text.match(re); test.isTrue(match); - var enrollPasswordToken = match[1]; + const enrollPasswordToken = match[1]; makeTestConnection( test, - function (clientConn) { + clientConn => { test.isTrue(clientConn.call( "resetPassword", enrollPasswordToken, @@ -1655,56 +1629,54 @@ if (Meteor.isServer) (function () { Tinytest.add( 'passwords - enroll password should not work when token is expired', - function (test) { - var username = Random.id(); - var email = username + '-intercept@example.com'; + test => { + const username = Random.id(); + const email = `${username}-intercept@example.com`; - var userId = Accounts.createUser({ + const userId = Accounts.createUser({ username: username, email: email }); - var user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); Accounts.sendEnrollmentEmail(userId, email); - var enrollPasswordEmailOptions = + const enrollPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0]; - var re = new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)"); - var match = enrollPasswordEmailOptions.text.match(re); + const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`); + const match = enrollPasswordEmailOptions.text.match(re); test.isTrue(match); - var enrollPasswordToken = match[1]; + const enrollPasswordToken = match[1]; - Meteor.users.update(userId, {$set: {"services.password.reset.when": new Date(Date.now() + -35 * 24 * 3600 * 1000) }}); + Meteor.users.update(userId, {$set: {"services.password.reset.when": new Date(Date.now() + -35 * 24 * 3600 * 1000) }}); - test.throws(function () { - Meteor.call("resetPassword", enrollPasswordToken, "new-password"); - }, /Token expired/); + test.throws( + () => Meteor.call("resetPassword", enrollPasswordToken, "new-password"), + /Token expired/ + ); }); - Tinytest.add( - 'passwords - enroll tokens get cleaned up', - function (test) { - var email = test.id + '-intercept@example.com'; - var userId = Accounts.createUser({email: email, password: 'password'}); + Tinytest.add('passwords - enroll tokens get cleaned up', test => { + const email = `${test.id}-intercept@example.com`; + const userId = Accounts.createUser({email: email, password: 'password'}); - Accounts.sendEnrollmentEmail(userId, email); - test.isTrue(!!Meteor.users.findOne(userId).services.password.reset); + Accounts.sendEnrollmentEmail(userId, email); + test.isTrue(!!Meteor.users.findOne(userId).services.password.reset); - Accounts._expirePasswordEnrollTokens(new Date(), userId); - test.isUndefined(Meteor.users.findOne(userId).services.password.reset); - } - ) + Accounts._expirePasswordEnrollTokens(new Date(), userId); + test.isUndefined(Meteor.users.findOne(userId).services.password.reset); + }); Tinytest.add( "passwords - enroll tokens don't get cleaned up when reset tokens are cleaned up", - function (test) { - var email = test.id + '-intercept@example.com'; - var userId = Accounts.createUser({email: email, password: 'password'}); + test => { + const email = `${test.id}-intercept@example.com`; + const userId = Accounts.createUser({email: email, password: 'password'}); Accounts.sendEnrollmentEmail(userId, email); - var enrollToken = Meteor.users.findOne(userId).services.password.reset; + const enrollToken = Meteor.users.findOne(userId).services.password.reset; test.isTrue(enrollToken); Accounts._expirePasswordResetTokens(new Date(), userId); @@ -1714,12 +1686,12 @@ if (Meteor.isServer) (function () { Tinytest.add( "passwords - reset tokens don't get cleaned up when enroll tokens are cleaned up", - function (test) { - var email = test.id + '-intercept@example.com'; - var userId = Accounts.createUser({email: email, password: 'password'}); + test => { + const email = `${test.id}-intercept@example.com`; + const userId = Accounts.createUser({email: email, password: 'password'}); Accounts.sendResetPasswordEmail(userId, email); - var resetToken = Meteor.users.findOne(userId).services.password.reset; + const resetToken = Meteor.users.findOne(userId).services.password.reset; test.isTrue(resetToken); Accounts._expirePasswordEnrollTokens(new Date(), userId); @@ -1728,15 +1700,15 @@ if (Meteor.isServer) (function () { ) // We should be able to change the username - Tinytest.add("passwords - change username", function (test) { - var username = Random.id(); - var userId = Accounts.createUser({ + Tinytest.add("passwords - change username", test => { + const username = Random.id(); + const userId = Accounts.createUser({ username: username }); test.isTrue(userId); - var newUsername = Random.id(); + const newUsername = Random.id(); Accounts.setUsername(userId, newUsername); test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername); @@ -1746,15 +1718,15 @@ if (Meteor.isServer) (function () { }); Tinytest.add("passwords - change username to a new one only differing " + - "in case", function (test) { - var username = Random.id() + "user"; - var userId = Accounts.createUser({ + "in case", test => { + const username = `${Random.id()}user`; + const userId = Accounts.createUser({ username: username.toUpperCase() }); test.isTrue(userId); - var newUsername = username.toLowerCase(); + const newUsername = username.toLowerCase(); Accounts.setUsername(userId, newUsername); test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername); @@ -1763,40 +1735,41 @@ if (Meteor.isServer) (function () { // We should not be able to change the username to one that only // differs in case from an existing one Tinytest.add("passwords - change username should fail when there are " + - "existing users with a username only differing in case", function (test) { - var username = Random.id() + "user"; - var usernameUpper = username.toUpperCase(); + "existing users with a username only differing in case", test => { + const username = `${Random.id()}user`; + const usernameUpper = username.toUpperCase(); - var userId1 = Accounts.createUser({ + const userId1 = Accounts.createUser({ username: username }); - var user2OriginalUsername = Random.id(); - var userId2 = Accounts.createUser({ + const user2OriginalUsername = Random.id(); + const userId2 = Accounts.createUser({ username: user2OriginalUsername }); test.isTrue(userId1); test.isTrue(userId2); - test.throws(function () { - Accounts.setUsername(userId2, usernameUpper); - }, /Username already exists/); + test.throws( + () => Accounts.setUsername(userId2, usernameUpper), + /Username already exists/ + ); test.equal(Accounts._findUserByQuery({id: userId2}).username, user2OriginalUsername); }); - Tinytest.add("passwords - add email", function (test) { - var origEmail = Random.id() + "@turing.com"; - var userId = Accounts.createUser({ + Tinytest.add("passwords - add email", test => { + const origEmail = `${Random.id()}@turing.com`; + const userId = Accounts.createUser({ email: origEmail }); - var newEmail = Random.id() + "@turing.com"; + const newEmail = `${Random.id()}@turing.com`; Accounts.addEmail(userId, newEmail); - var thirdEmail = Random.id() + "@turing.com"; + const thirdEmail = `${Random.id()}@turing.com`; Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ @@ -1810,16 +1783,16 @@ if (Meteor.isServer) (function () { }); Tinytest.add("passwords - add email when the user has an existing email " + - "only differing in case", function (test) { - var origEmail = Random.id() + "@turing.com"; - var userId = Accounts.createUser({ + "only differing in case", test => { + const origEmail = `${Random.id()}@turing.com`; + const userId = Accounts.createUser({ email: origEmail }); - var newEmail = Random.id() + "@turing.com"; + const newEmail = `${Random.id()}@turing.com`; Accounts.addEmail(userId, newEmail); - var thirdEmail = origEmail.toUpperCase(); + const thirdEmail = origEmail.toUpperCase(); Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ @@ -1829,21 +1802,22 @@ if (Meteor.isServer) (function () { }); Tinytest.add("passwords - add email should fail when there is an existing " + - "user with an email only differing in case", function (test) { - var user1Email = Random.id() + "@turing.com"; - var userId1 = Accounts.createUser({ + "user with an email only differing in case", test => { + const user1Email = `${Random.id()}@turing.com`; + const userId1 = Accounts.createUser({ email: user1Email }); - var user2Email = Random.id() + "@turing.com"; - var userId2 = Accounts.createUser({ + const user2Email = `${Random.id()}@turing.com`; + const userId2 = Accounts.createUser({ email: user2Email }); - var dupEmail = user1Email.toUpperCase(); - test.throws(function () { - Accounts.addEmail(userId2, dupEmail); - }, /Email already exists/); + const dupEmail = user1Email.toUpperCase(); + test.throws( + () => Accounts.addEmail(userId2, dupEmail), + /Email already exists/ + ); test.equal(Accounts._findUserByQuery({id: userId1}).emails, [ { address: user1Email, verified: false } @@ -1854,16 +1828,16 @@ if (Meteor.isServer) (function () { ]); }); - Tinytest.add("passwords - remove email", function (test) { - var origEmail = Random.id() + "@turing.com"; - var userId = Accounts.createUser({ + Tinytest.add("passwords - remove email", test => { + const origEmail = `${Random.id()}@turing.com`; + const userId = Accounts.createUser({ email: origEmail }); - var newEmail = Random.id() + "@turing.com"; + const newEmail = `${Random.id()}@turing.com`; Accounts.addEmail(userId, newEmail); - var thirdEmail = Random.id() + "@turing.com"; + const thirdEmail = `${Random.id()}@turing.com`; Accounts.addEmail(userId, thirdEmail, true); test.equal(Accounts._findUserByQuery({id: userId}).emails, [ @@ -1888,10 +1862,10 @@ if (Meteor.isServer) (function () { Tinytest.addAsync( 'passwords - allow custom bcrypt rounds', - function (test, done) { - function getUserHashRounds(user) { - return Number(user.services.password.bcrypt.substring(4, 6)); - } + (test, done) => { + const getUserHashRounds = user => + Number(user.services.password.bcrypt.substring(4, 6)); + // Verify that a bcrypt hash generated for a new account uses the // default number of rounds. diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index 07db15556b..9b34358c21 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -1,10 +1,10 @@ -Accounts.validateNewUser(function (user) { +Accounts.validateNewUser(user => { if (user.profile && user.profile.invalidAndThrowException) throw new Meteor.Error(403, "An exception thrown within Accounts.validateNewUser"); return !(user.profile && user.profile.invalid); }); -Accounts.onCreateUser(function (options, user) { +Accounts.onCreateUser((options, user) => { if (options.testOnCreateUserHook) { user.profile = user.profile || {}; user.profile.touchedByOnCreateUser = true; @@ -16,7 +16,7 @@ Accounts.onCreateUser(function (options, user) { // connection id -> action -var invalidateLogins = {}; +const invalidateLogins = {}; Meteor.methods({ @@ -29,8 +29,8 @@ Meteor.methods({ }); -Accounts.validateLoginAttempt(function (attempt) { - var action = +Accounts.validateLoginAttempt(attempt => { + const action = attempt && attempt.connection && invalidateLogins[attempt.connection.id]; @@ -42,25 +42,26 @@ Accounts.validateLoginAttempt(function (attempt) { else if (action === 'hide') throw new Meteor.Error(403, 'hide actual error'); else - throw new Error('unknown action: ' + action); + throw new Error(`unknown action: ${action}`); }); // connection id -> [{successful: boolean, attempt: object}] -var capturedLogins = {}; +const capturedLogins = {}; +let capturedLogouts = []; Meteor.methods({ testCaptureLogins: function () { capturedLogins[this.connection.id] = []; }, - testCaptureLogouts: function() { + testCaptureLogouts: () => { capturedLogouts = []; }, testFetchCapturedLogins: function () { if (capturedLogins[this.connection.id]) { - var logins = capturedLogins[this.connection.id]; + const logins = capturedLogins[this.connection.id]; delete capturedLogins[this.connection.id]; return logins; } @@ -68,41 +69,37 @@ Meteor.methods({ return []; }, - testFetchCapturedLogouts: function() { - return capturedLogouts; - } + testFetchCapturedLogouts: () => capturedLogouts, }); -Accounts.onLogin(function (attempt) { +Accounts.onLogin(attempt => { if (!attempt.connection) // if login method called from the server return; + const attemptWithoutConnection = { ...attempt }; + delete attemptWithoutConnection.connection; if (capturedLogins[attempt.connection.id]) capturedLogins[attempt.connection.id].push({ successful: true, - attempt: _.omit(attempt, 'connection') + attempt: attemptWithoutConnection, }); }); -Accounts.onLoginFailure(function (attempt) { +Accounts.onLoginFailure(attempt => { if (!attempt.connection) // if login method called from the server return; + const attemptWithoutConnection = { ...attempt }; + delete attemptWithoutConnection.connection; if (capturedLogins[attempt.connection.id]) { capturedLogins[attempt.connection.id].push({ successful: false, - attempt: _.omit(attempt, 'connection') + attempt: attemptWithoutConnection, }); } }); -var capturedLogouts = []; - -Accounts.onLogout(function() { - capturedLogouts.push({ - successful: true - }); -}); +Accounts.onLogout(() => capturedLogouts.push({ successful: true })); // Because this is global state that affects every client, we can't turn // it on and off during the tests. Doing so would mean two simultaneous @@ -122,7 +119,7 @@ Accounts.config({ Meteor.methods({ - testMeteorUser: function () { return Meteor.user(); }, + testMeteorUser: () => Meteor.user(), clearUsernameAndProfile: function () { if (!this.userId) throw new Error("Not logged in!"); @@ -133,19 +130,17 @@ Meteor.methods({ expireTokens: function () { Accounts._expireTokens(new Date(), this.userId); }, - removeUser: function (username) { - Meteor.users.remove({ "username": username }); - } + removeUser: username => Meteor.users.remove({ "username": username }), }); // Create a user that had previously logged in with SRP. Meteor.methods({ - testCreateSRPUser: function () { - var username = Random.id(); + testCreateSRPUser: () => { + const username = Random.id(); Meteor.users.remove({username: username}); - var userId = Accounts.createUser({username: username}); + const userId = Accounts.createUser({username: username}); Meteor.users.update( userId, { '$set': { 'services.password.srp': { @@ -157,16 +152,16 @@ Meteor.methods({ return username; }, - testSRPUpgrade: function (username) { - var user = Meteor.users.findOne({username: username}); + testSRPUpgrade: username => { + const user = Meteor.users.findOne({username: username}); if (user.services && user.services.password && user.services.password.srp) throw new Error("srp wasn't removed"); if (!(user.services && user.services.password && user.services.password.bcrypt)) throw new Error("bcrypt wasn't added"); }, - testNoSRPUpgrade: function (username) { - var user = Meteor.users.findOne({username: username}); + testNoSRPUpgrade: username => { + const user = Meteor.users.findOne({username: username}); if (user.services && user.services.password && user.services.password.bcrypt) throw new Error("bcrypt was added"); if (user.services && user.services.password && ! user.services.password.srp) diff --git a/packages/accounts-twitter/notice.js b/packages/accounts-twitter/notice.js index 0a4b630e8f..aadc910e31 100644 --- a/packages/accounts-twitter/notice.js +++ b/packages/accounts-twitter/notice.js @@ -1,6 +1,6 @@ if (Package['accounts-ui'] && !Package['service-configuration'] - && !Package.hasOwnProperty('twitter-config-ui')) { + && !Object.prototype.hasOwnProperty.call(Package, 'twitter-config-ui')) { console.warn( "Note: You're using accounts-ui and accounts-twitter,\n" + "but didn't install the configuration UI for Twitter\n" + diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index 034d0a8f98..196726d96b 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -1,11 +1,10 @@ Package.describe({ summary: "Login service for Twitter accounts", - version: "1.4.1" + version: "1.4.2", }); -Package.onUse(function(api) { +Package.onUse(api => { api.use('ecmascript'); - api.use('underscore', ['server']); api.use('accounts-base', ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); diff --git a/packages/accounts-twitter/twitter.js b/packages/accounts-twitter/twitter.js index 1d44f474f2..616f925050 100644 --- a/packages/accounts-twitter/twitter.js +++ b/packages/accounts-twitter/twitter.js @@ -1,25 +1,25 @@ Accounts.oauth.registerService('twitter'); if (Meteor.isClient) { - const loginWithTwitter = function(options, callback) { + const loginWithTwitter = (options, callback) => { // support a callback without options if (! callback && typeof options === "function") { callback = options; options = null; } - var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); Twitter.requestCredential(options, credentialRequestCompleteCallback); }; Accounts.registerClientLoginFunction('twitter', loginWithTwitter); - Meteor.loginWithTwitter = function () { - return Accounts.applyLoginFunction('twitter', arguments); - }; + Meteor.loginWithTwitter = (...args) => + Accounts.applyLoginFunction('twitter', args); } else { - var autopublishedFields = _.map( + const autopublishedFields = // don't send access token. https://dev.twitter.com/discussions/5025 - Twitter.whitelistedFields.concat(['id', 'screenName']), - function (subfield) { return 'services.twitter.' + subfield; }); + Twitter.whitelistedFields.concat(['id', 'screenName']).map( + subfield => `services.twitter.${subfield}` + ); Accounts.addAutopublishFields({ forLoggedInUser: autopublishedFields, diff --git a/packages/accounts-ui-unstyled/accounts_ui.js b/packages/accounts-ui-unstyled/accounts_ui.js index a696d98608..87c96a82fb 100644 --- a/packages/accounts-ui-unstyled/accounts_ui.js +++ b/packages/accounts-ui-unstyled/accounts_ui.js @@ -24,37 +24,42 @@ Accounts.ui._options = { * @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default). * @importFromPackage accounts-base */ -Accounts.ui.config = function(options) { +Accounts.ui.config = options => { // validate options keys - var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; - _.each(_.keys(options), function (key) { - if (!_.contains(VALID_KEYS, key)) - throw new Error("Accounts.ui.config: Invalid key: " + key); + const VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; + Object.keys(options).forEach(key => { + if (!VALID_KEYS.includes(key)) + throw new Error(`Accounts.ui.config: Invalid key: ${key}`); }); // deal with `passwordSignupFields` if (options.passwordSignupFields) { - if (_.contains([ - "USERNAME_AND_EMAIL", - "USERNAME_AND_OPTIONAL_EMAIL", - "USERNAME_ONLY", - "EMAIL_ONLY" - ], options.passwordSignupFields)) { + if (options.passwordSignupFields.reduce((prev, field) => + prev && + [ + "USERNAME_AND_EMAIL", + "USERNAME_AND_OPTIONAL_EMAIL", + "USERNAME_ONLY", + "EMAIL_ONLY" + ].includes(field), + true + )) { if (Accounts.ui._options.passwordSignupFields) throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); else Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; } else { - throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); + throw new Error(`Accounts.ui.config: Invalid option for \`passwordSignupFields\`: ${options.passwordSignupFields}`); } } // deal with `requestPermissions` if (options.requestPermissions) { - _.each(options.requestPermissions, function (scope, service) { + Object.keys(options.requestPermissions).forEach(service => { + const scope = options.forceApprovalPrompt[service]; if (Accounts.ui._options.requestPermissions[service]) { - throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); - } else if (!(scope instanceof Array)) { + throw new Error(`Accounts.ui.config: Can't set \`requestPermissions\` more than once for ${service}`); + } else if (!Array.isArray(scope)) { throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); } else { Accounts.ui._options.requestPermissions[service] = scope; @@ -64,12 +69,13 @@ Accounts.ui.config = function(options) { // deal with `requestOfflineToken` if (options.requestOfflineToken) { - _.each(options.requestOfflineToken, function (value, service) { + Object.keys(options.requestOfflineToken).forEach(service => { + const value = options.forceApprovalPrompt[service]; if (service !== 'google') throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); if (Accounts.ui._options.requestOfflineToken[service]) { - throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); + throw new Error(`Accounts.ui.config: Can't set \`requestOfflineToken\` more than once for ${service}`); } else { Accounts.ui._options.requestOfflineToken[service] = value; } @@ -78,12 +84,13 @@ Accounts.ui.config = function(options) { // deal with `forceApprovalPrompt` if (options.forceApprovalPrompt) { - _.each(options.forceApprovalPrompt, function (value, service) { + Object.keys(options.forceApprovalPrompt).forEach(service => { + const value = options.forceApprovalPrompt[service]; if (service !== 'google') throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); if (Accounts.ui._options.forceApprovalPrompt[service]) { - throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); + throw new Error(`Accounts.ui.config: Can't set \`forceApprovalPrompt\` more than once for ${service}`); } else { Accounts.ui._options.forceApprovalPrompt[service] = value; } @@ -91,7 +98,13 @@ Accounts.ui.config = function(options) { } }; -passwordSignupFields = function () { - return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; -}; - +export const passwordSignupFields = () => { + const { passwordSignupFields } = Accounts.ui._options; + if (Array.isArray(passwordSignupFields)) { + return passwordSignupFields; + } else if (typeof passwordSignupFields === 'string') { + return [passwordSignupFields]; + } + return ["EMAIL_ONLY"]; +} + \ No newline at end of file diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js index 5e4bef9ea0..b42cab9072 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -7,20 +7,18 @@ // XXX it'd be cool to also test that the right thing happens if options // *are* validated, but Accounts.ui._options is global state which makes this hard // (impossible?) -Tinytest.add('accounts-ui - config validates keys', function (test) { - test.throws(function () { - Accounts.ui.config({foo: "bar"}); - }); +Tinytest.add('accounts-ui - config validates keys', test => { + test.throws(() => Accounts.ui.config({foo: "bar"})); - test.throws(function () { - Accounts.ui.config({passwordSignupFields: "not a valid option"}); - }); + test.throws( + () => Accounts.ui.config({passwordSignupFields: "not a valid option"}) + ); - test.throws(function () { - Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); - }); + test.throws( + () => Accounts.ui.config({requestPermissions: {facebook: "not an array"}}) + ); - test.throws(function () { - Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); - }); + test.throws( + () => Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}) + ); }); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index e13068fc56..9a9f626de6 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -1,16 +1,15 @@ +import { passwordSignupFields } from './accounts_ui.js'; + // for convenience -var loginButtonsSession = Accounts._loginButtonsSession; +const loginButtonsSession = Accounts._loginButtonsSession; // shared between dropdown and single mode Template.loginButtons.events({ - 'click #login-buttons-logout': function() { - Meteor.logout(function () { - loginButtonsSession.closeDropdown(); - }); - } + 'click #login-buttons-logout': () => + Meteor.logout(() => loginButtonsSession.closeDropdown()), }); -Template.registerHelper('loginButtons', function () { +Template.registerHelper('loginButtons', () => { throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}"); }); @@ -18,8 +17,8 @@ Template.registerHelper('loginButtons', function () { // helpers // -displayName = function () { - var user = Meteor.user(); +export const displayName = () => { + const user = Meteor.user(); if (!user) return ''; @@ -43,11 +42,9 @@ displayName = function () { // NOTE: It is very important to have this return password last // because of the way we render the different providers in // login_buttons_dropdown.html -getLoginServices = function () { - var self = this; - +export const getLoginServices = () => { // First look for OAuth services. - var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; + const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; // Be equally kind to all login services. This also preserves // backwards-compatibility. (But maybe order should be @@ -58,24 +55,19 @@ getLoginServices = function () { if (hasPasswordService()) services.push('password'); - return _.map(services, function(name) { - return {name: name}; - }); + return services.map(name => ({ name })); }; -hasPasswordService = function () { - return !!Package['accounts-password']; -}; +export const hasPasswordService = () => !!Package['accounts-password']; -dropdown = function () { - return hasPasswordService() || getLoginServices().length > 1; -}; +export const dropdown = () => + hasPasswordService() || getLoginServices().length > 1; // XXX improve these. should this be in accounts-password instead? // // XXX these will become configurable, and will be validated on // the server as well. -validateUsername = function (username) { +export const validateUsername = username => { if (username.length >= 3) { return true; } else { @@ -83,18 +75,20 @@ validateUsername = function (username) { return false; } }; -validateEmail = function (email) { + +export const validateEmail = email => { if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') return true; - if (email.indexOf('@') !== -1) { + if (email.includes('@')) { return true; } else { loginButtonsSession.errorMessage("Invalid email"); return false; } }; -validatePassword = function (password) { + +export const validatePassword = password => { if (password.length >= 6) { return true; } else { @@ -108,18 +102,16 @@ validatePassword = function (password) { // Template._loginButtonsLoggedOut.helpers({ - dropdown: dropdown, + dropdown, services: getLoginServices, - singleService: function () { - var services = getLoginServices(); + singleService: () => { + const services = getLoginServices(); if (services.length !== 1) throw new Error( "Shouldn't be rendering this template with more than one configured service"); return services[0]; }, - configurationLoaded: function () { - return Accounts.loginServicesConfigured(); - } + configurationLoaded: () => Accounts.loginServicesConfigured(), }); @@ -129,9 +121,7 @@ Template._loginButtonsLoggedOut.helpers({ // decide whether we should show a dropdown rather than a row of // buttons -Template._loginButtonsLoggedIn.helpers({ - dropdown: dropdown -}); +Template._loginButtonsLoggedIn.helpers({ dropdown }); @@ -139,9 +129,7 @@ Template._loginButtonsLoggedIn.helpers({ // loginButtonsLoggedInSingleLogoutButton template // -Template._loginButtonsLoggedInSingleLogoutButton.helpers({ - displayName: displayName -}); +Template._loginButtonsLoggedInSingleLogoutButton.helpers({ displayName }); @@ -150,15 +138,11 @@ Template._loginButtonsLoggedInSingleLogoutButton.helpers({ // Template._loginButtonsMessages.helpers({ - errorMessage: function () { - return loginButtonsSession.get('errorMessage'); - } + errorMessage: () => loginButtonsSession.get('errorMessage'), }); Template._loginButtonsMessages.helpers({ - infoMessage: function () { - return loginButtonsSession.get('infoMessage'); - } + infoMessage: () => loginButtonsSession.get('infoMessage'), }); @@ -166,6 +150,4 @@ Template._loginButtonsMessages.helpers({ // loginButtonsLoggingInPadding template // -Template._loginButtonsLoggingInPadding.helpers({ - dropdown: dropdown -}); +Template._loginButtonsLoggingInPadding.helpers({ dropdown }); diff --git a/packages/accounts-ui-unstyled/login_buttons_dialogs.html b/packages/accounts-ui-unstyled/login_buttons_dialogs.html index c6582d0a2d..caf78c0a22 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dialogs.html +++ b/packages/accounts-ui-unstyled/login_buttons_dialogs.html @@ -14,13 +14,27 @@ {{#if inResetPasswordFlow}}
-
+
+ + + + +
- +
{{> _loginButtonsMessages}} @@ -30,7 +44,7 @@
× - + {{/if}} @@ -48,13 +62,27 @@ {{#if inEnrollAccountFlow}}
-
+
+ + + + {{> _loginButtonsMessages}} @@ -64,7 +92,7 @@
× - + {{/if}} diff --git a/packages/accounts-ui-unstyled/login_buttons_dialogs.js b/packages/accounts-ui-unstyled/login_buttons_dialogs.js index 152781fb03..370d9b0d27 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dialogs.js +++ b/packages/accounts-ui-unstyled/login_buttons_dialogs.js @@ -1,22 +1,23 @@ +import { displayName, dropdown, validatePassword } from './login_buttons.js'; // for convenience -var loginButtonsSession = Accounts._loginButtonsSession; +const loginButtonsSession = Accounts._loginButtonsSession; // since we don't want to pass around the callback that we get from our event // handlers, we just make it a variable for the whole file -var doneCallback; +let doneCallback; -Accounts.onResetPasswordLink(function (token, done) { +Accounts.onResetPasswordLink((token, done) => { loginButtonsSession.set("resetPasswordToken", token); doneCallback = done; }); -Accounts.onEnrollmentLink(function (token, done) { +Accounts.onEnrollmentLink((token, done) => { loginButtonsSession.set("enrollAccountToken", token); doneCallback = done; }); -Accounts.onEmailVerificationLink(function (token, done) { - Accounts.verifyEmail(token, function (error) { +Accounts.onEmailVerificationLink((token, done) => { + Accounts.verifyEmail(token, error => { if (! error) { loginButtonsSession.set('justVerifiedEmail', true); } @@ -32,29 +33,27 @@ Accounts.onEmailVerificationLink(function (token, done) { // Template._resetPasswordDialog.events({ - 'click #login-buttons-reset-password-button': function () { - resetPassword(); - }, - 'keypress #reset-password-new-password': function (event) { + 'click #login-buttons-reset-password-button': () => resetPassword(), + 'keypress #reset-password-new-password': event => { if (event.keyCode === 13) resetPassword(); }, - 'click #login-buttons-cancel-reset-password': function () { + 'click #login-buttons-cancel-reset-password': () => { loginButtonsSession.set('resetPasswordToken', null); if (doneCallback) doneCallback(); } }); -var resetPassword = function () { +const resetPassword = () => { loginButtonsSession.resetMessages(); - var newPassword = document.getElementById('reset-password-new-password').value; + const newPassword = document.getElementById('reset-password-new-password').value; if (!validatePassword(newPassword)) return; Accounts.resetPassword( loginButtonsSession.get('resetPasswordToken'), newPassword, - function (error) { + error => { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { @@ -67,9 +66,8 @@ var resetPassword = function () { }; Template._resetPasswordDialog.helpers({ - inResetPasswordFlow: function () { - return loginButtonsSession.get('resetPasswordToken'); - } + displayName, + inResetPasswordFlow: () => loginButtonsSession.get('resetPasswordToken'), }); // @@ -77,16 +75,13 @@ Template._resetPasswordDialog.helpers({ // Template._justResetPasswordDialog.events({ - 'click #just-verified-dismiss-button': function () { - loginButtonsSession.set('justResetPassword', false); - } + 'click #just-verified-dismiss-button': () => + loginButtonsSession.set('justResetPassword', false), }); Template._justResetPasswordDialog.helpers({ - visible: function () { - return loginButtonsSession.get('justResetPassword'); - }, - displayName: displayName + visible: () => loginButtonsSession.get('justResetPassword'), + displayName, }); @@ -95,30 +90,15 @@ Template._justResetPasswordDialog.helpers({ // enrollAccountDialog template // -Template._enrollAccountDialog.events({ - 'click #login-buttons-enroll-account-button': function () { - enrollAccount(); - }, - 'keypress #enroll-account-password': function (event) { - if (event.keyCode === 13) - enrollAccount(); - }, - 'click #login-buttons-cancel-enroll-account': function () { - loginButtonsSession.set('enrollAccountToken', null); - if (doneCallback) - doneCallback(); - } -}); - -var enrollAccount = function () { +const enrollAccount = () => { loginButtonsSession.resetMessages(); - var password = document.getElementById('enroll-account-password').value; + const password = document.getElementById('enroll-account-password').value; if (!validatePassword(password)) return; Accounts.resetPassword( loginButtonsSession.get('enrollAccountToken'), password, - function (error) { + error => { if (error) { loginButtonsSession.errorMessage(error.reason || "Unknown error"); } else { @@ -129,28 +109,37 @@ var enrollAccount = function () { }); }; -Template._enrollAccountDialog.helpers({ - inEnrollAccountFlow: function () { - return loginButtonsSession.get('enrollAccountToken'); +Template._enrollAccountDialog.events({ + 'click #login-buttons-enroll-account-button': enrollAccount, + 'keypress #enroll-account-password': event => { + if (event.keyCode === 13) + enrollAccount(); + }, + 'click #login-buttons-cancel-enroll-account': () => { + loginButtonsSession.set('enrollAccountToken', null); + if (doneCallback) + doneCallback(); } }); +Template._enrollAccountDialog.helpers({ + displayName, + inEnrollAccountFlow: () => loginButtonsSession.get('enrollAccountToken'), +}); + // // justVerifiedEmailDialog template // Template._justVerifiedEmailDialog.events({ - 'click #just-verified-dismiss-button': function () { - loginButtonsSession.set('justVerifiedEmail', false); - } + 'click #just-verified-dismiss-button': () => + loginButtonsSession.set('justVerifiedEmail', false), }); Template._justVerifiedEmailDialog.helpers({ - visible: function () { - return loginButtonsSession.get('justVerifiedEmail'); - }, - displayName: displayName + visible: () => loginButtonsSession.get('justVerifiedEmail'), + displayName, }); @@ -159,14 +148,13 @@ Template._justVerifiedEmailDialog.helpers({ // Template._loginButtonsMessagesDialog.events({ - 'click #messages-dialog-dismiss-button': function () { - loginButtonsSession.resetMessages(); - } + 'click #messages-dialog-dismiss-button': () => + loginButtonsSession.resetMessages(), }); Template._loginButtonsMessagesDialog.helpers({ - visible: function () { - var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); + visible: () => { + const hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); return !dropdown() && hasMessage; } }); @@ -177,42 +165,40 @@ Template._loginButtonsMessagesDialog.helpers({ // Template._configureLoginServiceDialog.events({ - 'click .configure-login-service-dismiss-button': function () { - loginButtonsSession.set('configureLoginServiceDialogVisible', false); - }, - 'click #configure-login-service-dialog-save-configuration': function () { + 'click .configure-login-service-dismiss-button': () => + loginButtonsSession.set('configureLoginServiceDialogVisible', false), + 'click #configure-login-service-dialog-save-configuration': () => { if (loginButtonsSession.get('configureLoginServiceDialogVisible') && ! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) { // Prepare the configuration document for this login service - var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); - var configuration = { + const serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); + const configuration = { service: serviceName }; // Fetch the value of each input field - _.each(configurationFields(), function(field) { + configurationFields().forEach(field => { configuration[field.property] = document.getElementById( - 'configure-login-service-dialog-' + field.property).value + `configure-login-service-dialog-${field.property}`).value .replace(/^\s*|\s*$/g, ""); // trim() doesnt work on IE8; }); - Array.prototype.some.call( - document.getElementById("configure-login-service-dialog") - .getElementsByTagName("input"), - function (input) { - if (input.getAttribute("name") === "loginStyle" && - input.checked) { - configuration.loginStyle = input.value; - return true; - } - } + // Replacement of single use of jQuery in this package so we can remove + // the dependency + const inputs = [].slice.call( // Because HTMLCollections aren't arrays + document + .getElementById('configure-login-service-dialog') + .getElementsByTagName('input') ); + configuration.loginStyle = + document.querySelector('#configure-login-service-dialog input[name="loginStyle"]:checked').value; + // Configure this login service Accounts.connection.call( - "configureLoginService", configuration, function (error, result) { + "configureLoginService", configuration, (error, result) => { if (error) - Meteor._debug("Error configuring login service " + serviceName, + Meteor._debug(`Error configuring login service ${serviceName}`, error); else loginButtonsSession.set('configureLoginServiceDialogVisible', @@ -223,7 +209,7 @@ Template._configureLoginServiceDialog.events({ // IE8 doesn't support the 'input' event, so we'll run this on the keyup as // well. (Keeping the 'input' event means that this also fires when you use // the mouse to change the contents of the field, eg 'Cut' menu item.) - 'input, keyup input': function (event) { + 'input, keyup input': event => { // if the event fired on one of the configuration input fields, // check whether we should enable the 'save configuration' button if (event.target.id.indexOf('configure-login-service-dialog') === 0) @@ -234,62 +220,55 @@ Template._configureLoginServiceDialog.events({ // check whether the 'save configuration' button should be enabled. // this is a really strange way to implement this and a Forms // Abstraction would make all of this reactive, and simpler. -var updateSaveDisabled = function () { - var anyFieldEmpty = _.any(configurationFields(), function(field) { - return document.getElementById( - 'configure-login-service-dialog-' + field.property).value === ''; - }); +const updateSaveDisabled = () => { + const anyFieldEmpty = configurationFields().reduce((prev, field) => + prev || document.getElementById( + `configure-login-service-dialog-${field.property}` + ).value === '', + false + ); loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty); }; // Returns the appropriate template for this login service. This // template should be defined in the service's package -Template._configureLoginServiceDialog.templateForService = function(serviceName) { +Template._configureLoginServiceDialog.templateForService = serviceName => { serviceName = serviceName || loginButtonsSession.get('configureLoginServiceDialogServiceName'); // XXX Service providers should be able to specify their configuration // template name. - return Template['configureLoginServiceDialogFor' + - (serviceName === 'meteor-developer' ? + return Template[`configureLoginServiceDialogFor${ + serviceName === 'meteor-developer' ? 'MeteorDeveloper' : - capitalize(serviceName))]; + capitalize(serviceName)}`]; }; -var configurationFields = function () { - var template = Template._configureLoginServiceDialog.templateForService(); +const configurationFields = () => { + const template = Template._configureLoginServiceDialog.templateForService(); return template.fields(); }; Template._configureLoginServiceDialog.helpers({ - configurationFields: function () { - return configurationFields(); - }, - visible: function () { - return loginButtonsSession.get('configureLoginServiceDialogVisible'); - }, - configurationSteps: function () { - // renders the appropriate template - return Template._configureLoginServiceDialog.templateForService(); - }, - saveDisabled: function () { - return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'); - } + configurationFields, + visible: () => loginButtonsSession.get('configureLoginServiceDialogVisible'), + // renders the appropriate template + configurationSteps: () => + Template._configureLoginServiceDialog.templateForService(), + saveDisabled: () => + loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'), }); // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js -var capitalize = function(str){ +const capitalize = str => { str = str == null ? '' : String(str); return str.charAt(0).toUpperCase() + str.slice(1); }; Template._configureLoginOnDesktopDialog.helpers({ - visible: function () { - return loginButtonsSession.get('configureOnDesktopVisible'); - } + visible: () => loginButtonsSession.get('configureOnDesktopVisible'), }); Template._configureLoginOnDesktopDialog.events({ - 'click #configure-on-desktop-dismiss-button': function () { - loginButtonsSession.set('configureOnDesktopVisible', false); - } + 'click #configure-on-desktop-dismiss-button': () => + loginButtonsSession.set('configureOnDesktopVisible', false), }); diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.html b/packages/accounts-ui-unstyled/login_buttons_dropdown.html index 6b1f11b756..358b71ec99 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.html +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.html @@ -137,10 +137,10 @@ diff --git a/packages/meteor-developer-config-ui/meteor_developer_configure.js b/packages/meteor-developer-config-ui/meteor_developer_configure.js index 3828856bda..ed4c1af044 100644 --- a/packages/meteor-developer-config-ui/meteor_developer_configure.js +++ b/packages/meteor-developer-config-ui/meteor_developer_configure.js @@ -1,12 +1,8 @@ Template.configureLoginServiceDialogForMeteorDeveloper.helpers({ - siteUrl: function () { - return Meteor.absoluteUrl(); - } + siteUrl: () => Meteor.absoluteUrl(), }); -Template.configureLoginServiceDialogForMeteorDeveloper.fields = function () { - return [ - {property: 'clientId', label: 'App ID'}, - {property: 'secret', label: 'App secret'} - ]; -}; +Template.configureLoginServiceDialogForMeteorDeveloper.fields = () => [ + {property: 'clientId', label: 'App ID'}, + {property: 'secret', label: 'App secret'} +]; diff --git a/packages/meteor-developer-config-ui/package.js b/packages/meteor-developer-config-ui/package.js index 3b1d990246..de9bd8d557 100644 --- a/packages/meteor-developer-config-ui/package.js +++ b/packages/meteor-developer-config-ui/package.js @@ -1,9 +1,10 @@ Package.describe({ summary: 'Blaze configuration templates for the Meteor developer accounts OAuth.', - version: '1.0.0' + version: '1.0.1' }); -Package.onUse(function (api) { +Package.onUse(api => { + api.use('ecmascript', 'client'); api.use('templating@1.2.13', 'client'); api.addFiles('meteor_developer_login_button.css', 'client'); api.addFiles( diff --git a/packages/meteor-developer-oauth/meteor_developer_client.js b/packages/meteor-developer-oauth/meteor_developer_client.js index d281533cbd..bd5c5688d3 100644 --- a/packages/meteor-developer-oauth/meteor_developer_client.js +++ b/packages/meteor-developer-oauth/meteor_developer_client.js @@ -2,14 +2,14 @@ // @param credentialRequestCompleteCallback {Function} Callback function to call on // completion. Takes one argument, credentialToken on success, or Error on // error. -var requestCredential = function (options, credentialRequestCompleteCallback) { +const requestCredential = (options, credentialRequestCompleteCallback) => { // support a callback without options if (! credentialRequestCompleteCallback && typeof options === "function") { credentialRequestCompleteCallback = options; options = null; } - var config = ServiceConfiguration.configurations.findOne({ + const config = ServiceConfiguration.configurations.findOne({ service: 'meteor-developer' }); if (!config) { @@ -18,16 +18,16 @@ var requestCredential = function (options, credentialRequestCompleteCallback) { return; } - var credentialToken = Random.secret(); + const credentialToken = Random.secret(); - var loginStyle = OAuth._loginStyle('meteor-developer', config, options); + const loginStyle = OAuth._loginStyle('meteor-developer', config, options); - var loginUrl = + let loginUrl = MeteorDeveloperAccounts._server + "/oauth2/authorize?" + - "state=" + OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl) + + `state=${OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl)}` + "&response_type=code&" + - "client_id=" + config.clientId; + `client_id=${config.clientId}`; /** * @deprecated in 1.3.0 @@ -38,17 +38,17 @@ var requestCredential = function (options, credentialRequestCompleteCallback) { } if (options && options.loginHint) { - loginUrl += '&user_email=' + encodeURIComponent(options.loginHint); + loginUrl += `&user_email=${encodeURIComponent(options.loginHint)}`; } - loginUrl += "&redirect_uri=" + OAuth._redirectUri('meteor-developer', config); + loginUrl += `&redirect_uri=${OAuth._redirectUri('meteor-developer', config)}`; OAuth.launchLogin({ loginService: "meteor-developer", - loginStyle: loginStyle, - loginUrl: loginUrl, - credentialRequestCompleteCallback: credentialRequestCompleteCallback, - credentialToken: credentialToken, + loginStyle, + loginUrl, + credentialRequestCompleteCallback, + credentialToken, popupOptions: {width: 470, height: 490} }); }; diff --git a/packages/meteor-developer-oauth/meteor_developer_common.js b/packages/meteor-developer-oauth/meteor_developer_common.js index 57c1361210..4202c73a2b 100644 --- a/packages/meteor-developer-oauth/meteor_developer_common.js +++ b/packages/meteor-developer-oauth/meteor_developer_common.js @@ -4,7 +4,7 @@ MeteorDeveloperAccounts._server = "https://www.meteor.com"; // Options are: // - developerAccountsServer: defaults to "https://www.meteor.com" -MeteorDeveloperAccounts._config = function (options) { +MeteorDeveloperAccounts._config = options => { if (options.developerAccountsServer) { MeteorDeveloperAccounts._server = options.developerAccountsServer; } diff --git a/packages/meteor-developer-oauth/meteor_developer_server.js b/packages/meteor-developer-oauth/meteor_developer_server.js index 77cc7c966f..c563ba47e8 100644 --- a/packages/meteor-developer-oauth/meteor_developer_server.js +++ b/packages/meteor-developer-oauth/meteor_developer_server.js @@ -1,14 +1,14 @@ -OAuth.registerService("meteor-developer", 2, null, function (query) { - var response = getTokens(query); - var accessToken = response.accessToken; - var identity = getIdentity(accessToken); +OAuth.registerService("meteor-developer", 2, null, query => { + const response = getTokens(query); + const { accessToken } = response; + const identity = getIdentity(accessToken); - var serviceData = { + const serviceData = { accessToken: OAuth.sealSecret(accessToken), expiresAt: (+new Date) + (1000 * response.expiresIn) }; - _.extend(serviceData, identity); + Object.assign(serviceData, identity); // only set the token in serviceData if it's there. this ensures // that we don't lose old ones (since we only get this on the first @@ -17,7 +17,7 @@ OAuth.registerService("meteor-developer", 2, null, function (query) { serviceData.refreshToken = OAuth.sealSecret(response.refreshToken); return { - serviceData: serviceData, + serviceData, options: {profile: {name: serviceData.username}} // XXX use username for name until meteor accounts has a profile with a name }; @@ -28,14 +28,14 @@ OAuth.registerService("meteor-developer", 2, null, function (query) { // - expiresIn: lifetime of token in seconds // - refreshToken, if this is the first authorization request and we got a // refresh token from the server -var getTokens = function (query) { - var config = ServiceConfiguration.configurations.findOne({ +const getTokens = query => { + const config = ServiceConfiguration.configurations.findOne({ service: 'meteor-developer' }); if (!config) throw new ServiceConfiguration.ConfigError(); - var response; + let response; try { response = HTTP.post( MeteorDeveloperAccounts._server + "/oauth2/token", { @@ -49,7 +49,7 @@ var getTokens = function (query) { } ); } catch (err) { - throw _.extend( + throw Object.assign( new Error( "Failed to complete OAuth handshake with Meteor developer accounts. " + err.message @@ -74,16 +74,16 @@ var getTokens = function (query) { } }; -var getIdentity = function (accessToken) { +const getIdentity = accessToken => { try { return HTTP.get( - MeteorDeveloperAccounts._server + "/api/v1/identity", + `${MeteorDeveloperAccounts._server}/api/v1/identity`, { - headers: { Authorization: "Bearer " + accessToken } + headers: { Authorization: `Bearer ${accessToken}`} } ).data; } catch (err) { - throw _.extend( + throw Object.assign( new Error("Failed to fetch identity from Meteor developer accounts. " + err.message), {response: err.response} @@ -91,7 +91,6 @@ var getIdentity = function (accessToken) { } }; -MeteorDeveloperAccounts.retrieveCredential = function (credentialToken, - credentialSecret) { - return OAuth.retrieveCredential(credentialToken, credentialSecret); -}; +MeteorDeveloperAccounts.retrieveCredential = + (credentialToken, credentialSecret) => + OAuth.retrieveCredential(credentialToken, credentialSecret); diff --git a/packages/meteor-developer-oauth/package.js b/packages/meteor-developer-oauth/package.js index 048249d4fc..02702b8dec 100644 --- a/packages/meteor-developer-oauth/package.js +++ b/packages/meteor-developer-oauth/package.js @@ -1,13 +1,13 @@ Package.describe({ summary: 'Meteor developer accounts OAuth flow', - version: '1.2.0' + version: '1.2.1' }); -Package.onUse(function (api) { +Package.onUse(api => { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); api.use('http', ['server']); - api.use(['underscore', 'service-configuration'], ['client', 'server']); + api.use(['ecmascript', 'service-configuration'], ['client', 'server']); api.use('random', 'client'); api.addFiles('meteor_developer_common.js'); diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 0d8f2aaf33..deecf926da 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -308,10 +308,7 @@ export default class Cursor { } if (!options._suppress_initial && !this.collection.paused) { - const results = ordered ? query.results : query.results._map; - - Object.keys(results).forEach(key => { - const doc = results[key]; + query.results.forEach(doc => { const fields = EJSON.clone(doc); delete fields._id; diff --git a/packages/mongo-id/id.js b/packages/mongo-id/id.js index d18ddb5ac1..b0bf43dd37 100644 --- a/packages/mongo-id/id.js +++ b/packages/mongo-id/id.js @@ -36,7 +36,7 @@ MongoID.ObjectID = class ObjectID { typeName() { return 'oid'; } - + getTimestamp() { return Number.parseInt(this._str.substr(0, 8), 16); } @@ -61,12 +61,13 @@ MongoID.idStringify = (id) => { if (id instanceof MongoID.ObjectID) { return id.valueOf(); } else if (typeof id === 'string') { + var firstChar = id.charAt(0); if (id === '') { return id; - } else if (id.startsWith('-') || // escape previously dashed strings - id.startsWith('~') || // escape escaped numbers, true, false + } else if (firstChar === '-' || // escape previously dashed strings + firstChar === '~' || // escape escaped numbers, true, false MongoID._looksLikeObjectID(id) || // escape object-id-form strings - id.startsWith('{')) { // escape object-form strings, for maybe implementing later + firstChar === '{') { // escape object-form strings, for maybe implementing later return `-${id}`; } else { return id; // other strings go through unchanged. @@ -81,13 +82,14 @@ MongoID.idStringify = (id) => { }; MongoID.idParse = (id) => { + var firstChar = id.charAt(0); if (id === '') { return id; } else if (id === '-') { return undefined; - } else if (id.startsWith('-')) { + } else if (firstChar === '-') { return id.substr(1); - } else if (id.startsWith('~')) { + } else if (firstChar === '~') { return JSON.parse(id.substr(1)); } else if (MongoID._looksLikeObjectID(id)) { return new MongoID.ObjectID(id); diff --git a/packages/mongo/mongo_driver.js b/packages/mongo/mongo_driver.js index 6819079d0d..735c7286fe 100644 --- a/packages/mongo/mongo_driver.js +++ b/packages/mongo/mongo_driver.js @@ -61,6 +61,9 @@ var replaceMongoAtomWithMeteor = function (document) { if (document instanceof MongoDB.ObjectID) { return new Mongo.ObjectID(document.toHexString()); } + if (document instanceof MongoDB.Decimal128) { + return Decimal(document.toString()); + } if (document["EJSON$type"] && document["EJSON$value"] && _.size(document) === 2) { return EJSON.fromJSONValue(replaceNames(unmakeMongoLegal, document)); } @@ -91,6 +94,9 @@ var replaceMeteorAtomWithMongo = function (document) { // structural clone and lose the prototype. return document; } + if (document instanceof Decimal) { + return MongoDB.Decimal128.fromString(document.toString()); + } if (EJSON._isCustomType(document)) { return replaceNames(makeMongoLegal, EJSON.toJSONValue(document)); } diff --git a/packages/mongo/package.js b/packages/mongo/package.js index c78049bf82..7fbe6ba178 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -37,6 +37,10 @@ Package.onUse(function (api) { 'mongo-dev-server', ]); + // Make weak use of Decimal type on client + api.use('mongo-decimal', 'client', {weak: true}); + api.use('mongo-decimal', 'server'); + api.use('underscore', 'server'); // Binary Heap data structure is used to optimize oplog observe driver diff --git a/packages/non-core/mongo-decimal/.npm/package/.gitignore b/packages/non-core/mongo-decimal/.npm/package/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/non-core/mongo-decimal/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/tools/tests/apps/dynamic-import/packages/meteor-phantomjs-tests/.npm/package/README b/packages/non-core/mongo-decimal/.npm/package/README similarity index 100% rename from tools/tests/apps/dynamic-import/packages/meteor-phantomjs-tests/.npm/package/README rename to packages/non-core/mongo-decimal/.npm/package/README diff --git a/packages/non-core/mongo-decimal/.npm/package/npm-shrinkwrap.json b/packages/non-core/mongo-decimal/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000000..9dc4ea8214 --- /dev/null +++ b/packages/non-core/mongo-decimal/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,10 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "decimal.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-9.0.1.tgz", + "integrity": "sha512-2h0iKbJwnImBk4TGk7CG1xadoA0g3LDPlQhQzbZ221zvG0p2YVUedbKIPsOZXKZGx6YmZMJKYOalpCMxSdDqTQ==" + } + } +} diff --git a/packages/non-core/mongo-decimal/README.md b/packages/non-core/mongo-decimal/README.md new file mode 100644 index 0000000000..b551848f50 --- /dev/null +++ b/packages/non-core/mongo-decimal/README.md @@ -0,0 +1,3 @@ +# mongo-decimal +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/non-core/mongo-decimal) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/non-core/mongo-decimal) +*** diff --git a/packages/non-core/mongo-decimal/decimal.js b/packages/non-core/mongo-decimal/decimal.js new file mode 100644 index 0000000000..5e6d02207e --- /dev/null +++ b/packages/non-core/mongo-decimal/decimal.js @@ -0,0 +1,20 @@ +import { EJSON } from 'meteor/ejson'; +import { Decimal } from 'decimal.js'; + +Decimal.prototype.typeName = function() { + return 'Decimal'; +}; + +Decimal.prototype.toJSONValue = function () { + return this.toJSON(); +}; + +Decimal.prototype.clone = function () { + return Decimal(this.toString()); +}; + +EJSON.addType('Decimal', function (str) { + return Decimal(str); +}); + +export { Decimal }; diff --git a/packages/non-core/mongo-decimal/decimal_tests.js b/packages/non-core/mongo-decimal/decimal_tests.js new file mode 100644 index 0000000000..0f643c9e0b --- /dev/null +++ b/packages/non-core/mongo-decimal/decimal_tests.js @@ -0,0 +1,9 @@ +Tinytest.add('mongo-decimal - insert/find Decimal', function (test) { + var coll = new Mongo.Collection('mongo-decimal'); + var pi = Decimal('3.141592653589793'); + + coll.insert({pi: pi}); + var found = coll.findOne({pi: pi}); + + test.equal(found.pi, pi); +}); diff --git a/packages/non-core/mongo-decimal/package.js b/packages/non-core/mongo-decimal/package.js new file mode 100644 index 0000000000..24a5d1cd80 --- /dev/null +++ b/packages/non-core/mongo-decimal/package.js @@ -0,0 +1,23 @@ +Package.describe({ + summary: "JS simulation of MongoDB Decimal128 type", + version: '0.0.1' +}); + +Npm.depends({ + "decimal.js": "9.0.1" +}); + +Package.onUse(function (api) { + api.use('ecmascript'); + api.use('ejson'); + api.mainModule('decimal.js'); + api.export('Decimal'); +}); + +Package.onTest(function (api) { + api.use('mongo'); + api.use('mongo-decimal'); + api.use('insecure'); + api.use(['tinytest']); + api.addFiles('decimal_tests.js', ['client', 'server']); +}); diff --git a/packages/oauth-encryption/encrypt.js b/packages/oauth-encryption/encrypt.js index f8939c3c45..2451f62c90 100644 --- a/packages/oauth-encryption/encrypt.js +++ b/packages/oauth-encryption/encrypt.js @@ -1,11 +1,9 @@ -var crypto = require("crypto"); -var gcmKey = null; -var OAuthEncryption = exports.OAuthEncryption = {}; -var objToStr = Object.prototype.toString; +import crypto from 'crypto'; +let gcmKey = null; +const OAuthEncryption = exports.OAuthEncryption = {}; +const objToStr = Object.prototype.toString; -function isString(value) { - return objToStr.call(value) === "[object String]"; -} +const isString = value => objToStr.call(value) === "[object String]"; // Node leniently ignores non-base64 characters when parsing a base64 // string, but we want to provide a more informative error message if @@ -15,9 +13,8 @@ function isString(value) { // // Exported for the convenience of tests. // -OAuthEncryption._isBase64 = function (str) { - return isString(str) && /^[A-Za-z0-9\+\/]*\={0,2}$/.test(str); -}; +OAuthEncryption._isBase64 = str => + isString(str) && /^[A-Za-z0-9\+\/]*\={0,2}$/.test(str); // Loads the OAuth secret key, which must be 16 bytes in length @@ -26,7 +23,7 @@ OAuthEncryption._isBase64 = function (str) { // The key may be `null` which reverts to having no key (mainly used // by tests). // -OAuthEncryption.loadKey = function (key) { +OAuthEncryption.loadKey = key => { if (key === null) { gcmKey = null; return; @@ -35,7 +32,7 @@ OAuthEncryption.loadKey = function (key) { if (! OAuthEncryption._isBase64(key)) throw new Error("The OAuth encryption key must be encoded in base64"); - var buf = Buffer.from(key, "base64"); + const buf = Buffer.from(key, "base64"); if (buf.length !== 16) throw new Error("The OAuth encryption AES-128-GCM key must be 16 bytes in length"); @@ -58,22 +55,22 @@ OAuthEncryption.loadKey = function (key) { // and it's not clear that we want to incur the compatibility issues of // relying on that feature, even though it's now supported by Node 4. // -OAuthEncryption.seal = function (data, userId) { +OAuthEncryption.seal = (data, userId) => { if (! gcmKey) { throw new Error("No OAuth encryption key loaded"); } - var plaintext = Buffer.from(EJSON.stringify({ - data: data, - userId: userId + const plaintext = Buffer.from(EJSON.stringify({ + data, + userId, })); - var iv = crypto.randomBytes(12); - var cipher = crypto.createCipheriv("aes-128-gcm", gcmKey, iv); + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv("aes-128-gcm", gcmKey, iv); cipher.setAAD(Buffer.from([])); - var chunks = [cipher.update(plaintext)]; + const chunks = [cipher.update(plaintext)]; chunks.push(cipher.final()); - var encrypted = Buffer.concat(chunks); + const encrypted = Buffer.concat(chunks); return { iv: iv.toString("base64"), @@ -93,7 +90,7 @@ OAuthEncryption.seal = function (data, userId) { // To prevent an attacker from breaking the encryption key by // observing the result of sending manipulated ciphertexts, `open` // throws "decryption unsuccessful" on any error. -OAuthEncryption.open = function (ciphertext, userId) { +OAuthEncryption.open = (ciphertext, userId) => { if (! gcmKey) throw new Error("No OAuth encryption key loaded"); @@ -102,7 +99,7 @@ OAuthEncryption.open = function (ciphertext, userId) { throw new Error(); } - var decipher = crypto.createDecipheriv( + const decipher = crypto.createDecipheriv( "aes-128-gcm", gcmKey, Buffer.from(ciphertext.iv, "base64") @@ -110,13 +107,13 @@ OAuthEncryption.open = function (ciphertext, userId) { decipher.setAAD(Buffer.from([])); decipher.setAuthTag(Buffer.from(ciphertext.authTag, "base64")); - var chunks = [decipher.update( + const chunks = [decipher.update( Buffer.from(ciphertext.ciphertext, "base64"))]; chunks.push(decipher.final()); - var plaintext = Buffer.concat(chunks).toString("utf8"); + const plaintext = Buffer.concat(chunks).toString("utf8"); - var err; - var data; + let err; + let data; try { data = EJSON.parse(plaintext); @@ -139,15 +136,12 @@ OAuthEncryption.open = function (ciphertext, userId) { }; -OAuthEncryption.isSealed = function (maybeCipherText) { - return maybeCipherText && +OAuthEncryption.isSealed = maybeCipherText => + maybeCipherText && OAuthEncryption._isBase64(maybeCipherText.iv) && OAuthEncryption._isBase64(maybeCipherText.ciphertext) && OAuthEncryption._isBase64(maybeCipherText.authTag) && isString(maybeCipherText.algorithm); -}; -OAuthEncryption.keyIsLoaded = function () { - return !! gcmKey; -}; +OAuthEncryption.keyIsLoaded = () => !! gcmKey; diff --git a/packages/oauth-encryption/encrypt_tests.js b/packages/oauth-encryption/encrypt_tests.js index 343cd8ca3e..e2c73da896 100644 --- a/packages/oauth-encryption/encrypt_tests.js +++ b/packages/oauth-encryption/encrypt_tests.js @@ -1,15 +1,12 @@ -Tinytest.add("oauth-encryption - loadKey", function (test) { +Tinytest.add("oauth-encryption - loadKey", test => { test.throws( - function () { - OAuthEncryption.loadKey("my encryption key"); - }, + () => OAuthEncryption.loadKey("my encryption key"), "The OAuth encryption key must be encoded in base64" ); test.throws( - function () { - OAuthEncryption.loadKey(Buffer.from([1, 2, 3, 4, 5]).toString("base64")); - }, + () => OAuthEncryption.loadKey(Buffer.from([1, 2, 3, 4, 5]) + .toString("base64")), "The OAuth encryption AES-128-GCM key must be 16 bytes in length" ); @@ -21,13 +18,13 @@ Tinytest.add("oauth-encryption - loadKey", function (test) { OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - seal", function (test) { +Tinytest.add("oauth-encryption - seal", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}); + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}); test.isTrue(Buffer.from(ciphertext.iv, "base64").length === 12); test.isTrue(OAuthEncryption._isBase64(ciphertext.ciphertext)); test.isTrue(ciphertext.algorithm === "aes-128-gcm"); @@ -36,98 +33,92 @@ Tinytest.add("oauth-encryption - seal", function (test) { OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - open successful", function (test) { +Tinytest.add("oauth-encryption - open successful", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var userId = "rH6rNSWd2hBTfkwcc"; - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); + const userId = "rH6rNSWd2hBTfkwcc"; + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); - var decrypted = OAuthEncryption.open(ciphertext, userId); + const decrypted = OAuthEncryption.open(ciphertext, userId); test.equal(decrypted, {a: 1, b: 2}); OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - open with wrong key", function (test) { +Tinytest.add("oauth-encryption - open with wrong key", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var userId = "rH6rNSWd2hBTfkwcc"; - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); + const userId = "rH6rNSWd2hBTfkwcc"; + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); OAuthEncryption.loadKey( Buffer.from([9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]). toString("base64") ); test.throws( - function () { - OAuthEncryption.open(ciphertext, userId); - }, + () => OAuthEncryption.open(ciphertext, userId), "decryption failed" ); OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - open with wrong userId", function (test) { +Tinytest.add("oauth-encryption - open with wrong userId", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var userId = "rH6rNSWd2hBTfkwcc"; - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); + const userId = "rH6rNSWd2hBTfkwcc"; + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); - var differentUser = "3FPxY2mBNeBpigm86"; + const differentUser = "3FPxY2mBNeBpigm86"; test.throws( - function () { - OAuthEncryption.open(ciphertext, differentUser); - }, + () => OAuthEncryption.open(ciphertext, differentUser), "decryption failed" ); OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - seal and open with no userId", function (test) { +Tinytest.add("oauth-encryption - seal and open with no userId", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}); - var decrypted = OAuthEncryption.open(ciphertext); + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}); + const decrypted = OAuthEncryption.open(ciphertext); test.equal(decrypted, {a: 1, b: 2}); }); -Tinytest.add("oauth-encryption - open modified ciphertext", function (test) { +Tinytest.add("oauth-encryption - open modified ciphertext", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}); + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}); - var b = Buffer.from(ciphertext.ciphertext, "base64"); + const b = Buffer.from(ciphertext.ciphertext, "base64"); b[0] = b[0] ^ 1; ciphertext.ciphertext = b.toString("base64"); test.throws( - function () { - OAuthEncryption.open(ciphertext); - }, + () => OAuthEncryption.open(ciphertext), "decryption failed" ); }); -Tinytest.add("oauth-encryption - isSealed", function (test) { +Tinytest.add("oauth-encryption - isSealed", test => { OAuthEncryption.loadKey( Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]). toString("base64") ); - var userId = "rH6rNSWd2hBTfkwcc"; - var ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); + const userId = "rH6rNSWd2hBTfkwcc"; + const ciphertext = OAuthEncryption.seal({a: 1, b: 2}, userId); test.isTrue(OAuthEncryption.isSealed(ciphertext)); test.isFalse(OAuthEncryption.isSealed("abcdef")); @@ -136,7 +127,7 @@ Tinytest.add("oauth-encryption - isSealed", function (test) { OAuthEncryption.loadKey(null); }); -Tinytest.add("oauth-encryption - keyIsLoaded", function (test) { +Tinytest.add("oauth-encryption - keyIsLoaded", test => { OAuthEncryption.loadKey(null); test.isFalse(OAuthEncryption.keyIsLoaded()); diff --git a/packages/oauth-encryption/package.js b/packages/oauth-encryption/package.js index 84a5c01552..b6a51a2aa0 100644 --- a/packages/oauth-encryption/package.js +++ b/packages/oauth-encryption/package.js @@ -1,16 +1,17 @@ Package.describe({ summary: "Encrypt account secrets stored in the database", - version: '1.3.0' + version: '1.3.1', }); -Package.onUse(function (api) { +Package.onUse(api => { + api.use('ecmascript', 'server'); api.use("modules@0.7.5", "server"); api.use("ejson@1.0.12", "server"); api.mainModule("encrypt.js", "server"); api.export("OAuthEncryption", "server"); }); -Package.onTest(function (api) { +Package.onTest(api => { api.use("tinytest"); api.use("oauth-encryption"); api.addFiles("encrypt_tests.js", ["server"]); diff --git a/packages/oauth/end_of_popup_response.js b/packages/oauth/end_of_popup_response.js index 739830a241..e142f9c30d 100644 --- a/packages/oauth/end_of_popup_response.js +++ b/packages/oauth/end_of_popup_response.js @@ -1,15 +1,14 @@ -(function () { +(() => { - var config = JSON.parse(document.getElementById("config").innerHTML); + const config = JSON.parse(document.getElementById("config").innerHTML); if (config.setCredentialToken) { - var credentialToken = config.credentialToken; - var credentialSecret = config.credentialSecret; + const { credentialToken, credentialSecret } = config; if (config.isCordova) { - var credentialString = JSON.stringify({ - credentialToken: credentialToken, - credentialSecret: credentialSecret + const credentialString = JSON.stringify({ + credentialToken, + credentialSecret, }); window.location.hash = credentialString; @@ -31,7 +30,7 @@ if (! config.isCordova) { document.getElementById("completedText").style.display = "block"; - document.getElementById("loginCompleted").onclick = function(){ window.close(); }; + document.getElementById("loginCompleted").onclick = () => window.close(); window.close(); } })(); diff --git a/packages/oauth/end_of_redirect_response.js b/packages/oauth/end_of_redirect_response.js index f196db2283..4c88aa356f 100644 --- a/packages/oauth/end_of_redirect_response.js +++ b/packages/oauth/end_of_redirect_response.js @@ -1,6 +1,6 @@ -(function () { +(() => { - var config = JSON.parse(document.getElementById("config").innerHTML); + const config = JSON.parse(document.getElementById("config").innerHTML); if (config.setCredentialToken) { sessionStorage[config.storagePrefix + config.credentialToken] = diff --git a/packages/oauth/oauth_browser.js b/packages/oauth/oauth_browser.js index f1e435903b..a4e8a4bf5b 100644 --- a/packages/oauth/oauth_browser.js +++ b/packages/oauth/oauth_browser.js @@ -8,20 +8,21 @@ // arguments. // @param dimensions {optional Object(width, height)} The dimensions of // the popup. If not passed defaults to something sane. -OAuth.showPopup = function (url, callback, dimensions) { +OAuth.showPopup = (url, callback, dimensions) => { // default dimensions that worked well for facebook and google - var popup = openCenteredPopup( + const popup = openCenteredPopup( url, (dimensions && dimensions.width) || 650, (dimensions && dimensions.height) || 331 ); - var checkPopupOpen = setInterval(function() { + const checkPopupOpen = setInterval(() => { + let popupClosed; try { // Fix for #328 - added a second test criteria (popup.closed === undefined) // to humour this Android quirk: // http://code.google.com/p/android/issues/detail?id=21061 - var popupClosed = popup.closed || popup.closed === undefined; + popupClosed = popup.closed || popup.closed === undefined; } catch (e) { // For some unknown reason, IE9 (and others?) sometimes (when // the popup closes too quickly?) throws "SCRIPT16386: No such @@ -37,29 +38,29 @@ OAuth.showPopup = function (url, callback, dimensions) { }, 100); }; -var openCenteredPopup = function(url, width, height) { - var screenX = typeof window.screenX !== 'undefined' +const openCenteredPopup = function(url, width, height) { + const screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; - var screenY = typeof window.screenY !== 'undefined' + const screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop; - var outerWidth = typeof window.outerWidth !== 'undefined' + const outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth; - var outerHeight = typeof window.outerHeight !== 'undefined' + const outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22); // XXX what is the 22? // Use `outerWidth - width` and `outerHeight - height` for help in // positioning the popup centered relative to the current window - var left = screenX + (outerWidth - width) / 2; - var top = screenY + (outerHeight - height) / 2; - var features = ('width=' + width + ',height=' + height + - ',left=' + left + ',top=' + top + ',scrollbars=yes'); + const left = screenX + (outerWidth - width) / 2; + const top = screenY + (outerHeight - height) / 2; + const features = (`width=${width},height=${height}` + + `,left=${left},top=${top},scrollbars=yes'`); - var newwindow = window.open(url, 'Login', features); + const newwindow = window.open(url, 'Login', features); if (typeof newwindow === 'undefined') { // blocked by a popup blocker maybe? - var err = new Error("The login popup was blocked by the browser"); + const err = new Error("The login popup was blocked by the browser"); err.attemptedUrl = url; throw err; } diff --git a/packages/oauth/oauth_client.js b/packages/oauth/oauth_client.js index 45314f0e9e..810c0847ee 100644 --- a/packages/oauth/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -1,27 +1,27 @@ // credentialToken -> credentialSecret. You must provide both the // credentialToken and the credentialSecret to retrieve an access token from // the _pendingCredentials collection. -var credentialSecrets = {}; +const credentialSecrets = {}; OAuth = {}; -OAuth.showPopup = function (url, callback, dimensions) { +OAuth.showPopup = (url, callback, dimensions) => { throw new Error("OAuth.showPopup must be implemented on this arch."); }; // Determine the login style (popup or redirect) for this login flow. // // -OAuth._loginStyle = function (service, config, options) { +OAuth._loginStyle = (service, config, options) => { if (Meteor.isCordova) { return "popup"; } - var loginStyle = (options && options.loginStyle) || config.loginStyle || 'popup'; + let loginStyle = (options && options.loginStyle) || config.loginStyle || 'popup'; - if (! _.contains(["popup", "redirect"], loginStyle)) - throw new Error("Invalid login style: " + loginStyle); + if (! ["popup", "redirect"].includes(loginStyle)) + throw new Error(`Invalid login style: ${loginStyle}`); // If we don't have session storage (for example, Safari in private // mode), the redirect login flow won't work, so fallback to the @@ -38,10 +38,10 @@ OAuth._loginStyle = function (service, config, options) { return loginStyle; }; -OAuth._stateParam = function (loginStyle, credentialToken, redirectUrl) { - var state = { - loginStyle: loginStyle, - credentialToken: credentialToken, +OAuth._stateParam = (loginStyle, credentialToken, redirectUrl) => { + const state = { + loginStyle, + credentialToken, isCordova: Meteor.isCordova }; @@ -59,10 +59,8 @@ OAuth._stateParam = function (loginStyle, credentialToken, redirectUrl) { // the login service, save the credential token for this login attempt // in the reload migration data. // -OAuth.saveDataForRedirect = function (loginService, credentialToken) { - Reload._onMigrate('oauth', function () { - return [true, {loginService: loginService, credentialToken: credentialToken}]; - }); +OAuth.saveDataForRedirect = (loginService, credentialToken) => { + Reload._onMigrate('oauth', () => [true, { loginService, credentialToken }]); Reload._migrate(null, {immediateMigration: true}); }; @@ -74,15 +72,15 @@ OAuth.saveDataForRedirect = function (loginService, credentialToken) { // application startup and we weren't just redirected at the end of // the login flow. // -OAuth.getDataAfterRedirect = function () { - var migrationData = Reload._migrationData('oauth'); +OAuth.getDataAfterRedirect = () => { + const migrationData = Reload._migrationData('oauth'); if (! (migrationData && migrationData.credentialToken)) return null; - var credentialToken = migrationData.credentialToken; - var key = OAuth._storageTokenPrefix + credentialToken; - var credentialSecret; + const { credentialToken } = migrationData; + const key = OAuth._storageTokenPrefix + credentialToken; + let credentialSecret; try { credentialSecret = sessionStorage.getItem(key); sessionStorage.removeItem(key); @@ -91,8 +89,8 @@ OAuth.getDataAfterRedirect = function () { } return { loginService: migrationData.loginService, - credentialToken: credentialToken, - credentialSecret: credentialSecret + credentialToken, + credentialSecret, }; }; @@ -109,13 +107,13 @@ OAuth.getDataAfterRedirect = function () { // is closed and we have the credential from the login service. // credentialToken: our identifier for this login flow. // -OAuth.launchLogin = function (options) { +OAuth.launchLogin = options => { if (! options.loginService) throw new Error('loginService required'); if (options.loginStyle === 'popup') { OAuth.showPopup( options.loginUrl, - _.bind(options.credentialRequestCompleteCallback, null, options.credentialToken), + options.credentialRequestCompleteCallback.bind(null, options.credentialToken), options.popupOptions); } else if (options.loginStyle === 'redirect') { OAuth.saveDataForRedirect(options.loginService, options.credentialToken); @@ -127,20 +125,20 @@ OAuth.launchLogin = function (options) { // XXX COMPAT WITH 0.7.0.1 // Private interface but probably used by many oauth clients in atmosphere. -OAuth.initiateLogin = function (credentialToken, url, callback, dimensions) { +OAuth.initiateLogin = (credentialToken, url, callback, dimensions) => { OAuth.showPopup( url, - _.bind(callback, null, credentialToken), + callback.bind(null, credentialToken), dimensions ); }; // Called by the popup when the OAuth flow is completed, right before // the popup closes. -OAuth._handleCredentialSecret = function (credentialToken, secret) { +OAuth._handleCredentialSecret = (credentialToken, secret) => { check(credentialToken, String); check(secret, String); - if (! _.has(credentialSecrets,credentialToken)) { + if (! Object.prototype.hasOwnProperty.call(credentialSecrets, credentialToken)) { credentialSecrets[credentialToken] = secret; } else { throw new Error("Duplicate credential token from OAuth login"); @@ -149,13 +147,13 @@ OAuth._handleCredentialSecret = function (credentialToken, secret) { // Used by accounts-oauth, which needs both a credentialToken and the // corresponding to credential secret to call the `login` method over DDP. -OAuth._retrieveCredentialSecret = function (credentialToken) { +OAuth._retrieveCredentialSecret = credentialToken => { // First check the secrets collected by OAuth._handleCredentialSecret, // then check localStorage. This matches what we do in // end_of_login_response.html. - var secret = credentialSecrets[credentialToken]; + let secret = credentialSecrets[credentialToken]; if (! secret) { - var localStorageKey = OAuth._storageTokenPrefix + credentialToken; + const localStorageKey = OAuth._storageTokenPrefix + credentialToken; secret = Meteor._localStorage.getItem(localStorageKey); Meteor._localStorage.removeItem(localStorageKey); } else { diff --git a/packages/oauth/oauth_common.js b/packages/oauth/oauth_common.js index 57ec936317..68fe3319fe 100644 --- a/packages/oauth/oauth_common.js +++ b/packages/oauth/oauth_common.js @@ -1,6 +1,8 @@ +import url from 'url'; + OAuth._storageTokenPrefix = "Meteor.oauth.credentialSecret-"; -OAuth._redirectUri = function (serviceName, config, params, absoluteUrlOptions) { +OAuth._redirectUri = (serviceName, config, params, absoluteUrlOptions) => { // XXX COMPAT WITH 0.9.0 // The redirect URI used to have a "?close" query argument. We // detect whether we need to be backwards compatible by checking for @@ -8,26 +10,26 @@ OAuth._redirectUri = function (serviceName, config, params, absoluteUrlOptions) // code which had the "?close" argument. // This logic is duplicated in the tool so that the tool can do OAuth // flow with <= 0.9.0 servers (tools/auth.js). - var query = config.loginStyle ? null : "close"; + const query = config.loginStyle ? null : "close"; // Clone because we're going to mutate 'params'. The 'cordova' and // 'android' parameters are only used for picking the host of the // redirect URL, and not actually included in the redirect URL itself. - var isCordova = false; - var isAndroid = false; + let isCordova = false; + let isAndroid = false; if (params) { - params = _.clone(params); + params = { ...params }; isCordova = params.cordova; isAndroid = params.android; delete params.cordova; delete params.android; - if (_.isEmpty(params)) { + if (Object.keys(params).length === 0) { params = undefined; } } if (Meteor.isServer && isCordova) { - var rootUrl = process.env.MOBILE_ROOT_URL || + let rootUrl = process.env.MOBILE_ROOT_URL || __meteor_runtime_config__.ROOT_URL; if (isAndroid) { @@ -36,8 +38,7 @@ OAuth._redirectUri = function (serviceName, config, params, absoluteUrlOptions) // XXX Maybe we should put this in a separate package or something // that is used here and by boilerplate-generator? Or maybe // `Meteor.absoluteUrl` should know how to do this? - var url = Npm.require("url"); - var parsedRootUrl = url.parse(rootUrl); + const parsedRootUrl = url.parse(rootUrl); if (parsedRootUrl.hostname === "localhost") { parsedRootUrl.hostname = "10.0.2.2"; delete parsedRootUrl.host; @@ -45,15 +46,16 @@ OAuth._redirectUri = function (serviceName, config, params, absoluteUrlOptions) rootUrl = url.format(parsedRootUrl); } - absoluteUrlOptions = _.extend({}, absoluteUrlOptions, { + absoluteUrlOptions = { + ...absoluteUrlOptions, // For Cordova clients, redirect to the special Cordova root url // (likely a local IP in development mode). - rootUrl: rootUrl - }); + rootUrl, + }; } return URL._constructUrl( - Meteor.absoluteUrl('_oauth/' + serviceName, absoluteUrlOptions), + Meteor.absoluteUrl(`_oauth/${serviceName}`, absoluteUrlOptions), query, params); }; diff --git a/packages/oauth/oauth_cordova.js b/packages/oauth/oauth_cordova.js index 325febfaca..06c8870731 100644 --- a/packages/oauth/oauth_cordova.js +++ b/packages/oauth/oauth_cordova.js @@ -8,32 +8,31 @@ // arguments. // @param dimensions {optional Object(width, height)} The dimensions of // the popup. If not passed defaults to something sane. -OAuth.showPopup = function (url, callback, dimensions) { - var fail = function (err) { - Meteor._debug("Error from OAuth popup", err); - }; +OAuth.showPopup = (url, callback, dimensions) => { + const fail = err => + Meteor._debug(`Error from OAuth popup: ${JSON.stringify(err)}`); // When running on an android device, we sometimes see the // `pageLoaded` callback fire twice for the final page in the OAuth // popup, even though the page only loads once. This is maybe an // Android bug or maybe something intentional about how onPageFinished // works that we don't understand and isn't well-documented. - var oauthFinished = false; + let oauthFinished = false; - var pageLoaded = function (event) { + const pageLoaded = event => { if (oauthFinished) { return; } if (event.url.indexOf(Meteor.absoluteUrl('_oauth')) === 0) { - var splitUrl = event.url.split("#"); - var hashFragment = splitUrl[1]; + const splitUrl = event.url.split("#"); + const hashFragment = splitUrl[1]; if (! hashFragment) { throw new Error("No hash fragment in OAuth popup?"); } - var credentials = JSON.parse(decodeURIComponent(hashFragment)); + const credentials = JSON.parse(decodeURIComponent(hashFragment)); OAuth._handleCredentialSecret(credentials.credentialToken, credentials.credentialSecret); @@ -47,20 +46,20 @@ OAuth.showPopup = function (url, callback, dimensions) { // https://issues.apache.org/jira/browse/CB-2285. // // XXX Can we make this timeout smaller? - setTimeout(function () { + setTimeout(() => { popup.close(); callback(); }, 100); } }; - var onExit = function () { + const onExit = () => { popup.removeEventListener('loadstop', pageLoaded); popup.removeEventListener('loaderror', fail); popup.removeEventListener('exit', onExit); }; - var popup = window.open(url, '_blank', 'location=yes,hidden=yes'); + const popup = window.open(url, '_blank', 'location=yes,hidden=yes'); popup.addEventListener('loadstop', pageLoaded); popup.addEventListener('loaderror', fail); popup.addEventListener('exit', onExit); diff --git a/packages/oauth/oauth_server.js b/packages/oauth/oauth_server.js index b3cde82f33..c84f109c72 100644 --- a/packages/oauth/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -1,11 +1,12 @@ -var url = Npm.require('url'); +import Fiber from 'fibers'; +import url from 'url'; OAuth = {}; OAuthTest = {}; RoutePolicy.declare('/_oauth/', 'network'); -var registeredServices = {}; +const registeredServices = {}; // Internal: Maps from service version to handler function. The // 'oauth1' and 'oauth2' packages manipulate this directly to register @@ -29,58 +30,57 @@ OAuth._requestHandlers = {}; // up in the user's services[name] field // - `null` if the user declined to give permissions // -OAuth.registerService = function (name, version, urls, handleOauthRequest) { +OAuth.registerService = (name, version, urls, handleOauthRequest) => { if (registeredServices[name]) - throw new Error("Already registered the " + name + " OAuth service"); + throw new Error(`Already registered the ${name} OAuth service`); registeredServices[name] = { serviceName: name, - version: version, - urls: urls, - handleOauthRequest: handleOauthRequest + version, + urls, + handleOauthRequest, }; }; // For test cleanup. -OAuthTest.unregisterService = function (name) { +OAuthTest.unregisterService = name => { delete registeredServices[name]; }; -OAuth.retrieveCredential = function(credentialToken, credentialSecret) { - return OAuth._retrievePendingCredential(credentialToken, credentialSecret); -}; +OAuth.retrieveCredential = (credentialToken, credentialSecret) => + OAuth._retrievePendingCredential(credentialToken, credentialSecret); // The state parameter is normally generated on the client using // `btoa`, but for tests we need a version that runs on the server. // -OAuth._generateState = function (loginStyle, credentialToken, redirectUrl) { +OAuth._generateState = (loginStyle, credentialToken, redirectUrl) => { return Buffer.from(JSON.stringify({ loginStyle: loginStyle, credentialToken: credentialToken, redirectUrl: redirectUrl})).toString('base64'); }; -OAuth._stateFromQuery = function (query) { - var string; +OAuth._stateFromQuery = query => { + let string; try { string = Buffer.from(query.state, 'base64').toString('binary'); } catch (e) { - Log.warn('Unable to base64 decode state from OAuth query: ' + query.state); + Log.warn(`Unable to base64 decode state from OAuth query: ${query.state}`); throw e; } try { return JSON.parse(string); } catch (e) { - Log.warn('Unable to parse state from OAuth query: ' + string); + Log.warn(`Unable to parse state from OAuth query: ${string}`); throw e; } }; -OAuth._loginStyleFromQuery = function (query) { - var style; +OAuth._loginStyleFromQuery = query => { + let style; // For backwards-compatibility for older clients, catch any errors // that result from parsing the state parameter. If we can't parse it, // set login style to popup by default. @@ -90,13 +90,13 @@ OAuth._loginStyleFromQuery = function (query) { style = "popup"; } if (style !== "popup" && style !== "redirect") { - throw new Error("Unrecognized login style: " + style); + throw new Error(`Unrecognized login style: ${style}`); } return style; }; -OAuth._credentialTokenFromQuery = function (query) { - var state; +OAuth._credentialTokenFromQuery = query => { + let state; // For backwards-compatibility for older clients, catch any errors // that result from parsing the state parameter. If we can't parse it, // assume that the state parameter's value is the credential token, as @@ -109,7 +109,7 @@ OAuth._credentialTokenFromQuery = function (query) { return state.credentialToken; }; -OAuth._isCordovaFromQuery = function (query) { +OAuth._isCordovaFromQuery = query => { try { return !! OAuth._stateFromQuery(query).isCordova; } catch (err) { @@ -125,9 +125,9 @@ OAuth._isCordovaFromQuery = function (query) { // We export this function so that developers can override this // behavior to allow apps from external domains to login using the // redirect OAuth flow. -OAuth._checkRedirectUrlOrigin = function (redirectUrl) { - var appHost = Meteor.absoluteUrl(); - var appHostReplacedLocalhost = Meteor.absoluteUrl(undefined, { +OAuth._checkRedirectUrlOrigin = redirectUrl => { + const appHost = Meteor.absoluteUrl(); + const appHostReplacedLocalhost = Meteor.absoluteUrl(undefined, { replaceLocalhost: true }); return ( @@ -138,29 +138,35 @@ OAuth._checkRedirectUrlOrigin = function (redirectUrl) { // Listen to incoming OAuth http requests -var middleware = function (req, res, next) { +WebApp.connectHandlers.use((req, res, next) => { + // Need to create a Fiber since we're using synchronous http calls and nothing + // else is wrapping this in a fiber automatically + Fiber(() => middleware(req, res, next)).run(); +}); + +const middleware = (req, res, next) => { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { - var serviceName = oauthServiceName(req); + const serviceName = oauthServiceName(req); if (!serviceName) { // not an oauth request. pass to next middleware. next(); return; } - var service = registeredServices[serviceName]; + const service = registeredServices[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) - throw new Error("Unexpected OAuth service " + serviceName); + throw new Error(`Unexpected OAuth service ${serviceName}`); // Make sure we're configured ensureConfigured(serviceName); - var handler = OAuth._requestHandlers[service.version]; + const handler = OAuth._requestHandlers[service.version]; if (!handler) - throw new Error("Unexpected OAuth version " + service.version); + throw new Error(`Unexpected OAuth version ${service.version}`); handler(service, req.query, res); } catch (err) { // if we got thrown an error, save it off, it will get passed to @@ -206,15 +212,15 @@ OAuthTest.middleware = middleware; // // @returns {String|null} e.g. "facebook", or null if this isn't an // oauth request -var oauthServiceName = function (req) { +const oauthServiceName = req => { // req.url will be "/_oauth/" with an optional "?close". - var i = req.url.indexOf('?'); - var barePath; + const i = req.url.indexOf('?'); + let barePath; if (i === -1) barePath = req.url; else barePath = req.url.substring(0, i); - var splitPath = barePath.split('/'); + const splitPath = barePath.split('/'); // Any non-oauth request will continue down the default // middlewares. @@ -222,18 +228,18 @@ var oauthServiceName = function (req) { return null; // Find service based on url - var serviceName = splitPath[2]; + const serviceName = splitPath[2]; return serviceName; }; // Make sure we're configured -var ensureConfigured = function(serviceName) { +const ensureConfigured = serviceName => { if (!ServiceConfiguration.configurations.findOne({service: serviceName})) { throw new ServiceConfiguration.ConfigError(); } }; -var isSafe = function (value) { +const isSafe = value => { // This matches strings generated by `Random.secret` and // `Random.id`. return typeof value === "string" && @@ -241,7 +247,7 @@ var isSafe = function (value) { }; // Internal: used by the oauth1 and oauth2 packages -OAuth._renderOauthResults = function(res, query, credentialSecret) { +OAuth._renderOauthResults = (res, query, credentialSecret) => { // For tests, we support the `only_credential_secret_for_test` // parameter, which just returns the credential secret without any // surrounding HTML. (The test needs to be able to easily grab the @@ -255,15 +261,15 @@ OAuth._renderOauthResults = function(res, query, credentialSecret) { res.writeHead(200, {'Content-Type': 'text/html'}); res.end(credentialSecret, 'utf-8'); } else { - var details = { - query: query, + const details = { + query, loginStyle: OAuth._loginStyleFromQuery(query) }; if (query.error) { details.error = query.error; } else { - var token = OAuth._credentialTokenFromQuery(query); - var secret = credentialSecret; + const token = OAuth._credentialTokenFromQuery(query); + const secret = credentialSecret; if (token && secret && isSafe(token) && isSafe(secret)) { details.credentials = { token: token, secret: secret}; @@ -296,13 +302,13 @@ OAuth._endOfRedirectResponseTemplate = Assets.getText( // - redirectUrl // - isCordova (boolean) // -var renderEndOfLoginResponse = function (options) { +const renderEndOfLoginResponse = options => { // It would be nice to use Blaze here, but it's a little tricky // because our mustaches would be inside a