Merge branch 'devel' into release-1.7.1

This commit is contained in:
Ben Newman
2018-07-18 12:41:25 -04:00
175 changed files with 7078 additions and 6403 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -203,3 +203,9 @@ valid-identifier: https://github.com/purplecabbage/valid-identifier
----------
Jesse MacFadyen
----------
puppeteer: https://github.com/GoogleChrome/puppeteer
----------
Copyright 2017 Google Inc.

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
browser
server

View File

@@ -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
);
});
}
}
});

View File

@@ -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=="
}
}
}

View File

@@ -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"
}
}

9
meteor
View File

@@ -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" "$@"

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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();

View File

@@ -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(

File diff suppressed because it is too large Load Diff

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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'
]);

View File

@@ -1,6 +1,4 @@
import {AccountsServer} from "./accounts_server.js";
import "./accounts_rate_limit.js";
import "./url_server.js";
/**
* @namespace Accounts

View File

@@ -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;
};

View File

@@ -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);
}
};

View File

@@ -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

View File

@@ -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" +

View File

@@ -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.

View File

@@ -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,

View File

@@ -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" +

View File

@@ -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.

View File

@@ -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}`
),
});
}

View File

@@ -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" +

View File

@@ -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']);

View File

@@ -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

View File

@@ -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" +

View File

@@ -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.

View File

@@ -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

View File

@@ -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" +

View File

@@ -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']);

View File

@@ -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);
}
};
};
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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');
});

View File

@@ -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"),
},
};

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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);
}

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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" +

View File

@@ -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']);

View File

@@ -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,

View File

@@ -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"];
}

View File

@@ -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"}})
);
});

View File

@@ -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 });

View File

@@ -14,13 +14,27 @@
{{#if inResetPasswordFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<form class="accounts-dialog accounts-centered-dialog">
<label id="reset-password-username-email-label" for="reset-password-username-email" style="display: none;">
Username or email
</label>
<div class="reset-password-username-email-wrapper" style="display: none;" >
<input
id="reset-password-username-email"
type="text"
value="{{displayName}}"
autocomplete="username email"
disabled
/>
</div>
<label id="reset-password-new-password-label" for="reset-password-new-password">
New password
</label>
</label>
<div class="reset-password-new-password-wrapper">
<input id="reset-password-new-password" type="password" />
<input id="reset-password-new-password" type="password" autocomplete="new-password" />
</div>
{{> _loginButtonsMessages}}
@@ -30,7 +44,7 @@
</div>
<a class="accounts-close" id="login-buttons-cancel-reset-password">&times;</a>
</div>
</form>
{{/if}}
</template>
@@ -48,13 +62,27 @@
{{#if inEnrollAccountFlow}}
<div class="hide-background"></div>
<div class="accounts-dialog accounts-centered-dialog">
<form class="accounts-dialog accounts-centered-dialog">
<label id="enroll-account-username-email-label" for="enroll-account-username-email" style="display: none;">
Username or email
</label>
<div class="enroll-account-username-email-wrapper" style="display: none;" >
<input
id="enroll-account-username-email"
type="text"
value="{{displayName}}"
autocomplete="username email"
disabled
/>
</div>
<label id="enroll-account-password-label" for="enroll-account-password">
Choose a password
</label>
<div class="enroll-account-password-wrapper">
<input id="enroll-account-password" type="password" />
<input id="enroll-account-password" type="password" autocomplete="new-password" />
</div>
{{> _loginButtonsMessages}}
@@ -64,7 +92,7 @@
</div>
<a class="accounts-close" id="login-buttons-cancel-enroll-account">&times;</a>
</div>
</form>
{{/if}}
</template>

View File

@@ -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),
});

View File

@@ -137,10 +137,10 @@
</template>
<template name="_forgotPasswordForm">
<div class="login-form">
<form class="login-form">
<div id="forgot-password-email-label-and-input"> {{! XXX we should probably use loginButtonsFormField }}
<label id="forgot-password-email-label" for="forgot-password-email">Email</label>
<input id="forgot-password-email" type="email"/>
<input id="forgot-password-email" type="email" autocomplete="email"/>
</div>
{{> _loginButtonsMessages}}
@@ -150,7 +150,7 @@
</div>
{{> _loginButtonsBackToLoginLink}}
</div>
</form>
</template>
<template name="_loginButtonsBackToLoginLink">
@@ -161,23 +161,31 @@
<template name="_loginButtonsFormField">
{{#if visible}}
<div id="login-{{fieldName}}-label-and-input">
<div id="login-{{fieldName}}-label-and-input" style="{{fieldStyle}}">
<label id="login-{{fieldName}}-label" for="login-{{fieldName}}">
{{fieldLabel}}
</label>
<input id="login-{{fieldName}}" type="{{inputType}}" />
<input
id="login-{{fieldName}}"
type="{{inputType}}"
value="{{fieldValue}}"
autocomplete="{{autocomplete}}"
/>
</div>
{{/if}}
</template>
<template name="_loginButtonsChangePassword">
{{#each fields}}
{{> _loginButtonsFormField}}
{{/each}}
<form class="login-form">
{{> _loginButtonsMessages}}
{{#each fields}}
{{> _loginButtonsFormField}}
{{/each}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
{{> _loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
</form>
</template>

View File

@@ -1,378 +1,53 @@
import { passwordSignupFields } from './accounts_ui.js';
import {
displayName,
getLoginServices,
hasPasswordService,
validateUsername,
validateEmail,
validatePassword,
} from './login_buttons.js';
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
// events shared between loginButtonsLoggedOutDropdown and
// loginButtonsLoggedInDropdown
Template.loginButtons.events({
'click #login-name-link, click #login-sign-in-link': function () {
loginButtonsSession.set('dropdownVisible', true);
},
'click .login-close-text': function () {
loginButtonsSession.closeDropdown();
}
});
//
// loginButtonsLoggedInDropdown template and related
//
Template._loginButtonsLoggedInDropdown.events({
'click #login-buttons-open-change-password': function() {
loginButtonsSession.resetMessages();
loginButtonsSession.set('inChangePasswordFlow', true);
}
});
Template._loginButtonsLoggedInDropdown.helpers({
displayName: displayName,
inChangePasswordFlow: function () {
return loginButtonsSession.get('inChangePasswordFlow');
},
inMessageOnlyFlow: function () {
return loginButtonsSession.get('inMessageOnlyFlow');
},
dropdownVisible: function () {
return loginButtonsSession.get('dropdownVisible');
}
});
Template._loginButtonsLoggedInDropdownActions.helpers({
allowChangingPassword: function () {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
var user = Meteor.user();
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
}
});
//
// loginButtonsLoggedOutDropdown template and related
//
Template._loginButtonsLoggedOutDropdown.events({
'click #login-buttons-password': function (event) {
event.preventDefault();
loginOrSignup();
},
'keypress #forgot-password-email': function (event) {
if (event.keyCode === 13)
forgotPassword();
},
'click #login-buttons-forgot-password': function () {
forgotPassword();
},
'click #signup-link': function () {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
var username = trimmedElementValueById('login-username');
var email = trimmedElementValueById('login-email');
var usernameOrEmail = trimmedElementValueById('login-username-or-email');
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', true);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (username !== null)
document.getElementById('login-username').value = username;
else if (email !== null)
document.getElementById('login-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') === -1)
document.getElementById('login-username').value = usernameOrEmail;
else
document.getElementById('login-email').value = usernameOrEmail;
if (password !== null)
document.getElementById('login-password').value = password;
// Force redrawing the `login-dropdown-list` element because of
// a bizarre Chrome bug in which part of the DIV is not redrawn
// in case you had tried to unsuccessfully log in before
// switching to the signup form.
//
// Found tip on how to force a redraw on
// http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654
var redraw = document.getElementById('login-dropdown-list');
redraw.style.display = 'none';
redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work
redraw.style.display = 'block';
},
'click #forgot-password-link': function () {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
var email = trimmedElementValueById('login-email');
var usernameOrEmail = trimmedElementValueById('login-username-or-email');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', true);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (email !== null)
document.getElementById('forgot-password-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.indexOf('@') !== -1)
document.getElementById('forgot-password-email').value = usernameOrEmail;
},
'click #back-to-login-link': function () {
loginButtonsSession.resetMessages();
var username = trimmedElementValueById('login-username');
var email = trimmedElementValueById('login-email')
|| trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names?
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
if (document.getElementById('login-username') && username !== null)
document.getElementById('login-username').value = username;
if (document.getElementById('login-email') && email !== null)
document.getElementById('login-email').value = email;
var usernameOrEmailInput = document.getElementById('login-username-or-email');
if (usernameOrEmailInput) {
if (email !== null)
usernameOrEmailInput.value = email;
if (username !== null)
usernameOrEmailInput.value = username;
}
if (password !== null)
document.getElementById('login-password').value = password;
},
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
loginOrSignup();
}
});
Template._loginButtonsLoggedOutDropdown.helpers({
// additional classes that can be helpful in styling the dropdown
additionalClasses: function () {
if (!hasPasswordService()) {
return false;
} else {
if (loginButtonsSession.get('inSignupFlow')) {
return 'login-form-create-account';
} else if (loginButtonsSession.get('inForgotPasswordFlow')) {
return 'login-form-forgot-password';
} else {
return 'login-form-sign-in';
}
}
},
dropdownVisible: function () {
return loginButtonsSession.get('dropdownVisible');
},
hasPasswordService: hasPasswordService
});
// return all login services, with password last
Template._loginButtonsLoggedOutAllServices.helpers({
services: getLoginServices,
isPasswordService: function () {
return this.name === 'password';
},
hasOtherServices: function () {
return getLoginServices().length > 1;
},
hasPasswordService: hasPasswordService
});
Template._loginButtonsLoggedOutPasswordService.helpers({
fields: function () {
var loginFields = [
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
passwordSignupFields());
}},
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return passwordSignupFields() === "USERNAME_ONLY";
}},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
visible: function () {
return passwordSignupFields() === "EMAIL_ONLY";
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}}
];
var signupFields = [
{fieldName: 'username', fieldLabel: 'Username',
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
passwordSignupFields());
}},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
visible: function () {
return _.contains(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"],
passwordSignupFields());
}},
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
visible: function () {
return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL";
}},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'Password (again)',
inputType: 'password',
visible: function () {
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
return _.contains(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
passwordSignupFields());
}}
];
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
},
inForgotPasswordFlow: function () {
return loginButtonsSession.get('inForgotPasswordFlow');
},
inLoginFlow: function () {
return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow');
},
inSignupFlow: function () {
return loginButtonsSession.get('inSignupFlow');
},
showCreateAccountLink: function () {
return !Accounts._options.forbidClientAccountCreation;
},
showForgotPasswordLink: function () {
return _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
passwordSignupFields());
}
});
Template._loginButtonsFormField.helpers({
inputType: function () {
return this.inputType || "text";
}
});
//
// loginButtonsChangePassword template
//
Template._loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': function () {
changePassword();
}
});
Template._loginButtonsChangePassword.helpers({
fields: function () {
return [
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
visible: function () {
return true;
}},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password',
visible: function () {
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
return _.contains(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
passwordSignupFields());
}}
];
}
});
const loginButtonsSession = Accounts._loginButtonsSession;
//
// helpers
//
var elementValueById = function(id) {
var element = document.getElementById(id);
const elementValueById = id => {
const element = document.getElementById(id);
if (!element)
return null;
else
return element.value;
};
var trimmedElementValueById = function(id) {
var element = document.getElementById(id);
const trimmedElementValueById = id => {
const element = document.getElementById(id);
if (!element)
return null;
else
return element.value.replace(/^\s*|\s*$/g, ""); // trim() doesn't work on IE8;
};
var loginOrSignup = function () {
const loginOrSignup = () => {
if (loginButtonsSession.get('inSignupFlow'))
signup();
else
login();
};
var login = function () {
const login = () => {
loginButtonsSession.resetMessages();
var username = trimmedElementValueById('login-username');
var email = trimmedElementValueById('login-email');
var usernameOrEmail = trimmedElementValueById('login-username-or-email');
const username = trimmedElementValueById('login-username');
const email = trimmedElementValueById('login-email');
const usernameOrEmail = trimmedElementValueById('login-username-or-email');
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
const password = elementValueById('login-password');
var loginSelector;
let loginSelector;
if (username !== null) {
if (!validateUsername(username))
return;
@@ -394,7 +69,7 @@ var login = function () {
throw new Error("Unexpected -- no element to use as a login user selector");
}
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
Meteor.loginWithPassword(loginSelector, password, (error, result) => {
if (error) {
loginButtonsSession.errorMessage(error.reason || "Unknown error");
} else {
@@ -403,12 +78,12 @@ var login = function () {
});
};
var signup = function () {
const signup = () => {
loginButtonsSession.resetMessages();
var options = {}; // to be passed to Accounts.createUser
const options = {}; // to be passed to Accounts.createUser
var username = trimmedElementValueById('login-username');
const username = trimmedElementValueById('login-username');
if (username !== null) {
if (!validateUsername(username))
return;
@@ -416,7 +91,7 @@ var signup = function () {
options.username = username;
}
var email = trimmedElementValueById('login-email');
const email = trimmedElementValueById('login-email');
if (email !== null) {
if (!validateEmail(email))
return;
@@ -425,7 +100,7 @@ var signup = function () {
}
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
const password = elementValueById('login-password');
if (!validatePassword(password))
return;
else
@@ -434,7 +109,7 @@ var signup = function () {
if (!matchPasswordAgainIfPresent())
return;
Accounts.createUser(options, function (error) {
Accounts.createUser(options, error => {
if (error) {
loginButtonsSession.errorMessage(error.reason || "Unknown error");
} else {
@@ -443,12 +118,12 @@ var signup = function () {
});
};
var forgotPassword = function () {
const forgotPassword = () => {
loginButtonsSession.resetMessages();
var email = trimmedElementValueById("forgot-password-email");
if (email.indexOf('@') !== -1) {
Accounts.forgotPassword({email: email}, function (error) {
const email = trimmedElementValueById("forgot-password-email");
if (email.includes('@')) {
Accounts.forgotPassword({email: email}, error => {
if (error)
loginButtonsSession.errorMessage(error.reason || "Unknown error");
else
@@ -459,21 +134,21 @@ var forgotPassword = function () {
}
};
var changePassword = function () {
const changePassword = () => {
loginButtonsSession.resetMessages();
// notably not trimmed. a password could (?) start or end with a space
var oldPassword = elementValueById('login-old-password');
const oldPassword = elementValueById('login-old-password');
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
const password = elementValueById('login-password');
if (!validatePassword(password))
return;
if (!matchPasswordAgainIfPresent())
return;
Accounts.changePassword(oldPassword, password, function (error) {
Accounts.changePassword(oldPassword, password, error => {
if (error) {
loginButtonsSession.errorMessage(error.reason || "Unknown error");
} else {
@@ -484,12 +159,12 @@ var changePassword = function () {
});
};
var matchPasswordAgainIfPresent = function () {
const matchPasswordAgainIfPresent = () => {
// notably not trimmed. a password could (?) start or end with a space
var passwordAgain = elementValueById('login-password-again');
const passwordAgain = elementValueById('login-password-again');
if (passwordAgain !== null) {
// notably not trimmed. a password could (?) start or end with a space
var password = elementValueById('login-password');
const password = elementValueById('login-password');
if (password !== passwordAgain) {
loginButtonsSession.errorMessage("Passwords don't match");
return false;
@@ -497,3 +172,341 @@ var matchPasswordAgainIfPresent = function () {
}
return true;
};
// Utility containment function that works with both arrays and single values
const isInPasswordSignupFields = (fieldOrFields) => {
const signupFields = passwordSignupFields();
if (Array.isArray(fieldOrFields)) {
return signupFields.reduce(
(prev, field) => prev && fieldOrFields.includes(field),
true,
)
}
return signupFields.includes(fieldOrFields);
};
// events shared between loginButtonsLoggedOutDropdown and
// loginButtonsLoggedInDropdown
Template.loginButtons.events({
'click #login-name-link, click #login-sign-in-link': () =>
loginButtonsSession.set('dropdownVisible', true),
'click .login-close-text': loginButtonsSession.closeDropdown,
});
//
// loginButtonsLoggedInDropdown template and related
//
Template._loginButtonsLoggedInDropdown.events({
'click #login-buttons-open-change-password': () => {
loginButtonsSession.resetMessages();
loginButtonsSession.set('inChangePasswordFlow', true);
}
});
Template._loginButtonsLoggedInDropdown.helpers({
displayName,
inChangePasswordFlow: () => loginButtonsSession.get('inChangePasswordFlow'),
inMessageOnlyFlow: () => loginButtonsSession.get('inMessageOnlyFlow'),
dropdownVisible: () => loginButtonsSession.get('dropdownVisible'),
});
Template._loginButtonsLoggedInDropdownActions.helpers({
allowChangingPassword: () => {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
const user = Meteor.user();
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
}
});
//
// loginButtonsLoggedOutDropdown template and related
//
Template._loginButtonsLoggedOutDropdown.events({
'click #login-buttons-password': event => {
event.preventDefault();
loginOrSignup();
},
'keypress #forgot-password-email': event => {
if (event.keyCode === 13)
forgotPassword();
},
'click #login-buttons-forgot-password': forgotPassword,
'click #signup-link': () => {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
const username = trimmedElementValueById('login-username');
const email = trimmedElementValueById('login-email');
const usernameOrEmail = trimmedElementValueById('login-username-or-email');
// notably not trimmed. a password could (?) start or end with a space
const password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', true);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (username !== null)
document.getElementById('login-username').value = username;
else if (email !== null)
document.getElementById('login-email').value = email;
else if (usernameOrEmail !== null)
if (!usernameOrEmail.includes('@'))
document.getElementById('login-username').value = usernameOrEmail;
else
document.getElementById('login-email').value = usernameOrEmail;
if (password !== null)
document.getElementById('login-password').value = password;
// Force redrawing the `login-dropdown-list` element because of
// a bizarre Chrome bug in which part of the DIV is not redrawn
// in case you had tried to unsuccessfully log in before
// switching to the signup form.
//
// Found tip on how to force a redraw on
// http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654
const redraw = document.getElementById('login-dropdown-list');
redraw.style.display = 'none';
redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work
redraw.style.display = 'block';
},
'click #forgot-password-link': () => {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
const email = trimmedElementValueById('login-email');
const usernameOrEmail = trimmedElementValueById('login-username-or-email');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', true);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (email !== null)
document.getElementById('forgot-password-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.includes('@'))
document.getElementById('forgot-password-email').value = usernameOrEmail;
},
'click #back-to-login-link': () => {
loginButtonsSession.resetMessages();
const username = trimmedElementValueById('login-username');
const email = trimmedElementValueById('login-email')
|| trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names?
// notably not trimmed. a password could (?) start or end with a space
const password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
if (document.getElementById('login-username') && username !== null)
document.getElementById('login-username').value = username;
if (document.getElementById('login-email') && email !== null)
document.getElementById('login-email').value = email;
const usernameOrEmailInput = document.getElementById('login-username-or-email');
if (usernameOrEmailInput) {
if (email !== null)
usernameOrEmailInput.value = email;
if (username !== null)
usernameOrEmailInput.value = username;
}
if (password !== null)
document.getElementById('login-password').value = password;
},
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': event => {
if (event.keyCode === 13)
loginOrSignup();
}
});
Template._loginButtonsLoggedOutDropdown.helpers({
// additional classes that can be helpful in styling the dropdown
additionalClasses: () => {
if (!hasPasswordService()) {
return false;
} else {
if (loginButtonsSession.get('inSignupFlow')) {
return 'login-form-create-account';
} else if (loginButtonsSession.get('inForgotPasswordFlow')) {
return 'login-form-forgot-password';
} else {
return 'login-form-sign-in';
}
}
},
dropdownVisible: () => loginButtonsSession.get('dropdownVisible'),
hasPasswordService,
});
// return all login services, with password last
Template._loginButtonsLoggedOutAllServices.helpers({
services: getLoginServices,
isPasswordService: function () {
return this.name === 'password';
},
hasOtherServices: () => getLoginServices().length > 1,
hasPasswordService,
});
Template._loginButtonsLoggedOutPasswordService.helpers({
fields: () => {
const loginFields = [
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
autocomplete: 'username email',
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"]
),
},
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
visible: () => isInPasswordSignupFields("USERNAME_ONLY"),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields("EMAIL_ONLY"),
},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
autocomplete: 'current-password',
visible: () => true,
}
];
const signupFields = [
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
visible: () => isInPasswordSignupFields([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
]),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"]
),
},
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields("USERNAME_AND_OPTIONAL_EMAIL"),
},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
autocomplete: 'new-password',
visible: () => true,
},
{fieldName: 'password-again', fieldLabel: 'Password (again)',
inputType: 'password', autocomplete: 'new-password',
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
visible: () => isInPasswordSignupFields(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"]
),
},
];
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
},
inForgotPasswordFlow: () => loginButtonsSession.get('inForgotPasswordFlow'),
inLoginFlow: () =>
!loginButtonsSession.get('inSignupFlow') &&
!loginButtonsSession.get('inForgotPasswordFlow'),
inSignupFlow: () => loginButtonsSession.get('inSignupFlow'),
showCreateAccountLink: () => !Accounts._options.forbidClientAccountCreation,
showForgotPasswordLink: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"]
),
});
Template._loginButtonsFormField.helpers({
inputType: function () {
return this.inputType || "text"
}
});
//
// loginButtonsChangePassword template
//
Template._loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': event => {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': changePassword,
});
Template._loginButtonsChangePassword.helpers({
fields: () => {
const { username, emails } = Meteor.user()
let email;
if (emails) {
email = emails[0].address;
}
return [
// The username and email fields are included here to address an
// accessibility warning in Chrome, but the fields don't actually display.
// The warning states that there should be an optionally hidden
// username/email field on password forms.
// XXX I think we should not use a CSS class here because this is the
// `unstyled` package. So instead we apply an inline style.
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
fieldStyle: 'display: none;', fieldValue: username,
visible: () => isInPasswordSignupFields([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
]),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email', fieldStyle: 'display: none;', fieldValue: email,
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"]
),
},
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
autocomplete: 'current-password', visible: () => true,
},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
autocomplete: 'new-password', visible: () => true,
},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password', autocomplete: 'new-password',
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
visible: () => isInPasswordSignupFields(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"]
),
},
];
}
});

View File

@@ -1,4 +1,4 @@
var VALID_KEYS = [
const VALID_KEYS = [
'dropdownVisible',
// XXX consider replacing these with one key that has an enum for values.
@@ -19,91 +19,100 @@ var VALID_KEYS = [
'configureLoginServiceDialogVisible',
'configureLoginServiceDialogServiceName',
'configureLoginServiceDialogSaveDisabled',
'configureOnDesktopVisible'
'configureOnDesktopVisible',
];
var validateKey = function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Invalid key in loginButtonsSession: " + key);
const validateKey = key => {
if (!VALID_KEYS.includes(key))
throw new Error(`Invalid key in loginButtonsSession: ${key}`);
};
var KEY_PREFIX = "Meteor.loginButtons.";
const KEY_PREFIX = "Meteor.loginButtons.";
// XXX This should probably be package scope rather than exported
// (there was even a comment to that effect here from before we had
// namespacing) but accounts-ui-viewer uses it, so leave it as is for
// now
Accounts._loginButtonsSession = {
set: function(key, value) {
validateKey(key);
if (_.contains(['errorMessage', 'infoMessage'], key))
throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().");
const set = (key, value) => {
validateKey(key);
if (['errorMessage', 'infoMessage'].includes(key))
throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().");
this._set(key, value);
},
_set(key, value);
};
_set: function(key, value) {
Session.set(KEY_PREFIX + key, value);
},
const _set = (key, value) => Session.set(KEY_PREFIX + key, value);
get: function(key) {
validateKey(key);
return Session.get(KEY_PREFIX + key);
},
const get = key => {
validateKey(key);
return Session.get(KEY_PREFIX + key);
};
closeDropdown: function () {
this.set('inSignupFlow', false);
this.set('inForgotPasswordFlow', false);
this.set('inChangePasswordFlow', false);
this.set('inMessageOnlyFlow', false);
this.set('dropdownVisible', false);
this.resetMessages();
},
const closeDropdown = () => {
set('inSignupFlow', false);
set('inForgotPasswordFlow', false);
set('inChangePasswordFlow', false);
set('inMessageOnlyFlow', false);
set('dropdownVisible', false);
resetMessages();
};
infoMessage: function(message) {
this._set("errorMessage", null);
this._set("infoMessage", message);
this.ensureMessageVisible();
},
const infoMessage = message => {
_set("errorMessage", null);
_set("infoMessage", message);
ensureMessageVisible();
};
errorMessage: function(message) {
this._set("errorMessage", message);
this._set("infoMessage", null);
this.ensureMessageVisible();
},
const errorMessage = message => {
_set("errorMessage", message);
_set("infoMessage", null);
ensureMessageVisible();
};
// is there a visible dialog that shows messages (info and error)
isMessageDialogVisible: function () {
return this.get('resetPasswordToken') ||
this.get('enrollAccountToken') ||
this.get('justVerifiedEmail');
},
// is there a visible dialog that shows messages (info and error)
const isMessageDialogVisible = () => {
return get('resetPasswordToken') ||
get('enrollAccountToken') ||
get('justVerifiedEmail');
};
// ensure that somethings displaying a message (info or error) is
// visible. if a dialog with messages is open, do nothing;
// otherwise open the dropdown.
//
// notably this doesn't matter when only displaying a single login
// button since then we have an explicit message dialog
// (_loginButtonsMessageDialog), and dropdownVisible is ignored in
// this case.
ensureMessageVisible: function () {
if (!this.isMessageDialogVisible())
this.set("dropdownVisible", true);
},
// ensure that somethings displaying a message (info or error) is
// visible. If a dialog with messages is open, do nothing;
// otherwise open the dropdown.
//
// Notably this doesn't matter when only displaying a single login
// button since then we have an explicit message dialog
// (_loginButtonsMessageDialog), and dropdownVisible is ignored in
// this case.
const ensureMessageVisible = () => {
if (!isMessageDialogVisible())
set("dropdownVisible", true);
};
resetMessages: function () {
this._set("errorMessage", null);
this._set("infoMessage", null);
},
const resetMessages = () => {
_set("errorMessage", null);
_set("infoMessage", null);
};
configureService: function (name) {
if (Meteor.isCordova) {
this.set('configureOnDesktopVisible', true);
} else {
this.set('configureLoginServiceDialogVisible', true);
this.set('configureLoginServiceDialogServiceName', name);
this.set('configureLoginServiceDialogSaveDisabled', true);
}
const configureService = name => {
if (Meteor.isCordova) {
set('configureOnDesktopVisible', true);
} else {
set('configureLoginServiceDialogVisible', true);
set('configureLoginServiceDialogServiceName', name);
set('configureLoginServiceDialogSaveDisabled', true);
}
};
Accounts._loginButtonsSession = {
set,
_set,
get,
closeDropdown,
infoMessage,
errorMessage,
isMessageDialogVisible,
ensureMessageVisible,
resetMessages,
configureService,
};

View File

@@ -1,8 +1,10 @@
import { getLoginServices } from './login_buttons.js';
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
const loginButtonsSession = Accounts._loginButtonsSession;
var loginResultCallback = function (serviceName, err) {
const loginResultCallback = (serviceName, err) => {
if (!err) {
loginButtonsSession.closeDropdown();
} else if (err instanceof Accounts.LoginCancelledError) {
@@ -12,9 +14,9 @@ var loginResultCallback = function (serviceName, err) {
loginButtonsSession.configureService(serviceName);
} else {
loginButtonsSession.errorMessage(
"No configuration for " + capitalize(serviceName) + ".\n" +
`No configuration for ${capitalize(serviceName)}.\n` +
"Use `ServiceConfiguration` to configure it or " +
"install the `" +serviceName + "-config-ui` package."
`install the \`${serviceName}-config-ui\` package.`
);
}
} else {
@@ -29,26 +31,30 @@ var loginResultCallback = function (serviceName, err) {
// the dialog on a successful login or display the error on a failed
// login).
//
Accounts.onPageLoadLogin(function (attemptInfo) {
Accounts.onPageLoadLogin(attemptInfo => {
// Ignore if we have a left over login attempt for a service that is no longer registered.
if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type))
if (
getLoginServices()
.map(service => service.name)
.includes(attemptInfo.type)
)
loginResultCallback(attemptInfo.type, attemptInfo.error);
});
Template._loginButtonsLoggedOutSingleLoginButton.events({
'click .login-button': function () {
var serviceName = this.name;
const serviceName = this.name;
loginButtonsSession.resetMessages();
// XXX Service providers should be able to specify their
// `Meteor.loginWithX` method name.
var loginWithService = Meteor["loginWith" +
const loginWithService = Meteor[`loginWith${
(serviceName === 'meteor-developer' ?
'MeteorDeveloperAccount' :
capitalize(serviceName))];
capitalize(serviceName))}`];
var options = {}; // use default scope unless specified
const options = {}; // use default scope unless specified
if (Accounts.ui._options.requestPermissions[serviceName])
options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName];
if (Accounts.ui._options.requestOfflineToken[serviceName])
@@ -56,7 +62,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({
if (Accounts.ui._options.forceApprovalPrompt[serviceName])
options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName];
loginWithService(options, function (err) {
loginWithService(options, err => {
loginResultCallback(serviceName, err);
});
}
@@ -64,9 +70,9 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({
Template._loginButtonsLoggedOutSingleLoginButton.helpers({
// not configured and has no config UI
cannotConfigure: function() {
return !ServiceConfiguration.configurations.findOne({service: this.name})
&& !Template._configureLoginServiceDialog.templateForService(this.name);
cannotConfigure: function () {
return !ServiceConfiguration.configurations.findOne({service: this.name}) &&
!Template._configureLoginServiceDialog.templateForService(this.name);
},
configured: function () {
return !!ServiceConfiguration.configurations.findOne({service: this.name});
@@ -83,7 +89,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.helpers({
});
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
str = str == null ? '' : String(str);
const capitalize = input => {
str = input == null ? '' : String(input);
return str.charAt(0).toUpperCase() + str.slice(1);
};

View File

@@ -8,7 +8,7 @@ Package.onUse(function (api) {
'tracker',
'service-configuration',
'accounts-base',
'underscore',
'ecmascript',
'templating@1.2.13',
'session',
], 'client');
@@ -46,7 +46,7 @@ Package.onUse(function (api) {
api.addFiles('login_buttons.import.less');
});
Package.onTest(function (api) {
Package.onTest(api => {
api.use('accounts-ui-unstyled');
api.use('tinytest');
api.addFiles('accounts_ui_tests.js', 'client');

View File

@@ -1,9 +1,9 @@
Package.describe({
summary: "Simple templates to add login widgets to an app",
version: "1.3.0"
version: "1.3.1",
});
Package.onUse(function (api) {
Package.onUse(api => {
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('accounts-ui-unstyled', 'client');

View File

@@ -1,6 +1,6 @@
if (Package['accounts-ui']
&& !Package['service-configuration']
&& !Package.hasOwnProperty('weibo-config-ui')) {
&& !Object.prototype.hasOwnProperty.call(Package, 'weibo-config-ui')) {
console.warn(
"Note: You're using accounts-ui and accounts-weibo,\n" +
"but didn't install the configuration UI for the Weibo\n" +

View File

@@ -1,9 +1,9 @@
Package.describe({
summary: "Login service for Sina Weibo 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.

View File

@@ -1,20 +1,19 @@
Accounts.oauth.registerService('weibo');
if (Meteor.isClient) {
const loginWithWeibo = function(options, callback) {
const loginWithWeibo = (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);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('weibo', loginWithWeibo);
Meteor.loginWithWeibo = function () {
return Accounts.applyLoginFunction('weibo', arguments);
};
Meteor.loginWithWeibo = (...args) =>
Accounts.applyLoginFunction('weibo', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately

View File

@@ -1,5 +1,40 @@
Tinytest.add("binary-heap - simple max-heap tests", function (test) {
var h = new MaxHeap(function (a, b) { return a-b; });
import { MaxHeap } from './max-heap.js';
import { MinMaxHeap } from './min-max-heap.js';
// Based on underscore implementation (Fisher-Yates shuffle)
const shuffle = arr => {
let j = 0;
let temp = null;
for (let i = arr.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1));
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
};
// Based on underscore implementation
const range = (start, stop, step = 1) => {
if (stop == null) {
stop = start || 0;
start = 0;
}
const length = Math.max(Math.ceil((stop - start) / step), 0);
const range = Array(length);
for (let idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
};
Tinytest.add("binary-heap - simple max-heap tests", test => {
const h = new MaxHeap((a, b) => a - b);
h.set("a", 1);
h.set("b", 233);
h.set("c", -122);
@@ -28,29 +63,29 @@ Tinytest.add("binary-heap - simple max-heap tests", function (test) {
test.equal(h.maxElementId(), "a");
});
Tinytest.add("binary-heap - big test for max-heap", function (test) {
var positiveNumbers = _.shuffle(_.range(1, 41));
var negativeNumbers = _.shuffle(_.range(-1, -41, -1));
var allNumbers = negativeNumbers.concat(positiveNumbers);
Tinytest.add("binary-heap - big test for max-heap", test => {
const positiveNumbers = shuffle(range(1, 41));
const negativeNumbers = shuffle(range(-1, -41, -1));
const allNumbers = [...negativeNumbers, ...positiveNumbers];
var heap = new MaxHeap(function (a, b) { return a-b; });
var output = [];
const heap = new MaxHeap((a, b) => a - b);
const output = [];
_.each(allNumbers, function (n) { heap.set(n, n); });
allNumbers.forEach(n => heap.set(n, n));
_.times(positiveNumbers.length + negativeNumbers.length, function () {
var maxId = heap.maxElementId();
allNumbers.forEach(() => {
const maxId = heap.maxElementId();
output.push(heap.get(maxId));
heap.remove(maxId);
});
allNumbers.sort(function (a, b) { return b-a; });
allNumbers.sort((a, b) => b - a);
test.equal(output, allNumbers);
});
Tinytest.add("binary-heap - min-max heap tests", function (test) {
var h = new MinMaxHeap(function (a, b) { return a-b; });
Tinytest.add("binary-heap - min-max heap tests", test => {
const h = new MinMaxHeap((a, b) => a - b);
h.set("a", 1);
h.set("b", 233);
h.set("c", -122);
@@ -81,33 +116,33 @@ Tinytest.add("binary-heap - min-max heap tests", function (test) {
test.equal(h.minElementId(), "a");
});
Tinytest.add("binary-heap - big test for min-max-heap", function (test) {
var N = 500;
var positiveNumbers = _.shuffle(_.range(1, N + 1));
var negativeNumbers = _.shuffle(_.range(-1, -N - 1, -1));
var allNumbers = positiveNumbers.concat(negativeNumbers);
Tinytest.add("binary-heap - big test for min-max-heap", test => {
const N = 500;
const positiveNumbers = shuffle(range(1, N + 1));
const negativeNumbers = shuffle(range(-1, -N - 1, -1));
const allNumbers = [...positiveNumbers, ...negativeNumbers];
var heap = new MinMaxHeap(function (a, b) { return a-b; });
var output = [];
const heap = new MinMaxHeap((a, b) => a - b);
let output = [];
var initialSets = _.clone(allNumbers);
_.each(allNumbers, function (n) {
const initialSets = [...allNumbers];
allNumbers.forEach(n => {
heap.set(n, n);
heap._selfCheck();
heap._minHeap._selfCheck();
});
allNumbers = _.shuffle(allNumbers);
var secondarySets = _.clone(allNumbers);
shuffle(allNumbers);
const secondarySets = [...allNumbers];
_.each(allNumbers, function (n) {
allNumbers.forEach(n => {
heap.set(-n, n);
heap._selfCheck();
heap._minHeap._selfCheck();
});
_.times(positiveNumbers.length + negativeNumbers.length, function () {
var minId = heap.minElementId();
allNumbers.forEach(() => {
const minId = heap.minElementId();
output.push(heap.get(minId));
heap.remove(minId);
heap._selfCheck(); heap._minHeap._selfCheck();
@@ -115,19 +150,19 @@ Tinytest.add("binary-heap - big test for min-max-heap", function (test) {
test.equal(heap.size(), 0);
allNumbers.sort(function (a, b) { return a-b; });
allNumbers.sort((a, b) => a - b);
var initialTestText = "initial sets: " + initialSets.toString() +
"; secondary sets: " + secondarySets.toString();
const initialTestText = `initial sets: ${initialSets.toString()}` +
`; secondary sets: ${secondarySets.toString()}`;
test.equal(output, allNumbers, initialTestText);
_.each(initialSets, function (n) { heap.set(n, n); })
_.each(secondarySets, function (n) { heap.set(-n, n); });
initialSets.forEach(n => heap.set(n, n));
secondarySets.forEach(n => heap.set(-n, n));
allNumbers.sort(function (a, b) { return b-a; });
allNumbers.sort((a, b) => b - a);
output = [];
_.times(positiveNumbers.length + negativeNumbers.length, function () {
var maxId = heap.maxElementId();
allNumbers.forEach(() => {
const maxId = heap.maxElementId();
output.push(heap.get(maxId));
heap.remove(maxId);
heap._selfCheck(); heap._minHeap._selfCheck();
@@ -135,4 +170,3 @@ Tinytest.add("binary-heap - big test for min-max-heap", function (test) {
test.equal(output, allNumbers, initialTestText);
});

View File

@@ -0,0 +1,3 @@
export { MaxHeap } from './max-heap.js';
export { MinHeap } from './min-heap.js';
export { MinMaxHeap } from './min-max-heap.js';

View File

@@ -8,219 +8,210 @@
// each value is retained
// - IdMap - Constructor - Optional - custom IdMap class to store id->index
// mappings internally. Standard IdMap is used by default.
MaxHeap = function (comparator, options) {
if (! _.isFunction(comparator))
throw new Error('Passed comparator is invalid, should be a comparison function');
var self = this;
export class MaxHeap {
constructor(comparator, options = {}) {
if (typeof comparator !== 'function') {
throw new Error('Passed comparator is invalid, should be a comparison function');
}
// a C-style comparator that is given two values and returns a number,
// negative if the first value is less than the second, positive if the second
// value is greater than the first and zero if they are equal.
self._comparator = comparator;
// a C-style comparator that is given two values and returns a number,
// negative if the first value is less than the second, positive if the second
// value is greater than the first and zero if they are equal.
this._comparator = comparator;
options = _.defaults(options || {}, { IdMap: IdMap });
if (! options.IdMap) {
options.IdMap = IdMap;
}
// _heapIdx maps an id to an index in the Heap array the corresponding value
// is located on.
self._heapIdx = new options.IdMap;
// _heapIdx maps an id to an index in the Heap array the corresponding value
// is located on.
this._heapIdx = new options.IdMap;
// The Heap data-structure implemented as a 0-based contiguous array where
// every item on index idx is a node in a complete binary tree. Every node can
// have children on indexes idx*2+1 and idx*2+2, except for the leaves. Every
// node has a parent on index (idx-1)/2;
self._heap = [];
// The Heap data-structure implemented as a 0-based contiguous array where
// every item on index idx is a node in a complete binary tree. Every node can
// have children on indexes idx*2+1 and idx*2+2, except for the leaves. Every
// node has a parent on index (idx-1)/2;
this._heap = [];
// If the initial array is passed, we can build the heap in linear time
// complexity (O(N)) compared to linearithmic time complexity (O(nlogn)) if
// we push elements one by one.
if (_.isArray(options.initData))
self._initFromData(options.initData);
};
// If the initial array is passed, we can build the heap in linear time
// complexity (O(N)) compared to linearithmic time complexity (O(nlogn)) if
// we push elements one by one.
if (Array.isArray(options.initData)) {
this._initFromData(options.initData);
}
}
_.extend(MaxHeap.prototype, {
// Builds a new heap in-place in linear time based on passed data
_initFromData: function (data) {
var self = this;
_initFromData(data) {
this._heap = data.map(({ id, value }) => ({ id, value }));
self._heap = _.map(data, function (o) {
return { id: o.id, value: o.value };
});
data.forEach(({ id }, i) => this._heapIdx.set(id, i));
_.each(data, function (o, i) {
self._heapIdx.set(o.id, i);
});
if (! data.length)
if (! data.length) {
return;
}
// start from the first non-leaf - the parent of the last leaf
for (var i = parentIdx(data.length - 1); i >= 0; i--)
self._downHeap(i);
},
for (let i = parentIdx(data.length - 1); i >= 0; i--) {
this._downHeap(i);
}
}
_downHeap: function (idx) {
var self = this;
_downHeap(idx) {
while (leftChildIdx(idx) < this.size()) {
const left = leftChildIdx(idx);
const right = rightChildIdx(idx);
let largest = idx;
while (leftChildIdx(idx) < self.size()) {
var left = leftChildIdx(idx);
var right = rightChildIdx(idx);
var largest = idx;
if (left < self.size()) {
largest = self._maxIndex(largest, left);
}
if (right < self.size()) {
largest = self._maxIndex(largest, right);
if (left < this.size()) {
largest = this._maxIndex(largest, left);
}
if (largest === idx)
if (right < this.size()) {
largest = this._maxIndex(largest, right);
}
if (largest === idx) {
break;
}
self._swap(largest, idx);
this._swap(largest, idx);
idx = largest;
}
},
_upHeap: function (idx) {
var self = this;
}
_upHeap(idx) {
while (idx > 0) {
var parent = parentIdx(idx);
if (self._maxIndex(parent, idx) === idx) {
self._swap(parent, idx)
const parent = parentIdx(idx);
if (this._maxIndex(parent, idx) === idx) {
this._swap(parent, idx)
idx = parent;
} else {
break;
}
}
},
}
_maxIndex: function (idxA, idxB) {
var self = this;
var valueA = self._get(idxA);
var valueB = self._get(idxB);
return self._comparator(valueA, valueB) >= 0 ? idxA : idxB;
},
_maxIndex(idxA, idxB) {
const valueA = this._get(idxA);
const valueB = this._get(idxB);
return this._comparator(valueA, valueB) >= 0 ? idxA : idxB;
}
// Internal: gets raw data object placed on idxth place in heap
_get: function (idx) {
var self = this;
return self._heap[idx].value;
},
_get(idx) {
return this._heap[idx].value;
}
_swap: function (idxA, idxB) {
var self = this;
var recA = self._heap[idxA];
var recB = self._heap[idxB];
_swap(idxA, idxB) {
const recA = this._heap[idxA];
const recB = this._heap[idxB];
self._heapIdx.set(recA.id, idxB);
self._heapIdx.set(recB.id, idxA);
this._heapIdx.set(recA.id, idxB);
this._heapIdx.set(recB.id, idxA);
self._heap[idxA] = recB;
self._heap[idxB] = recA;
},
this._heap[idxA] = recB;
this._heap[idxB] = recA;
}
get: function (id) {
var self = this;
if (! self.has(id))
return null;
return self._get(self._heapIdx.get(id));
},
set: function (id, value) {
var self = this;
get(id) {
return this.has(id) ?
this._get(this._heapIdx.get(id)) :
null;
}
if (self.has(id)) {
if (self.get(id) === value)
set(id, value) {
if (this.has(id)) {
if (this.get(id) === value) {
return;
}
var idx = self._heapIdx.get(id);
self._heap[idx].value = value;
const idx = this._heapIdx.get(id);
this._heap[idx].value = value;
// Fix the new value's position
// Either bubble new value up if it is greater than its parent
self._upHeap(idx);
this._upHeap(idx);
// or bubble it down if it is smaller than one of its children
self._downHeap(idx);
this._downHeap(idx);
} else {
self._heapIdx.set(id, self._heap.length);
self._heap.push({ id: id, value: value });
self._upHeap(self._heap.length - 1);
this._heapIdx.set(id, this._heap.length);
this._heap.push({ id, value });
this._upHeap(this._heap.length - 1);
}
},
remove: function (id) {
var self = this;
}
if (self.has(id)) {
var last = self._heap.length - 1;
var idx = self._heapIdx.get(id);
remove(id) {
if (this.has(id)) {
const last = this._heap.length - 1;
const idx = this._heapIdx.get(id);
if (idx !== last) {
self._swap(idx, last);
self._heap.pop();
self._heapIdx.remove(id);
this._swap(idx, last);
this._heap.pop();
this._heapIdx.remove(id);
// Fix the swapped value's position
self._upHeap(idx);
self._downHeap(idx);
this._upHeap(idx);
this._downHeap(idx);
} else {
self._heap.pop();
self._heapIdx.remove(id);
this._heap.pop();
this._heapIdx.remove(id);
}
}
},
has: function (id) {
var self = this;
return self._heapIdx.has(id);
},
empty: function () {
var self = this;
return !self.size();
},
clear: function () {
var self = this;
self._heap = [];
self._heapIdx.clear();
},
// iterate over values in no particular order
forEach: function (iterator) {
var self = this;
_.each(self._heap, function (obj) {
return iterator(obj.value, obj.id);
});
},
size: function () {
var self = this;
return self._heap.length;
},
setDefault: function (id, def) {
var self = this;
if (self.has(id))
return self.get(id);
self.set(id, def);
return def;
},
clone: function () {
var self = this;
var clone = new MaxHeap(self._comparator, self._heap);
return clone;
},
maxElementId: function () {
var self = this;
return self.size() ? self._heap[0].id : null;
},
_selfCheck: function () {
var self = this;
for (var i = 1; i < self._heap.length; i++)
if (self._maxIndex(parentIdx(i), i) !== parentIdx(i))
throw new Error("An item with id " + self._heap[i].id +
" has a parent younger than it: " +
self._heap[parentIdx(i)].id);
}
});
function leftChildIdx (i) { return i * 2 + 1; }
function rightChildIdx (i) { return i * 2 + 2; }
function parentIdx (i) { return (i - 1) >> 1; }
has(id) {
return this._heapIdx.has(id);
}
empty() {
return !this.size();
}
clear() {
this._heap = [];
this._heapIdx.clear();
}
// iterate over values in no particular order
forEach(iterator) {
this._heap.forEach(obj => iterator(obj.value, obj.id));
}
size() {
return this._heap.length;
}
setDefault(id, def) {
if (this.has(id)) {
return this.get(id);
}
this.set(id, def);
return def;
}
clone() {
const clone = new MaxHeap(this._comparator, this._heap);
return clone;
}
maxElementId() {
return this.size() ? this._heap[0].id : null;
}
_selfCheck() {
for (let i = 1; i < this._heap.length; i++) {
if (this._maxIndex(parentIdx(i), i) !== parentIdx(i)) {
throw new Error(`An item with id ${this._heap[i].id}` +
" has a parent younger than it: " +
this._heap[parentIdx(i)].id);
}
}
}
}
const leftChildIdx = i => i * 2 + 1;
const rightChildIdx = i => i * 2 + 2;
const parentIdx = i => (i - 1) >> 1;

View File

@@ -1,19 +1,15 @@
MinHeap = function (comparator, options) {
var self = this;
MaxHeap.call(self, function (a, b) {
return -comparator(a, b);
}, options);
};
import { MaxHeap } from './max-heap.js';
Meteor._inherits(MinHeap, MaxHeap);
_.extend(MinHeap.prototype, {
maxElementId: function () {
throw new Error("Cannot call maxElementId on MinHeap");
},
minElementId: function () {
var self = this;
return MaxHeap.prototype.maxElementId.call(self);
export class MinHeap extends MaxHeap {
constructor(comparator, options) {
super((a, b) => -comparator(a, b), options);
}
});
maxElementId() {
throw new Error("Cannot call maxElementId on MinHeap");
}
minElementId() {
return super.maxElementId();
}
};

View File

@@ -1,3 +1,6 @@
import { MaxHeap } from './max-heap.js';
import { MinHeap } from './min-heap.js';
// This implementation of Min/Max-Heap is just a subclass of Max-Heap
// with a Min-Heap as an encapsulated property.
//
@@ -10,44 +13,39 @@
// (http://www.cs.otago.ac.nz/staffpriv/mike/Papers/MinMaxHeaps/MinMaxHeaps.pdf)
// and Interval Heaps
// (http://www.cise.ufl.edu/~sahni/dsaac/enrich/c13/double.htm)
MinMaxHeap = function (comparator, options) {
var self = this;
MaxHeap.call(self, comparator, options);
self._minHeap = new MinHeap(comparator, options);
};
Meteor._inherits(MinMaxHeap, MaxHeap);
_.extend(MinMaxHeap.prototype, {
set: function (id, value) {
var self = this;
MaxHeap.prototype.set.apply(self, arguments);
self._minHeap.set(id, value);
},
remove: function (id) {
var self = this;
MaxHeap.prototype.remove.apply(self, arguments);
self._minHeap.remove(id);
},
clear: function () {
var self = this;
MaxHeap.prototype.clear.apply(self, arguments);
self._minHeap.clear();
},
setDefault: function (id, def) {
var self = this;
MaxHeap.prototype.setDefault.apply(self, arguments);
return self._minHeap.setDefault(id, def);
},
clone: function () {
var self = this;
var clone = new MinMaxHeap(self._comparator, self._heap);
return clone;
},
minElementId: function () {
var self = this;
return self._minHeap.minElementId();
export class MinMaxHeap extends MaxHeap {
constructor(comparator, options) {
super(comparator, options);
this._minHeap = new MinHeap(comparator, options);
}
});
set(...args) {
super.set(...args);
this._minHeap.set(...args);
}
remove(...args) {
super.remove(...args);
this._minHeap.remove(...args);
}
clear(...args) {
super.clear(...args);
this._minHeap.clear(...args);
}
setDefault(...args) {
super.setDefault(...args);
return this._minHeap.setDefault(...args);
}
clone() {
const clone = new MinMaxHeap(this._comparator, this._heap);
return clone;
}
minElementId() {
return this._minHeap.minElementId();
}
};

View File

@@ -1,22 +1,15 @@
Package.describe({
summary: "Binary Heap datastructure implementation",
version: '1.0.10'
version: '1.0.11'
});
Package.onUse(function (api) {
api.export('MaxHeap');
api.export('MinHeap');
api.export('MinMaxHeap');
api.use(['underscore', 'id-map']);
api.addFiles(['max-heap.js', 'min-heap.js', 'min-max-heap.js']);
Package.onUse(api => {
api.export(['MaxHeap', 'MinHeap', 'MinMaxHeap']);
api.use(['id-map', 'ecmascript']);
api.mainModule('binary-heap.js');
});
Package.onTest(function (api) {
api.use([
'tinytest',
'underscore',
'binary-heap'
]);
Package.onTest(api => {
api.use(['tinytest', 'binary-heap', 'ecmascript']);
api.addFiles('binary-heap-tests.js');
});

View File

@@ -14,8 +14,8 @@ var Fiber = Npm.require('fibers');
// Represents a single document in a SessionCollectionView
var SessionDocumentView = function () {
var self = this;
self.existsIn = {}; // set of subscriptionHandle
self.dataByKey = {}; // key-> [ {subscriptionHandle, value} by precedence]
self.existsIn = new Set(); // set of subscriptionHandle
self.dataByKey = new Map(); // key-> [ {subscriptionHandle, value} by precedence]
};
DDPServer._SessionDocumentView = SessionDocumentView;
@@ -26,7 +26,7 @@ _.extend(SessionDocumentView.prototype, {
getFields: function () {
var self = this;
var ret = {};
_.each(self.dataByKey, function (precedenceList, key) {
self.dataByKey.forEach(function (precedenceList, key) {
ret[key] = precedenceList[0].value;
});
return ret;
@@ -37,7 +37,7 @@ _.extend(SessionDocumentView.prototype, {
// Publish API ignores _id if present in fields
if (key === "_id")
return;
var precedenceList = self.dataByKey[key];
var precedenceList = self.dataByKey.get(key);
// It's okay to clear fields that didn't exist. No need to throw
// an error.
@@ -56,8 +56,8 @@ _.extend(SessionDocumentView.prototype, {
break;
}
}
if (_.isEmpty(precedenceList)) {
delete self.dataByKey[key];
if (precedenceList.length === 0) {
self.dataByKey.delete(key);
changeCollector[key] = undefined;
} else if (removedValue !== undefined &&
!EJSON.equals(removedValue, precedenceList[0].value)) {
@@ -75,17 +75,17 @@ _.extend(SessionDocumentView.prototype, {
// Don't share state with the data passed in by the user.
value = EJSON.clone(value);
if (!_.has(self.dataByKey, key)) {
self.dataByKey[key] = [{subscriptionHandle: subscriptionHandle,
value: value}];
if (!self.dataByKey.has(key)) {
self.dataByKey.set(key, [{subscriptionHandle: subscriptionHandle,
value: value}]);
changeCollector[key] = value;
return;
}
var precedenceList = self.dataByKey[key];
var precedenceList = self.dataByKey.get(key);
var elt;
if (!isAdd) {
elt = _.find(precedenceList, function (precedence) {
return precedence.subscriptionHandle === subscriptionHandle;
elt = precedenceList.find(function (precedence) {
return precedence.subscriptionHandle === subscriptionHandle;
});
}
@@ -112,7 +112,7 @@ _.extend(SessionDocumentView.prototype, {
var SessionCollectionView = function (collectionName, sessionCallbacks) {
var self = this;
self.collectionName = collectionName;
self.documents = {};
self.documents = new Map();
self.callbacks = sessionCallbacks;
};
@@ -123,12 +123,12 @@ _.extend(SessionCollectionView.prototype, {
isEmpty: function () {
var self = this;
return _.isEmpty(self.documents);
return self.documents.size === 0;
},
diff: function (previous) {
var self = this;
DiffSequence.diffObjects(previous.documents, self.documents, {
DiffSequence.diffMaps(previous.documents, self.documents, {
both: _.bind(self.diffDocument, self),
rightOnly: function (id, nowDV) {
@@ -161,14 +161,14 @@ _.extend(SessionCollectionView.prototype, {
added: function (subscriptionHandle, id, fields) {
var self = this;
var docView = self.documents[id];
var docView = self.documents.get(id);
var added = false;
if (!docView) {
added = true;
docView = new SessionDocumentView();
self.documents[id] = docView;
self.documents.set(id, docView);
}
docView.existsIn[subscriptionHandle] = true;
docView.existsIn.add(subscriptionHandle);
var changeCollector = {};
_.each(fields, function (value, key) {
docView.changeField(
@@ -183,7 +183,7 @@ _.extend(SessionCollectionView.prototype, {
changed: function (subscriptionHandle, id, changed) {
var self = this;
var changedResult = {};
var docView = self.documents[id];
var docView = self.documents.get(id);
if (!docView)
throw new Error("Could not find element with id " + id + " to change");
_.each(changed, function (value, key) {
@@ -197,21 +197,21 @@ _.extend(SessionCollectionView.prototype, {
removed: function (subscriptionHandle, id) {
var self = this;
var docView = self.documents[id];
var docView = self.documents.get(id);
if (!docView) {
var err = new Error("Removed nonexistent document " + id);
throw err;
}
delete docView.existsIn[subscriptionHandle];
if (_.isEmpty(docView.existsIn)) {
docView.existsIn.delete(subscriptionHandle);
if (docView.existsIn.size === 0) {
// it is gone from everyone
self.callbacks.removed(self.collectionName, id);
delete self.documents[id];
self.documents.delete(id);
} else {
var changed = {};
// remove this subscription from every precedence list
// and record the changes
_.each(docView.dataByKey, function (precedenceList, key) {
docView.dataByKey.forEach(function (precedenceList, key) {
docView.clearField(subscriptionHandle, key, changed);
});
@@ -242,12 +242,12 @@ var Session = function (server, version, socket, options) {
self.workerRunning = false;
// Sub objects for active subscriptions
self._namedSubs = {};
self._namedSubs = new Map();
self._universalSubs = [];
self.userId = null;
self.collectionViews = {};
self.collectionViews = new Map();
// Set this to false to not send messages when collectionViews are
// modified. This is done when rerunning subs in _setUserId and those messages
@@ -373,12 +373,12 @@ _.extend(Session.prototype, {
getCollectionView: function (collectionName) {
var self = this;
if (_.has(self.collectionViews, collectionName)) {
return self.collectionViews[collectionName];
}
var ret = new SessionCollectionView(collectionName,
var ret = self.collectionViews.get(collectionName);
if (!ret) {
ret = new SessionCollectionView(collectionName,
self.getSendCallbacks());
self.collectionViews[collectionName] = ret;
self.collectionViews.set(collectionName, ret);
}
return ret;
},
@@ -393,7 +393,7 @@ _.extend(Session.prototype, {
var view = self.getCollectionView(collectionName);
view.removed(subscriptionHandle, id);
if (view.isEmpty()) {
delete self.collectionViews[collectionName];
self.collectionViews.delete(collectionName);
}
},
@@ -428,7 +428,7 @@ _.extend(Session.prototype, {
// Drop the merge box data immediately.
self.inQueue = null;
self.collectionViews = {};
self.collectionViews = new Map();
if (self.heartbeat) {
self.heartbeat.stop();
@@ -585,7 +585,7 @@ _.extend(Session.prototype, {
return;
}
if (_.has(self._namedSubs, msg.id))
if (self._namedSubs.has(msg.id))
// subs are idempotent, or rather, they are ignored if a sub
// with that id already exists. this is important during
// reconnect.
@@ -753,23 +753,23 @@ _.extend(Session.prototype, {
_eachSub: function (f) {
var self = this;
_.each(self._namedSubs, f);
_.each(self._universalSubs, f);
self._namedSubs.forEach(f);
self._universalSubs.forEach(f);
},
_diffCollectionViews: function (beforeCVs) {
var self = this;
DiffSequence.diffObjects(beforeCVs, self.collectionViews, {
DiffSequence.diffMaps(beforeCVs, self.collectionViews, {
both: function (collectionName, leftValue, rightValue) {
rightValue.diff(leftValue);
},
rightOnly: function (collectionName, rightValue) {
_.each(rightValue.documents, function (docView, id) {
rightValue.documents.forEach(function (docView, id) {
self.sendAdded(collectionName, id, docView.getFields());
});
},
leftOnly: function (collectionName, leftValue) {
_.each(leftValue.documents, function (doc, id) {
leftValue.documents.forEach(function (doc, id) {
self.sendRemoved(collectionName, id);
});
}
@@ -806,7 +806,7 @@ _.extend(Session.prototype, {
// update the userId.
self._isSending = false;
var beforeCVs = self.collectionViews;
self.collectionViews = {};
self.collectionViews = new Map();
self.userId = userId;
// _setUserId is normally called from a Meteor method with
@@ -816,14 +816,15 @@ _.extend(Session.prototype, {
DDP._CurrentMethodInvocation.withValue(undefined, function () {
// Save the old named subs, and reset to having no subscriptions.
var oldNamedSubs = self._namedSubs;
self._namedSubs = {};
self._namedSubs = new Map();
self._universalSubs = [];
_.each(oldNamedSubs, function (sub, subscriptionId) {
self._namedSubs[subscriptionId] = sub._recreate();
oldNamedSubs.forEach(function (sub, subscriptionId) {
var newSub = sub._recreate();
self._namedSubs.set(subscriptionId, newSub);
// nb: if the handler throws or calls this.error(), it will in fact
// immediately send its 'nosub'. This is OK, though.
self._namedSubs[subscriptionId]._runHandler();
newSub._runHandler();
});
// Allow newly-created universal subs to be started on our connection in
@@ -852,7 +853,7 @@ _.extend(Session.prototype, {
var sub = new Subscription(
self, handler, subId, params, name);
if (subId)
self._namedSubs[subId] = sub;
self._namedSubs.set(subId, sub);
else
self._universalSubs.push(sub);
@@ -864,12 +865,14 @@ _.extend(Session.prototype, {
var self = this;
var subName = null;
if (subId && self._namedSubs[subId]) {
subName = self._namedSubs[subId]._name;
self._namedSubs[subId]._removeAllDocuments();
self._namedSubs[subId]._deactivate();
delete self._namedSubs[subId];
if (subId) {
var maybeSub = self._namedSubs.get(subId);
if (maybeSub) {
subName = maybeSub._name;
maybeSub._removeAllDocuments();
maybeSub._deactivate();
self._namedSubs.delete(subId);
}
}
var response = {msg: 'nosub', id: subId};
@@ -889,12 +892,12 @@ _.extend(Session.prototype, {
_deactivateAllSubscriptions: function () {
var self = this;
_.each(self._namedSubs, function (sub, id) {
self._namedSubs.forEach(function (sub, id) {
sub._deactivate();
});
self._namedSubs = {};
self._namedSubs = new Map();
_.each(self._universalSubs, function (sub) {
self._universalSubs.forEach(function (sub) {
sub._deactivate();
});
self._universalSubs = [];
@@ -993,7 +996,7 @@ var Subscription = function (
// the set of (collection, documentid) that this subscription has
// an opinion about
self._documents = {};
self._documents = new Map();
// remember if we are ready.
self._ready = false;
@@ -1160,10 +1163,8 @@ _.extend(Subscription.prototype, {
_removeAllDocuments: function () {
var self = this;
Meteor._noYieldsAllowed(function () {
_.each(self._documents, function(collectionDocs, collectionName) {
// Iterate over _.keys instead of the dictionary itself, since we'll be
// mutating it.
_.each(_.keys(collectionDocs), function (strId) {
self._documents.forEach(function (collectionDocs, collectionName) {
collectionDocs.forEach(function (strId) {
self.removed(collectionName, self._idFilter.idParse(strId));
});
});
@@ -1252,7 +1253,12 @@ _.extend(Subscription.prototype, {
if (self._isDeactivated())
return;
id = self._idFilter.idStringify(id);
Meteor._ensure(self._documents, collectionName)[id] = true;
let ids = self._documents.get(collectionName);
if (ids == null) {
ids = new Set();
self._documents.set(collectionName, ids);
}
ids.add(id);
self._session.added(self._subscriptionHandle, collectionName, id, fields);
},
@@ -1288,7 +1294,7 @@ _.extend(Subscription.prototype, {
id = self._idFilter.idStringify(id);
// We don't bother to delete sets of things in a collection if the
// collection is empty. It could break _removeAllDocuments.
delete self._documents[collectionName][id];
self._documents.get(collectionName).delete(id);
self._session.removed(self._subscriptionHandle, collectionName, id);
},
@@ -1350,7 +1356,7 @@ Server = function (options) {
self.method_handlers = {};
self.sessions = {}; // map from id to session
self.sessions = new Map(); // map from id to session
self.stream_server = new StreamServer;
@@ -1471,7 +1477,7 @@ _.extend(Server.prototype, {
// Note: Troposphere depends on the ability to mutate
// Meteor.server.options.heartbeatTimeout! This is a hack, but it's life.
socket._meteorSession = new Session(self, version, socket, self.options);
self.sessions[socket._meteorSession.id] = socket._meteorSession;
self.sessions.set(socket._meteorSession.id, socket._meteorSession);
self.onConnectionHook.each(function (callback) {
if (socket._meteorSession)
callback(socket._meteorSession.connectionHandle);
@@ -1552,7 +1558,7 @@ _.extend(Server.prototype, {
// Spin up the new publisher on any existing session too. Run each
// session's subscription in a new Fiber, so that there's no change for
// self.sessions to change while we're running this loop.
_.each(self.sessions, function (session) {
self.sessions.forEach(function (session) {
if (!session._dontStartNewUniversalSubs) {
Fiber(function() {
session._startSubscription(handler);
@@ -1570,9 +1576,7 @@ _.extend(Server.prototype, {
_removeSession: function (session) {
var self = this;
if (self.sessions[session.id]) {
delete self.sessions[session.id];
}
self.sessions.delete(session.id);
},
/**
@@ -1692,7 +1696,7 @@ _.extend(Server.prototype, {
_urlForSession: function (sessionId) {
var self = this;
var session = self.sessions[sessionId];
var session = self.sessions.get(sessionId);
if (session)
return session._socketUrl;
else

View File

@@ -238,6 +238,24 @@ DiffSequence.diffObjects = function (left, right, callbacks) {
}
};
DiffSequence.diffMaps = function (left, right, callbacks) {
left.forEach(function (leftValue, key) {
if (right.has(key)){
callbacks.both && callbacks.both(key, leftValue, right.get(key));
} else {
callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
}
});
if (callbacks.rightOnly) {
right.forEach(function (rightValue, key) {
if (!left.has(key)){
callbacks.rightOnly(key, rightValue);
}
});
}
};
DiffSequence.makeChangedFields = function (newDoc, oldDoc) {
var fields = {};

View File

@@ -1,12 +1,8 @@
Template.configureLoginServiceDialogForFacebook.helpers({
siteUrl: function () {
return Meteor.absoluteUrl();
}
siteUrl: () => Meteor.absoluteUrl(),
});
Template.configureLoginServiceDialogForFacebook.fields = function () {
return [
{property: 'appId', label: 'App ID'},
{property: 'secret', label: 'App Secret'}
];
};
Template.configureLoginServiceDialogForFacebook.fields = () => [
{property: 'appId', label: 'App ID'},
{property: 'secret', label: 'App Secret'}
];

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: "Blaze configuration templates for Facebook OAuth.",
version: "1.0.1"
version: "1.0.2",
});
Package.onUse(function(api) {
Package.onUse(api => {
api.use('ecmascript', 'client');
api.use('templating@1.2.13', 'client');
api.addFiles('facebook_login_button.css', 'client');

View File

@@ -6,46 +6,46 @@ Facebook = {};
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Facebook.requestCredential = function (options, credentialRequestCompleteCallback) {
Facebook.requestCredential = (options, credentialRequestCompleteCallback) => {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
options = {};
}
var config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
if (!config) {
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
new ServiceConfiguration.ConfigError());
return;
}
var credentialToken = Random.secret();
var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(navigator.userAgent);
var display = mobile ? 'touch' : 'popup';
const credentialToken = Random.secret();
const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(navigator.userAgent);
const display = mobile ? 'touch' : 'popup';
var scope = "email";
let scope = "email";
if (options && options.requestPermissions)
scope = options.requestPermissions.join(',');
var loginStyle = OAuth._loginStyle('facebook', config, options);
const loginStyle = OAuth._loginStyle('facebook', config, options);
var loginUrl =
'https://www.facebook.com/v3.0/dialog/oauth?client_id=' + config.appId +
'&redirect_uri=' + OAuth._redirectUri('facebook', config) +
'&display=' + display + '&scope=' + scope +
'&state=' + OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl);
let loginUrl =
`https://www.facebook.com/v3.0/dialog/oauth?client_id=${config.appId}` +
`&redirect_uri=${OAuth._redirectUri('facebook', config)}` +
`&display=${display}&scope=${scope}` +
`&state=${OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl)}`;
// Handle authentication type (e.g. for force login you need auth_type: "reauthenticate")
if (options && options.auth_type) {
loginUrl += "&auth_type=" + encodeURIComponent(options.auth_type);
loginUrl += `&auth_type=${encodeURIComponent(options.auth_type)}`;
}
OAuth.launchLogin({
loginService: "facebook",
loginStyle: loginStyle,
loginUrl: loginUrl,
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
credentialToken: credentialToken
loginStyle,
loginUrl,
credentialRequestCompleteCallback,
credentialToken,
});
};

View File

@@ -1,39 +1,39 @@
Facebook = {};
var crypto = Npm.require('crypto');
import crypto from 'crypto';
Facebook.handleAuthFromAccessToken = function handleAuthFromAccessToken(accessToken, expiresAt) {
Facebook.handleAuthFromAccessToken = (accessToken, expiresAt) => {
// include basic fields from facebook
// https://developers.facebook.com/docs/facebook-login/permissions/
var whitelisted = ['id', 'email', 'name', 'first_name', 'last_name',
const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name',
'middle_name', 'name_format', 'picture', 'short_name', 'age_range',
'birthday', 'friends', 'gender', 'hometown', 'link', 'location'];
'birthday', 'friends', 'gender', 'hometown', 'link', 'location', 'locale'];
var identity = getIdentity(accessToken, whitelisted);
const identity = getIdentity(accessToken, whitelisted);
var serviceData = {
accessToken: accessToken,
expiresAt: expiresAt
const fields = {};
whitelisted.forEach(field => fields[field] = identity[field]);
const serviceData = {
accessToken,
expiresAt,
...fields,
};
var fields = _.pick(identity, whitelisted);
_.extend(serviceData, fields);
return {
serviceData: serviceData,
serviceData,
options: {profile: {name: identity.name}}
};
};
OAuth.registerService('facebook', 2, null, function(query) {
var response = getTokenResponse(query);
var accessToken = response.accessToken;
var expiresIn = response.expiresIn;
OAuth.registerService('facebook', 2, null, query => {
const response = getTokenResponse(query);
const { accessToken } = response;
const { expiresIn } = response;
return Facebook.handleAuthFromAccessToken(accessToken, (+new Date) + (1000 * expiresIn));
});
// checks whether a string parses as JSON
var isJSON = function (str) {
const isJSON = str => {
try {
JSON.parse(str);
return true;
@@ -45,12 +45,12 @@ var isJSON = function (str) {
// returns an object containing:
// - accessToken
// - expiresIn: lifetime of token in seconds
var getTokenResponse = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
const getTokenResponse = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var responseContent;
let responseContent;
try {
// Request an access token
responseContent = HTTP.get(
@@ -63,16 +63,18 @@ var getTokenResponse = function (query) {
}
}).data;
} catch (err) {
throw _.extend(new Error("Failed to complete OAuth handshake with Facebook. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Facebook. ${err.message}`),
{ response: err.response },
);
}
var fbAccessToken = responseContent.access_token;
var fbExpires = responseContent.expires_in;
const fbAccessToken = responseContent.access_token;
const fbExpires = responseContent.expires_in;
if (!fbAccessToken) {
throw new Error("Failed to complete OAuth handshake with facebook " +
"-- can't find access token in HTTP response. " + responseContent);
`-- can't find access token in HTTP response. ${responseContent}`);
}
return {
accessToken: fbAccessToken,
@@ -80,14 +82,14 @@ var getTokenResponse = function (query) {
};
};
var getIdentity = function (accessToken, fields) {
var config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
const getIdentity = (accessToken, fields) => {
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
if (!config)
throw new ServiceConfiguration.ConfigError();
// Generate app secret proof that is a sha256 hash of the app access token, with the app secret as the key
// https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof
var hmac = crypto.createHmac('sha256', OAuth.openSecret(config.secret));
const hmac = crypto.createHmac('sha256', OAuth.openSecret(config.secret));
hmac.update(accessToken);
try {
@@ -99,11 +101,13 @@ var getIdentity = function (accessToken, fields) {
}
}).data;
} catch (err) {
throw _.extend(new Error("Failed to fetch identity from Facebook. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to fetch identity from Facebook. ${err.message}`),
{ response: err.response },
);
}
};
Facebook.retrieveCredential = function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};
Facebook.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

View File

@@ -3,11 +3,11 @@ Package.describe({
version: "1.5.0"
});
Package.onUse(function(api) {
Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http', ['server']);
api.use('underscore', 'server');
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);

View File

@@ -1,12 +1,8 @@
Template.configureLoginServiceDialogForGithub.helpers({
siteUrl: function () {
return Meteor.absoluteUrl();
}
siteUrl: () => Meteor.absoluteUrl(),
});
Template.configureLoginServiceDialogForGithub.fields = function () {
return [
{property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client Secret'}
];
};
Template.configureLoginServiceDialogForGithub.fields = () => [
{property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client Secret'}
];

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: 'Blaze configuration templates for GitHub 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('github_login_button.css', 'client');
api.addFiles(

View File

@@ -5,39 +5,39 @@ Github = {};
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Github.requestCredential = function (options, credentialRequestCompleteCallback) {
Github.requestCredential = (options, credentialRequestCompleteCallback) => {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
options = {};
}
var config = ServiceConfiguration.configurations.findOne({service: 'github'});
const config = ServiceConfiguration.configurations.findOne({service: 'github'});
if (!config) {
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
new ServiceConfiguration.ConfigError());
return;
}
var credentialToken = Random.secret();
const credentialToken = Random.secret();
var scope = (options && options.requestPermissions) || ['user:email'];
var flatScope = _.map(scope, encodeURIComponent).join('+');
const scope = (options && options.requestPermissions) || ['user:email'];
const flatScope = scope.map(encodeURIComponent).join('+');
var loginStyle = OAuth._loginStyle('github', config, options);
const loginStyle = OAuth._loginStyle('github', config, options);
var loginUrl =
const loginUrl =
'https://github.com/login/oauth/authorize' +
'?client_id=' + config.clientId +
'&scope=' + flatScope +
'&redirect_uri=' + OAuth._redirectUri('github', config) +
'&state=' + OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl);
`?client_id=${config.clientId}` +
`&scope=${flatScope}` +
`&redirect_uri=${OAuth._redirectUri('github', config)}` +
`&state=${OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl)}`;
OAuth.launchLogin({
loginService: "github",
loginStyle: loginStyle,
loginUrl: loginUrl,
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
credentialToken: credentialToken,
loginStyle,
loginUrl,
credentialRequestCompleteCallback,
credentialToken,
popupOptions: {width: 900, height: 450}
});
};

View File

@@ -1,11 +1,11 @@
Github = {};
OAuth.registerService('github', 2, null, function(query) {
OAuth.registerService('github', 2, null, query => {
var accessToken = getAccessToken(query);
var identity = getIdentity(accessToken);
var emails = getEmails(accessToken);
var primaryEmail = _.findWhere(emails, {primary: true});
const accessToken = getAccessToken(query);
const identity = getIdentity(accessToken);
const emails = getEmails(accessToken);
const primaryEmail = emails.find(email => email.primary);
return {
serviceData: {
@@ -13,23 +13,23 @@ OAuth.registerService('github', 2, null, function(query) {
accessToken: OAuth.sealSecret(accessToken),
email: identity.email || (primaryEmail && primaryEmail.email) || '',
username: identity.login,
emails: emails
emails,
},
options: {profile: {name: identity.name}}
};
});
// http://developer.github.com/v3/#user-agent-required
var userAgent = "Meteor";
let userAgent = "Meteor";
if (Meteor.release)
userAgent += "/" + Meteor.release;
userAgent += `/${Meteor.release}`;
var getAccessToken = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'github'});
const getAccessToken = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'github'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var response;
let response;
try {
response = HTTP.post(
"https://github.com/login/oauth/access_token", {
@@ -46,17 +46,19 @@ var getAccessToken = function (query) {
}
});
} catch (err) {
throw _.extend(new Error("Failed to complete OAuth handshake with Github. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Github. ${err.message}`),
{ response: err.response },
);
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error);
throw new Error(`Failed to complete OAuth handshake with GitHub. ${response.data.error}`);
} else {
return response.data.access_token;
}
};
var getIdentity = function (accessToken) {
const getIdentity = accessToken => {
try {
return HTTP.get(
"https://api.github.com/user", {
@@ -64,12 +66,14 @@ var getIdentity = function (accessToken) {
params: {access_token: accessToken}
}).data;
} catch (err) {
throw _.extend(new Error("Failed to fetch identity from Github. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to fetch identity from Github. ${err.message}`),
{ response: err.response },
);
}
};
var getEmails = function (accessToken) {
const getEmails = accessToken => {
try {
return HTTP.get(
"https://api.github.com/user/emails", {
@@ -81,6 +85,5 @@ var getEmails = function (accessToken) {
}
};
Github.retrieveCredential = function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};
Github.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

View File

@@ -1,13 +1,13 @@
Package.describe({
summary: 'GitHub OAuth flow',
version: '1.2.1'
version: '1.2.2'
});
Package.onUse(function (api) {
Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http', ['server']);
api.use('underscore', ['client', 'server']);
api.use('http', 'server');
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);

View File

@@ -22,7 +22,7 @@
Set Authorized Javascript Origins to: <span class="url">{{siteUrl}}</span>
</li>
<li>
Set Authorized Redirect URI to: <span class="url">{{siteUrl}}/_oauth/google</span>
Set Authorized Redirect URI to: <span class="url">{{siteUrl}}/_oauth/google?close</span>
</li>
<li>
Finish by clicking "Create".

View File

@@ -1,6 +1,6 @@
Template.configureLoginServiceDialogForGoogle.helpers({
siteUrl: function () {
var url = Meteor.absoluteUrl();
siteUrl: () => {
let url = Meteor.absoluteUrl();
if (url.slice(-1) === "/") {
url = url.slice(0,-1)
}
@@ -8,9 +8,7 @@ Template.configureLoginServiceDialogForGoogle.helpers({
}
});
Template.configureLoginServiceDialogForGoogle.fields = function () {
return [
{property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client secret'}
];
};
Template.configureLoginServiceDialogForGoogle.fields = () => [
{property: 'clientId', label: 'Client ID'},
{property: 'secret', label: 'Client secret'}
];

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: "Blaze configuration templates for Google 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('google_login_button.css', 'client');

View File

@@ -1,6 +1,6 @@
var Google = require("./namespace.js");
import Google from './namespace.js';
var ILLEGAL_PARAMETERS = {
const ILLEGAL_PARAMETERS = {
'response_type': 1,
'client_id': 1,
'scope': 1,
@@ -13,7 +13,7 @@ var ILLEGAL_PARAMETERS = {
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Google.requestCredential = function (options, credentialRequestCompleteCallback) {
Google.requestCredential = (options, credentialRequestCompleteCallback) => {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
@@ -22,24 +22,22 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
options = {};
}
var config = ServiceConfiguration.configurations.findOne({service: 'google'});
const config = ServiceConfiguration.configurations.findOne({service: 'google'});
if (!config) {
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
new ServiceConfiguration.ConfigError());
return;
}
var credentialToken = Random.secret();
const credentialToken = Random.secret();
// we need the email scope to get user id from google.
var requiredScopes = { 'email': 1 };
var scopes = options.requestPermissions || ['profile'];
scopes.forEach(function (scope) {
requiredScopes[scope] = 1;
});
const requiredScopes = { 'email': 1 };
let scopes = options.requestPermissions || ['profile'];
scopes.forEach(scope => requiredScopes[scope] = 1);
scopes = Object.keys(requiredScopes);
var loginUrlParameters = {};
const loginUrlParameters = {};
if (config.loginUrlParameters){
Object.assign(loginUrlParameters, config.loginUrlParameters);
}
@@ -48,9 +46,9 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
}
// validate options keys
Object.keys(loginUrlParameters).forEach(function (key) {
if (ILLEGAL_PARAMETERS.hasOwnProperty(key)) {
throw new Error("Google.requestCredential: Invalid loginUrlParameter: " + key);
Object.keys(loginUrlParameters).forEach(key => {
if (Object.prototype.hasOwnProperty.call(ILLEGAL_PARAMETERS, key)) {
throw new Error(`Google.requestCredential: Invalid loginUrlParameter: ${key}`);
}
});
@@ -68,7 +66,7 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
loginUrlParameters.login_hint = options.loginHint;
}
var loginStyle = OAuth._loginStyle('google', config, options);
const loginStyle = OAuth._loginStyle('google', config, options);
// https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl
Object.assign(loginUrlParameters, {
"response_type": "code",
@@ -77,18 +75,17 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
"redirect_uri": OAuth._redirectUri('google', config),
"state": OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl)
});
var loginUrl = 'https://accounts.google.com/o/oauth2/auth?' +
Object.keys(loginUrlParameters).map(function (param) {
return encodeURIComponent(param) + '=' +
encodeURIComponent(loginUrlParameters[param]);
}).join("&");
const loginUrl = 'https://accounts.google.com/o/oauth2/auth?' +
Object.keys(loginUrlParameters).map(param =>
`${encodeURIComponent(param)}=${encodeURIComponent(loginUrlParameters[param])}`
).join("&");
OAuth.launchLogin({
loginService: "google",
loginStyle: loginStyle,
loginUrl: loginUrl,
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
credentialToken: credentialToken,
loginStyle,
loginUrl,
credentialRequestCompleteCallback,
credentialToken,
popupOptions: { height: 600 }
});
};

View File

@@ -1,19 +1,17 @@
var Google = require("./namespace.js");
var Accounts = require("meteor/accounts-base").Accounts;
var hasOwn = Object.prototype.hasOwnProperty;
import Google from './namespace.js';
import { Accounts } from 'meteor/accounts-base';
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name',
'family_name', 'picture', 'locale', 'timezone', 'gender'];
function getServiceDataFromTokens(tokens) {
var accessToken = tokens.accessToken;
var idToken = tokens.idToken;
var scopes = getScopes(accessToken);
var identity = getIdentity(accessToken);
var serviceData = {
accessToken: accessToken,
idToken: idToken,
const getServiceDataFromTokens = tokens => {
const { accessToken, idToken } = tokens;
const scopes = getScopes(accessToken);
const identity = getIdentity(accessToken);
const serviceData = {
accessToken,
idToken,
scope: scopes
};
@@ -22,7 +20,7 @@ function getServiceDataFromTokens(tokens) {
Date.now() + 1000 * parseInt(tokens.expiresIn, 10);
}
var fields = Object.create(null);
const fields = Object.create(null);
Google.whitelistedFields.forEach(function (name) {
if (hasOwn.call(identity, name)) {
fields[name] = identity[name];
@@ -39,7 +37,7 @@ function getServiceDataFromTokens(tokens) {
}
return {
serviceData: serviceData,
serviceData,
options: {
profile: {
name: identity.name
@@ -48,7 +46,7 @@ function getServiceDataFromTokens(tokens) {
};
}
Accounts.registerLoginHandler(function (request) {
Accounts.registerLoginHandler(request => {
if (request.googleSignIn !== true) {
return;
}
@@ -77,9 +75,7 @@ Accounts.registerLoginHandler(function (request) {
}, result.options);
});
function getServiceData(query) {
return getServiceDataFromTokens(getTokens(query));
}
const getServiceData = query => getServiceDataFromTokens(getTokens(query));
OAuth.registerService('google', 2, null, getServiceData);
@@ -87,12 +83,12 @@ OAuth.registerService('google', 2, null, getServiceData);
// - accessToken
// - expiresIn: lifetime of token in seconds
// - refreshToken, if this is the first authorization request
var getTokens = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'google'});
const getTokens = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'google'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var response;
let response;
try {
response = HTTP.post(
"https://accounts.google.com/o/oauth2/token", {params: {
@@ -104,13 +100,13 @@ var getTokens = function (query) {
}});
} catch (err) {
throw Object.assign(
new Error("Failed to complete OAuth handshake with Google. " + err.message),
new Error(`Failed to complete OAuth handshake with Google. ${err.message}`),
{ response: err.response }
);
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error("Failed to complete OAuth handshake with Google. " + response.data.error);
throw new Error(`Failed to complete OAuth handshake with Google. ${response.data.error}`);
} else {
return {
accessToken: response.data.access_token,
@@ -121,32 +117,31 @@ var getTokens = function (query) {
}
};
var getIdentity = function (accessToken) {
const getIdentity = accessToken => {
try {
return HTTP.get(
"https://www.googleapis.com/oauth2/v1/userinfo",
{params: {access_token: accessToken}}).data;
} catch (err) {
throw Object.assign(
new Error("Failed to fetch identity from Google. " + err.message),
new Error(`Failed to fetch identity from Google. ${err.message}`),
{ response: err.response }
);
}
};
var getScopes = function (accessToken) {
const getScopes = accessToken => {
try {
return HTTP.get(
"https://www.googleapis.com/oauth2/v1/tokeninfo",
{params: {access_token: accessToken}}).data.scope.split(' ');
} catch (err) {
throw Object.assign(
new Error("Failed to fetch tokeninfo from Google. " + err.message),
new Error(`Failed to fetch tokeninfo from Google. ${err.message}`),
{ response: err.response }
);
}
};
Google.retrieveCredential = function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};
Google.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

View File

@@ -1,14 +1,14 @@
var Google = require("./namespace.js");
import Google from './namespace.js';
var gplusPromise = new Promise(function (resolve, reject) {
const gplusPromise = new Promise((resolve, reject) => {
if (! Meteor.isCordova) {
reject(new Error("plugins.googleplus requires Cordova"));
return;
}
Meteor.startup(function () {
var plugins = global.plugins;
var gplus = plugins && plugins.googleplus;
Meteor.startup(() => {
const { plugins } = global;
const gplus = plugins && plugins.googleplus;
if (gplus) {
resolve(gplus);
} else {
@@ -17,21 +17,21 @@ var gplusPromise = new Promise(function (resolve, reject) {
});
});
function tolerateUnhandledRejection() {}
const tolerateUnhandledRejection = () => {};
gplusPromise.catch(tolerateUnhandledRejection);
// After 20 April 2017, Google OAuth login will no longer work from a
// WebView, so Cordova apps must use Google Sign-In instead.
// https://github.com/meteor/meteor/issues/8253
exports.signIn = Google.signIn = function (options, callback) {
export const signIn = Google.signIn = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
gplusPromise.then(function (gplus) {
var config = ServiceConfiguration.configurations.findOne({
gplusPromise.then(gplus => {
const config = ServiceConfiguration.configurations.findOne({
service: "google"
});
@@ -45,7 +45,7 @@ exports.signIn = Google.signIn = function (options, callback) {
scopes: getScopes(options).join(" "),
webClientId: config.clientId,
offline: true
}, function (response) {
}, response => {
Accounts.callLoginMethod({
methodArguments: [Object.assign({
googleSignIn: true
@@ -57,30 +57,27 @@ exports.signIn = Google.signIn = function (options, callback) {
}).catch(callback);
};
function getScopes(options) {
const getScopes = options => {
// we need the email scope to get user id from google.
var requiredScopes = { 'email': 1 };
var scopes = options.requestPermissions || ['profile'];
const requiredScopes = { 'email': 1 };
const scopes = options.requestPermissions || ['profile'];
scopes.forEach(function (scope) {
requiredScopes[scope] = 1;
});
scopes.forEach(scope => requiredScopes[scope] = 1);
return Object.keys(requiredScopes);
}
exports.signOut = Google.signOut = function () {
return gplusPromise.then(function (gplus) {
return new Promise(function (resolve) {
gplus.logout(resolve);
});
});
};
export const signOut = Google.signOut = () =>
gplusPromise.then(gplus =>
new Promise(resolve =>
gplus.logout(resolve)
)
);
// Make sure we don't stay logged in with Google Sign-In after the client
// calls Meteor.logout().
Meteor.startup(function () {
Accounts.onLogout(function () {
Google.signOut().catch(tolerateUnhandledRejection);
});
});
Meteor.startup(() =>
Accounts.onLogout(() =>
Google.signOut().catch(tolerateUnhandledRejection)
)
);

View File

@@ -1,9 +1,9 @@
Package.describe({
summary: "Google OAuth flow",
version: "1.2.5"
version: "1.2.6",
});
var cordovaPluginGooglePlusURL =
const cordovaPluginGooglePlusURL =
// This revision is from the "update-entitlements-plist-files" branch.
// This logic can be reverted when/if this PR is merged:
// https://github.com/EddyVerbruggen/cordova-plugin-googleplus/pull/366
@@ -13,7 +13,7 @@ Cordova.depends({
"cordova-plugin-googleplus": cordovaPluginGooglePlusURL
});
Package.onUse(function(api) {
Package.onUse(api => {
api.use("ecmascript");
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);

View File

@@ -2,7 +2,7 @@ const hasOwn = Object.prototype.hasOwnProperty;
export class IdMap {
constructor(idStringify, idParse) {
this.clear();
this._map = new Map();
this._idStringify = idStringify || JSON.stringify;
this._idParse = idParse || JSON.parse;
}
@@ -14,44 +14,40 @@ export class IdMap {
get(id) {
var key = this._idStringify(id);
return this._map[key];
return this._map.get(key);
}
set(id, value) {
var key = this._idStringify(id);
this._map[key] = value;
this._map.set(key, value);
}
remove(id) {
var key = this._idStringify(id);
delete this._map[key];
this._map.delete(key);
}
has(id) {
var key = this._idStringify(id);
return hasOwn.call(this._map, key);
return this._map.has(key);
}
empty() {
for (let key in this._map) {
return false;
}
return true;
return this._map.size === 0;
}
clear() {
this._map = Object.create(null);
this._map.clear();
}
// Iterates over the items in the map. Return `false` to break the loop.
forEach(iterator) {
// don't use _.each, because we can't break out of it.
var keys = Object.keys(this._map);
for (var i = 0; i < keys.length; i++) {
for (const [key, value] of this._map){
var breakIfFalse = iterator.call(
null,
this._map[keys[i]],
this._idParse(keys[i])
value,
this._idParse(key)
);
if (breakIfFalse === false) {
return;
@@ -60,15 +56,15 @@ export class IdMap {
}
size() {
return Object.keys(this._map).length;
return this._map.size;
}
setDefault(id, def) {
var key = this._idStringify(id);
if (hasOwn.call(this._map, key)) {
return this._map[key];
if (this._map.has(key)) {
return this._map.get(key);
}
this._map[key] = def;
this._map.set(key, def);
return def;
}
@@ -76,8 +72,9 @@ export class IdMap {
// IDs (ie, that nobody is going to mutate an ObjectId).
clone() {
var clone = new IdMap(this._idStringify, this._idParse);
this.forEach(function (value, id) {
clone.set(id, EJSON.clone(value));
// copy directly to avoid stringify/parse overhead
this._map.forEach(function(value, key){
clone._map.set(key, EJSON.clone(value));
});
return clone;
}

View File

@@ -6,6 +6,9 @@
<li>
Visit <a href="http://www.meetup.com/meetup_api/oauth_consumers/create/" target="blank">http://www.meetup.com/meetup_api/oauth_consumers/create/</a>
</li>
<li>
Click on "Create New Consumer".
</li>
<li>
Set the Consumer name to the name of your application.
</li>

View File

@@ -1,12 +1,8 @@
Template.configureLoginServiceDialogForMeetup.helpers({
siteUrl: function () {
return Meteor.absoluteUrl();
}
siteUrl: () => Meteor.absoluteUrl(),
});
Template.configureLoginServiceDialogForMeetup.fields = function () {
return [
{property: 'clientId', label: 'Key'},
{property: 'secret', label: 'Secret'}
];
};
Template.configureLoginServiceDialogForMeetup.fields = () => [
{property: 'clientId', label: 'Key'},
{property: 'secret', label: 'Secret'}
]

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: 'Blaze configuration templates for the Meetup OAuth flow.',
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('meetup_login_button.css', 'client');
api.addFiles(

View File

@@ -4,14 +4,14 @@ Meetup = {};
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Meetup.requestCredential = function (options, credentialRequestCompleteCallback) {
Meetup.requestCredential = (options, credentialRequestCompleteCallback) => {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
options = {};
}
var config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
const config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
if (!config) {
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
new ServiceConfiguration.ConfigError());
@@ -22,32 +22,32 @@ Meetup.requestCredential = function (options, credentialRequestCompleteCallback)
// parameter when redirecting back to the client, so we use
// `Random.id()` here (alphanumerics) instead of `Random.secret()`
// (base 64 characters).
var credentialToken = Random.id();
const credentialToken = Random.id();
var scope = (options && options.requestPermissions) || [];
var flatScope = _.map(scope, encodeURIComponent).join('+');
const scope = (options && options.requestPermissions) || [];
const flatScope = scope.map(encodeURIComponent).join('+');
var loginStyle = OAuth._loginStyle('meetup', config, options);
const loginStyle = OAuth._loginStyle('meetup', config, options);
var loginUrl =
const loginUrl =
'https://secure.meetup.com/oauth2/authorize' +
'?client_id=' + config.clientId +
`?client_id=${config.clientId}` +
'&response_type=code' +
'&scope=' + flatScope +
'&redirect_uri=' + OAuth._redirectUri('meetup', config) +
'&state=' + OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl);
`&scope=${flatScope}` +
`&redirect_uri=${OAuth._redirectUri('meetup', config)}` +
`&state=${OAuth._stateParam(loginStyle, credentialToken, options && options.redirectUrl)}`;
// meetup box gets taller when permissions requested.
var height = 620;
if (_.without(scope, 'basic').length)
let height = 620;
if (Object.prototype.hasOwnProperty.call(scope, 'basic') ? scope.length - 1 : scope.length)
height += 130;
OAuth.launchLogin({
loginService: "meetup",
loginStyle: loginStyle,
loginUrl: loginUrl,
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
credentialToken: credentialToken,
popupOptions: {width: 900, height: height}
loginStyle,
loginUrl,
credentialRequestCompleteCallback,
credentialToken,
popupOptions: { width: 900, height },
});
};

View File

@@ -1,28 +1,28 @@
Meetup = {};
OAuth.registerService('meetup', 2, null, function(query) {
OAuth.registerService('meetup', 2, null, query => {
var response = getAccessToken(query);
var accessToken = response.access_token;
var expiresAt = (+new Date) + (1000 * response.expires_in);
var identity = getIdentity(accessToken);
const response = getAccessToken(query);
const accessToken = response.access_token;
const expiresAt = (+new Date) + (1000 * response.expires_in);
const identity = getIdentity(accessToken);
return {
serviceData: {
id: identity.id,
accessToken: accessToken,
expiresAt: expiresAt
accessToken,
expiresAt,
},
options: {profile: {name: identity.name}}
};
});
var getAccessToken = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
const getAccessToken = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var response;
let response;
try {
response = HTTP.post(
"https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: {
@@ -34,30 +34,33 @@ var getAccessToken = function (query) {
state: query.state
}});
} catch (err) {
throw _.extend(new Error("Failed to complete OAuth handshake with Meetup. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Meetup. ${err.message}`),
{ response: err.response }
);
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error);
throw new Error(`Failed to complete OAuth handshake with Meetup. ${response.data.error}`);
} else {
return response.data;
}
};
var getIdentity = function (accessToken) {
const getIdentity = accessToken => {
try {
var response = HTTP.get(
const response = HTTP.get(
"https://api.meetup.com/2/members",
{params: {member_id: 'self', access_token: accessToken}});
return response.data.results && response.data.results[0];
} catch (err) {
throw _.extend(new Error("Failed to fetch identity from Meetup. " + err.message),
{response: err.response});
throw Object.assign(
new Error(`Failed to fetch identity from Meetup. ${err.message}`),
{ response: err.response }
);
}
};
Meetup.retrieveCredential = function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};
Meetup.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

Some files were not shown because too many files have changed in this diff Show More