mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' into release-1.7.1
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -203,3 +203,9 @@ valid-identifier: https://github.com/purplecabbage/valid-identifier
|
||||
----------
|
||||
|
||||
Jesse MacFadyen
|
||||
|
||||
----------
|
||||
puppeteer: https://github.com/GoogleChrome/puppeteer
|
||||
----------
|
||||
|
||||
Copyright 2017 Google Inc.
|
||||
|
||||
7
examples/unfinished/accounts-ui-viewer/.meteor/.id
Normal file
7
examples/unfinished/accounts-ui-viewer/.meteor/.id
Normal 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
|
||||
@@ -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
|
||||
|
||||
2
examples/unfinished/accounts-ui-viewer/.meteor/platforms
Normal file
2
examples/unfinished/accounts-ui-viewer/.meteor/platforms
Normal file
@@ -0,0 +1,2 @@
|
||||
browser
|
||||
server
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
682
examples/unfinished/accounts-ui-viewer/package-lock.json
generated
Normal file
682
examples/unfinished/accounts-ui-viewer/package-lock.json
generated
Normal 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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
11
examples/unfinished/accounts-ui-viewer/package.json
Normal file
11
examples/unfinished/accounts-ui-viewer/package.json
Normal 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
9
meteor
@@ -132,7 +132,14 @@ fi
|
||||
# the script take precedence over $NODE_PATH; it used to be that users would
|
||||
# screw up their meteor installs by have a ~/node_modules
|
||||
|
||||
if [ "$ARCH" = "i686" ]; then
|
||||
# 32-bit platforms cannot request 4GB
|
||||
MAX_OLD_SPACE_SIZE=3072
|
||||
else
|
||||
MAX_OLD_SPACE_SIZE=4096
|
||||
fi
|
||||
|
||||
exec "$DEV_BUNDLE/bin/node" \
|
||||
--max-old-space-size=4096 \
|
||||
--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
|
||||
${TOOL_NODE_FLAGS} \
|
||||
"$METEOR" "$@"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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'
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {AccountsServer} from "./accounts_server.js";
|
||||
import "./accounts_rate_limit.js";
|
||||
import "./url_server.js";
|
||||
|
||||
/**
|
||||
* @namespace Accounts
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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"}})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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">×</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">×</a>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
3
packages/binary-heap/binary-heap.js
Normal file
3
packages/binary-heap/binary-heap.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MaxHeap } from './max-heap.js';
|
||||
export { MinHeap } from './min-heap.js';
|
||||
export { MinMaxHeap } from './min-max-heap.js';
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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'}
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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'}
|
||||
];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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'}
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user