Merge remote-tracking branch 'upstream/devel' into devel

This commit is contained in:
Vlad Lasky
2018-10-30 15:58:35 +11:00
961 changed files with 47835 additions and 45574 deletions

View File

@@ -1,13 +1,5 @@
version: 2
# These directories are cached across all builds, currently with no
# hashing mechanism, but we should consider doing it off dev_bundle.
meteor_cache_dirs: &meteor_cache_dirs
paths:
- "dev_bundle"
- ".babel-cache"
- ".meteor"
# A reusable "run" snippet which is ran before each test to setup the
# environment for user-limits, core-dumps, etc.
run_env_change: &run_env_change
@@ -17,13 +9,16 @@ run_env_change: &run_env_change
sudo mkdir -p /tmp/core_dumps
sudo chmod a+rwx /tmp/core_dumps
# Make a place for JUnit tests to live.
sudo mkdir -p /tmp/results/junit
sudo chmod -R a+rwx /tmp/results/
# Bake the locale we expect into the image.
echo "en_US.UTF-8 UTF-8" | sudo tee /etc/locale.gen
sudo locale-gen
# The commands below don't work in Docker images, but might be worth
# reenabling if we switch back to machine:true instead of Docker.
# Set the pattern for core dumps, so we can find them.
echo kernel.core_pattern="/tmp/core_dumps/core.%e.%p.%h.%t" | \
sudo tee -a /etc/sysctl.conf
# echo kernel.core_pattern="/tmp/core_dumps/core.%e.%p.%h.%t" | \
# sudo tee -a /etc/sysctl.conf
# Note that since every "run" command starts its own shell, and I wasn't
# able to set this at a system wide level for all users, it's necessary to
@@ -31,11 +26,11 @@ run_env_change: &run_env_change
# output a core dump.
# Raise inotify user watches up higher.
echo fs.inotify.max_user_watches=524288 | \
sudo tee -a /etc/sysctl.conf
# echo fs.inotify.max_user_watches=524288 | \
# sudo tee -a /etc/sysctl.conf
# Reload sysctl so these are in effect.
sudo sysctl -p
# sudo sysctl -p
# A reusable "run" snippet which enables the continued logging of memoryusage
# to a file on disk which can be saved to build artifacts for later analysis.
@@ -46,7 +41,8 @@ run_log_mem_use: &run_log_mem_use
# Log memory usage throughout entire build.
MEMUSELOG=/tmp/memuse.txt /bin/bash -c '\
while true; do\
ps -u $USER eo pid,%cpu,%mem,rss:10,vsz:10,args:20 --sort=-%mem >> $MEMUSELOG; \
ps -e -o user,%cpu,%mem,rss:10,vsz:10,command:20 \
--sort=-%mem >> $MEMUSELOG; \
echo "----------" >> $MEMUSELOG; \
sleep 1; \
done'
@@ -64,14 +60,17 @@ run_save_node_bin: &run_save_node_bin
# This environment is set to every job (and the initial build).
build_machine_environment: &build_machine_environment
# Specify that we want an actual machine (ala Circle 1.0), not a Docker image.
machine: true
docker:
- image: meteor/circleci
environment:
# This multiplier scales the waitSecs for selftests.
TIMEOUT_SCALE_FACTOR: 8
# Retry failed tests additional times.
METEOR_SELF_TEST_RETRIES: 2
# These, mostly overlapping, flags ensure that CircleCI is as pretty as
# possible for a non-interactive environment. See also: --headless.
EMACS: t
METEOR_HEADLESS: true
METEOR_PRETTY_OUTPUT: 0
@@ -80,26 +79,23 @@ build_machine_environment: &build_machine_environment
METEOR_SAVE_TMPDIRS: 1
# Skip these tests on every test run.
# For readability, this is a regex wrapped across multiple lines in quotes.
SELF_TEST_EXCLUDE: "\
^old cli tests|\
^minifiers can't register non-js|\
^minifiers: apps can't use|\
^compiler plugins - addAssets\
"
# If needed, for readability this should be a regex wrapped across
# multiple lines in quotes.
SELF_TEST_EXCLUDE: "add debugOnly and prodOnly packages"
# These will be evaled before each command.
PRE_TEST_COMMANDS: |-
ulimit -c unlimited; # Set core dump size as Ubuntu 14.04 lacks prlimit.
ulimit -n 4096; # CircleCI default is soft 1024, hard 4096. Take it all.
# Enable the Garbage Collection `gc` object to be exposed so we can try
# to our own, hopefully more graceful, technique.
TOOL_NODE_FLAGS: --expose-gc
ulimit -a # Display all ulimit settings for transparency.
# This is only to make Meteor self-test not remind us that we can set
# this argument for self-tests.
SELF_TEST_TOOL_NODE_FLAGS: " "
# Variables for load-balancing
NUM_GROUPS: 11
RUNNING_AVG_LENGTH: 5
jobs:
Get Ready:
<<: *build_machine_environment
@@ -114,7 +110,39 @@ jobs:
name: Git Submodules.
command: (git submodule sync && git submodule update --init --recursive) || (rm -fr .git/config .git/modules && git submodule deinit -f . && git submodule update --init --recursive)
- restore_cache:
key: meteor-cache
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-v3-{{ checksum "shrinkwraps.txt" }}
- package-npm-deps-cache-group2-v3-
- restore_cache:
keys:
- v2-other-deps-cache-{{ .Branch }}-{{ .Revision }}
- v2-other-deps-cache-{{ .Branch }}-
- restore_cache:
keys:
- v1-test-groups-{{ .Branch }}
- v1-test-groups-
- run:
name: Create Test Results Directory
command: |
sudo mkdir -p ./tmp/results/junit
sudo chmod a+rwx ./tmp/results/junit
# Clear dev_bundle/.npm to ensure consistent test runs.
- run:
name: Clear npm cache
command: ./meteor npm cache clear --force
- run:
name: Get Ready
command: |
@@ -122,15 +150,6 @@ jobs:
./meteor --get-ready
# shouldn't take longer than 20 minutes
no_output_timeout: 20m
# Clear dev_bundle/.npm to ensure consistent test runs.
- run:
name: Clear npm cache
command: ./meteor npm cache clear --force
# Since PhantomJS has been removed from dev_bundle/lib/node_modules
# (#6905), but self-test still needs it, install it now.
- run:
name: Test Prereqs
command: ./meteor npm install -g phantomjs-prebuilt browserstack-webdriver
- run:
<<: *run_save_node_bin
- persist_to_workspace:
@@ -141,16 +160,7 @@ jobs:
- store_artifacts:
path: /tmp/memuse.txt
save_caches:
<<: *build_machine_environment
steps:
- attach_workspace:
at: .
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
Group 0:
Isolated Tests:
<<: *build_machine_environment
steps:
- run:
@@ -163,30 +173,38 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running warehouse self-tests"
name: "Running self-test ('package-tests: add debugOnly and prodOnly packages')"
command: |
eval $PRE_TEST_COMMANDS;
./meteor self-test \
'add debugOnly and prodOnly packages' \
--retries ${METEOR_SELF_TEST_RETRIES} \
--headless \
--phantom
no_output_timeout: 20m
- run:
name: "Running self-test (Custom Warehouse Tests)"
command: |
eval $PRE_TEST_COMMANDS;
./meteor self-test \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/0.xml \
--phantom \
--with-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 1:
Test Group 0:
<<: *build_machine_environment
steps:
- run:
@@ -199,31 +217,35 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (1): A-Com"
name: "Running self-test (Test Group 0)"
command: |
if [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=$(<./tmp/test-groups/0.txt); else TEST_GROUP='^[a-b]|^c[a-n]|^co[a-l]|^comm'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/1.xml \
--file '^[a-b]|^c[a-n]|^co[a-l]|^compiler-plugins' \
--phantom \
--junit ./tmp/results/junit/0.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 2:
Test Group 1:
<<: *build_machine_environment
steps:
- run:
@@ -236,31 +258,35 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (2): Con-K"
name: "Running self-test (Test Group 1)"
command: |
if [ -f ./tmp/test-groups/1.txt ]; then TEST_GROUP=$(<./tmp/test-groups/1.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^com[n-z]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/2.xml \
--file "^co[n-z]|^c[p-z]|^[d-k]" \
--phantom \
--junit ./tmp/results/junit/1.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 3:
Test Group 2:
<<: *build_machine_environment
steps:
- run:
@@ -273,31 +299,35 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (3): L-O"
name: "Running self-test (Test Group 2)"
command: |
if [ -f ./tmp/test-groups/2.txt ]; then TEST_GROUP=$(<./tmp/test-groups/2.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^co[n-z]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/3.xml \
--file '^[l-o]' \
--phantom \
--junit ./tmp/results/junit/2.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 4:
Test Group 3:
<<: *build_machine_environment
steps:
- run:
@@ -310,68 +340,35 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (4): P"
command: |
eval $PRE_TEST_COMMANDS;
./meteor self-test \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/4.xml \
--file '^p' \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
- store_artifacts:
path: /tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 5:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (5): Run"
name: "Running self-test (Test Group 3)"
command: |
if [ -f ./tmp/test-groups/3.txt ]; then TEST_GROUP=$(<./tmp/test-groups/3.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^c[p-z]|^[d-g]|^h[a-e]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/5.xml \
--file '^run' \
--phantom \
--junit ./tmp/results/junit/3.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 6:
Test Group 4:
<<: *build_machine_environment
steps:
- run:
@@ -384,31 +381,35 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (6): R-S"
name: "Running self-test (Test Group 4)"
command: |
if [ -f ./tmp/test-groups/4.txt ]; then TEST_GROUP=$(<./tmp/test-groups/4.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^h[f-z]|^[i-l]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/6.xml \
--file '^r(?!un)|^s' \
--phantom \
--junit ./tmp/results/junit/4.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Group 7:
Test Group 5:
<<: *build_machine_environment
steps:
- run:
@@ -421,56 +422,406 @@ jobs:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (7): Sp-Z"
name: "Running self-test (Test Group 5)"
command: |
if [ -f ./tmp/test-groups/5.txt ]; then TEST_GROUP=$(<./tmp/test-groups/5.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^m[a-n]|^mo[a-d]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--retries ${METEOR_SELF_TEST_RETRIES} \
--exclude "${SELF_TEST_EXCLUDE}" \
--headless \
--junit /tmp/results/junit/7.xml \
--file '^[t-z]|^command-line' \
--phantom \
--junit ./tmp/results/junit/5.xml \
--without-tag "custom-warehouse"
no_output_timeout: 20m
- run:
<<: *run_save_node_bin
- save_cache:
key: meteor-cache
<<: *meteor_cache_dirs
- store_test_results:
path: /tmp/results
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: /tmp/results
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Test Group 6:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (Test Group 6)"
command: |
if [ -f ./tmp/test-groups/6.txt ]; then TEST_GROUP=$(<./tmp/test-groups/6.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^mo[e-z]|^m[p-z]|^[n-o]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--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
- run:
<<: *run_save_node_bin
- store_test_results:
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Test Group 7:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (Test Group 7)"
command: |
if [ -f ./tmp/test-groups/7.txt ]; then TEST_GROUP=$(<./tmp/test-groups/7.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^[p-q]|^r[a-e]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--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
- run:
<<: *run_save_node_bin
- store_test_results:
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Test Group 8:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (Test Group 8)"
command: |
if [ -f ./tmp/test-groups/8.txt ]; then TEST_GROUP=$(<./tmp/test-groups/8.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^r[f-z]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--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
- run:
<<: *run_save_node_bin
- store_test_results:
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Test Group 9:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (Test Group 9)"
command: |
if [ -f ./tmp/test-groups/9.txt ]; then TEST_GROUP=$(<./tmp/test-groups/9.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^s'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--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
- run:
<<: *run_save_node_bin
- store_test_results:
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
Test Group 10:
<<: *build_machine_environment
steps:
- run:
<<: *run_log_mem_use
- run:
<<: *run_env_change
- attach_workspace:
at: .
- run:
name: "Print environment"
command: printenv
- run:
name: "Running self-test (Test Group 10)"
command: |
if [ -f ./tmp/test-groups/10.txt ]; then TEST_GROUP=$(<./tmp/test-groups/10.txt); elif [ -f ./tmp/test-groups/0.txt ]; then TEST_GROUP=XXXXX; else TEST_GROUP='^[t-z]'; fi
echo $TEST_GROUP;
eval $PRE_TEST_COMMANDS;
./meteor self-test \
"$TEST_GROUP" \
--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
- run:
<<: *run_save_node_bin
- store_test_results:
path: ./tmp/results
- persist_to_workspace:
root: .
paths: ./tmp/results/junit
- store_artifacts:
path: ./tmp/results
- store_artifacts:
path: /tmp/core_dumps
- store_artifacts:
path: /tmp/memuse.txt
# Test the JSDoc declarations which live within this codebase against the
# Meteor Docs (https://github.com/meteor/docs) repository, where they'll
# eventually be consumed. This test aims to provide an early warning of
# potentially breaking changes, so they aren't discovered when the docs are
# next updated, which generally occurs during major Meteor version releases
# (for example, 1.4 to 1.5, 1.5 to 1.6).
Docs:
docker:
# This Node version should match that in the meteor/docs CircleCI config.
- image: circleci/node:8
environment:
CHECKOUT_METEOR_DOCS: /home/circleci/test_docs
steps:
- run:
name: Cloning "meteor/docs" Repository's "master" branch
command: |
git clone https://github.com/meteor/docs.git ${CHECKOUT_METEOR_DOCS}
# The "docs" repository normally brings in the Meteor code as a Git
# submodule checked out into the "code" directory. As the goal of this
# test is to run it against the _current_ repository's code, we'll move
# the "code" directory out of the way and move the checkout (of meteor)
# into that directory, rather than the default $CIRCLE_WORKING_DIRECTORY.
- checkout
- run:
name: Move Meteor checkout into docs repository's "code" directory
command: |
rmdir "${CHECKOUT_METEOR_DOCS}/code"
# $CIRCLE_WORKING_DIRECTORY uses a tilde, so expand it to $HOME.
mv "${CIRCLE_WORKING_DIRECTORY/#\~/$HOME}" \
"${CHECKOUT_METEOR_DOCS}/code"
# Run almost the same steps the meteor/docs repository runs, minus deploy.
- run:
name: Generating Meteor documentation for JSDoc testing
command: |
cd ${CHECKOUT_METEOR_DOCS}
npm install
npm test
Clean Up:
<<: *build_machine_environment
steps:
- attach_workspace:
at: .
- run:
name: Create Test Groups Directory
command: |
sudo mkdir -p ./tmp/test-groups
sudo chmod a+rwx ./tmp/test-groups
- run:
name: Calculate Balanced Test Groups
command: |
npm install --prefix ./scripts/test-balancer
npm start --prefix ./scripts/test-balancer --num-groups ${NUM_GROUPS} --running-avg-length ${RUNNING_AVG_LENGTH}
- save_cache:
key: v1-test-groups-{{ .Branch }}-{{ .BuildNum }}
paths:
- ./tmp/test-groups
when: on_success
- save_cache:
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-v3-{{ 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
- packages/inter-process-messaging/.npm/package/node_modules
- packages/fetch/.npm/package/node_modules
- packages/non-core/mongo-decimal/.npm/package/node_modules
- save_cache:
key: v2-other-deps-cache-{{ .Branch }}-{{ .Revision }}
paths:
- ".babel-cache"
- ".meteor"
workflows:
version: 2
Build and Test:
jobs:
- Docs
- Get Ready
- Group 0:
- Isolated Tests:
requires:
- Get Ready
- Group 1:
- Test Group 0:
requires:
- Get Ready
- Group 2:
- Test Group 1:
requires:
- Get Ready
- Group 3:
- Test Group 2:
requires:
- Get Ready
- Group 4:
- Test Group 3:
requires:
- Get Ready
- Group 5:
- Test Group 4:
requires:
- Get Ready
- Group 6:
- Test Group 5:
requires:
- Get Ready
- Group 7:
- Test Group 6:
requires:
- Get Ready
- Test Group 7:
requires:
- Get Ready
- Test Group 8:
requires:
- Get Ready
- Test Group 9:
requires:
- Get Ready
- Test Group 10:
requires:
- Get Ready
- Clean Up:
requires:
- Isolated Tests
- Test Group 0
- Test Group 1
- Test Group 2
- Test Group 3
- Test Group 4
- Test Group 5
- Test Group 6
- Test Group 7
- Test Group 8
- Test Group 9
- Test Group 10

View File

@@ -12,9 +12,9 @@ first and read the instructions for filing a bug report:
https://github.com/meteor/meteor/blob/devel/CONTRIBUTING.md#reporting-a-bug-in-meteor
### This bug report should include:
- [ ] A descriptive title
- [ ] A short, but descriptive title. The title doesn't need "Meteor" in it.
- [ ] The version of Meteor showing the problem.
- [ ] The last version of Meteor where the problem did _not_ occur (if applicable)
- [ ] The last version of Meteor where the problem did _not_ occur, if applicable.
- [ ] The operating system you're running Meteor on.
- [ ] The expected behavior.
- [ ] The actual behavior.

View File

@@ -1,11 +1,11 @@
language: node_js
node_js:
- "4.0"
- "8.11.1"
cache:
directories:
- ".meteor"
- ".babel-cache"
script: TEST_PACKAGES_EXCLUDE="less" ./packages/test-in-console/run.sh
script: TEST_PACKAGES_EXCLUDE="less" phantom=false ./packages/test-in-console/run.sh
sudo: false
env:
- CXX=g++-4.8

View File

@@ -11,7 +11,7 @@ Before we jump into detailed guidelines for opening and triaging issues and subm
There are many ways to contribute to the Meteor Project. Heres a list of technical contributions with increasing levels of involvement and required knowledge of Meteors code and operations.
- [Reporting a bug](CONTRIBUTING.md#reporting-a-bug-in-meteor)
- [Triaging issues](ISSUE_TRIAGE.md)
- [Contributing to documentation](https://github.com/meteor/docs/blob/master/CONTRIBUTING.md)
- [Contributing to documentation](CONTRIBUTING.md#documentation)
- [Finding work](CONTRIBUTING.md#finding-work)
- [Submitting pull requests](CONTRIBUTING.md#making-changes-to-meteor-core)
- [Reviewing pull requests](CONTRIBUTING.md#reviewer)
@@ -29,6 +29,8 @@ Issues which *also* have the `confirmed` label ([bugs](https://github.com/meteor
Any issue which does not have the `confirmed` label still requires discussion on implementation details but input and positive commentary is welcome! Any pull request opened on an issue which is not `confirmed` is still welcome, however the pull-request is more likely to be sent back for reworking than a `confirmed` issue. If in doubt about the best way to implement something, please create additional conversation on the issue.
Please note that `pull-requests-encouraged` issues with low activity will often be closed without being implemented. These issues are tagged with an additional [`not-implemented`](https://github.com/meteor/meteor/issues?utf8=✓&q=label%3Apull-requests-encouraged+label%3Anot-implemented) label, and can still be considered good candidates to work on. If you're interested in working on a closed and `not-implemented` issue, please let us know by posting on that issue.
### Project roles
Weve just begun to create more defined project roles for Meteor. Here are descriptions of the existing project roles, along with the current contributors taking on those roles today.
@@ -168,7 +170,7 @@ A great way to contribute to Meteor is by helping keep the issues in the reposit
## Documentation
If you'd like to contribution to Meteor's documentation, head over to https://github.com/meteor/docs and create issues or pull requests there.
If you'd like to contribute to Meteor's documentation, head over to https://github.com/meteor/docs and create issues or pull requests there.
## Blaze

1206
History.md

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,28 @@ The first step is in determining whether the issue is a bug, help question or fe
4. A reproduction should be confirmed by at least one person other than the original reporter. Run the reproduction and validate that the bug exists; then make a note of your findings on the issue. If a reproduction is supplied but doesn't work, add the `can't-reproduce` label and make a comment describing what happened.
5. Finally, once you've confirmed the reproduction add the `confirmed` label and [classify](#classification) the issue (removing the `can't-reproduce` label if it exists).
#### Bug issue lifespan
To help keep issues in this repository under control, and make sure the most important problems are visible to maintainers, unresolved issues (lacking recent activity) should be closed after a certain amount of time has elapsed.
##### Issues labelled with `pull-requests-encouraged`
- Open `pull-requests-encouraged` issues should be closed after one month of inactivity, unless someone has clearly identified that they are interested in working on the issue.
- When closing, the `not-implemented` label should be added.
- A message similar to the following should be included:
> While we think resolving this issue would be a great addition to the Meteor project, we're going to close it for now due to inactivity. If anyone comes across this issue in the future, and is interested in working on resolving it, please let us know by posting here and we'll consider re-opening this issue. Thanks!
##### Issues labelled with `bug` and `confirmed`
- Open `bug` + `confirmed` issues should be closed after two months of inactivity, unless someone has clearly identified that they are interested in working on the issue.
- Triagers should do everything possible to help get `bug` + `confirmed` issues to `pull-requests-encouraged`. This means helping clearly identify where the problem is, pointing towards parts of the codebase that someone might want to look into, documenting what a potential solution looks like, etc.
##### All other issues
- All open issues that cant be labelled as `bug` + `confirmed` and/or `pull-requests-encouraged`, should be closed after one month of inactivity.
- Triagers should do everything possible to help get `bug` + `confirmed` issues to `pull-requests-encouraged`.
### Help questions
[Stack Overflow](http://stackoverflow.com/questions/tagged/meteor) and our [forums](https://forums.meteor.com/c/help) are the place to ask for help on using the framework. Close issues that are help requests and politely refer the author to the above locations.

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2011 - 2017 Meteor Development Group, Inc.
Copyright (c) 2011 - 2018 Meteor Development Group, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

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

View File

@@ -2,6 +2,7 @@
[![TravisCI Status](https://travis-ci.org/meteor/meteor.svg?branch=devel)](https://travis-ci.org/meteor/meteor)
[![CircleCI Status](https://circleci.com/gh/meteor/meteor/tree/devel.svg?style=shield&circle-token=c2d3c041506bd493ef3795ffa4448684cfce97b8)](https://circleci.com/gh/meteor/meteor/tree/devel)
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=S2FjOVpYTlk1eHVxdkdrR24ra0JXaXBDaVA3WjJGejdkbThaWGRnWVJvWT0tLTlQMEdYM2NJbDJOYUd3RTc3RVVGQ2c9PQ==--9e2143718a0c216274cdacef7cd5a3d12950bcb8)](https://www.browserstack.com/automate/public-build/S2FjOVpYTlk1eHVxdkdrR24ra0JXaXBDaVA3WjJGejdkbThaWGRnWVJvWT0tLTlQMEdYM2NJbDJOYUd3RTc3RVVGQ2c9PQ==--9e2143718a0c216274cdacef7cd5a3d12950bcb8)
Meteor is an ultra-simple environment for building modern web
applications.

View File

@@ -2,7 +2,7 @@
# Meteor Roadmap
**Up to date as of September 8, 2017**
**Up to date as of October 9, 2018**
This document describes the high level features the Meteor project maintainers have decided to prioritize in the near- to medium-term future. A large fraction of the maintainers time will be dedicated to working on the features described here. As with any roadmap, this is a living document that will evolve as priorities and dependencies shift; we aim to update the roadmap with any changes or status updates on a monthly basis.
@@ -10,28 +10,6 @@ Contributors are encouraged to focus their efforts on work that aligns with the
Items can be added to this roadmap by first getting design approval for a solution to an open issue, as outlined by our [contributing guidelines](https://github.com/meteor/meteor/blob/devel/CONTRIBUTING.md). Then, when a contributor has committed to solving the issue in the short to medium term, they can submit a PR to add that work to the roadmap. All other PRs to the roadmap will be rejected.
## Upgrade to Node 8
*Tracking pull request: https://github.com/meteor/meteor/pull/8728*
Upgrading Node will allow Meteor to take better advantage of native support for new ECMAScript features on the server, which should speed up build performance and also improve runtime performance, thanks to performance improvements in Node itself.
Perhaps even more importantly, newer versions of Node support a vastly improved debugging experience. Not only can you use native Chrome DevTools and many other debugging clients (WebStorm, VS Code, etc.) to debug your app (no more [`node-inspector`](https://www.npmjs.com/package/node-inspector)), but also the Node process runs at full speed while debugging, so you don't have to wait as long for problems to manifest themselves.
## Upgrade to npm 5
*Status: implemented since `1.6-beta.4`.*
Its been an interesting year for npm clients. Once unrivaled as the tool of choice for installing npm packages, the `npm` command-line tool faced some serious competition starting last September from an innovative tool called `yarn`, which promised fast, reproducible installs based on `yarn.lock` files.
The popularity of `yarn` led Meteor to support `meteor yarn` in addition to `meteor npm` (though you had to `meteor npm install --global yarn` first, so `npm` still had an advantage). Our own Galaxy Server and Optics apps, which are built with Meteor, switched over to `yarn` soon after its release. The appeal was undeniable.
This competition was a good thing for JavaScript developers, first because yarn solved some long-standing problems with `npm`, and later because `npm@5` responded by shipping its own implementation of some similar features, with `package-lock.json` files and automatic addition of npm install-ed packages to `package.json`.
These improvements to `npm` will benefit all Meteor developers, even those who keep using `yarn`, because package dependencies specified with `Npm.depends` are automatically installed using `npm`, and `npm@5` performs those installations much faster and more reliably.
Meteor is careful to remain agnostic about how you choose to populate your `node_modules` directories, so we fully expect that `meteor npm` and `meteor yarn` will remain equally good alternatives for that purpose.
## Out of the box support for advanced React features
React is the most popular way to build UIs in JavaScript today, and a great companion to the rest of the features provided by Meteor. Meteor's zero-configuration environment provides a great opportunity to make features React apps depend on work out of the box. This includes features like:
@@ -45,28 +23,6 @@ We think Meteor has a clear set of benefits when compared to other popular React
## Remove blockers to Meteor adoption
### Support the latest version of Node
*Tracking pull request: https://github.com/meteor/meteor/pull/8728*
See [above](https://github.com/meteor/meteor/blob/devel/Roadmap.md#upgrade-to-node-8). Developers deserve to use the latest underlying technologies, and Meteor is uniquely able to smooth over any rough edges in early/experimental versions of technologies like Node. A number of developers are already using beta versions of Meteor 1.6 to deploy their apps, because the benefits outweigh the risks for them. Just as Meteor 1.5 climbed to more than 50% usage in less than two months, we expect Meteor 1.6 to become the most widely used version of Meteor soon after its release.
### Make Mongo more optional
*Preliminary solution: https://github.com/meteor/meteor/pull/8999*
Meteor has depended on Mongo for as long as the Meteor project has existed. However, we care deeply about supporting other data storage systems (especially via [GraphQL](https://www.apollodata.com/)), and would like to make it possible to avoid using Mongo altogether.
### Get rid of the `imports` directory
When Meteor 1.3 first introduced a module system based on [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) and [ECMAScript module syntax](2ality.com/2014/09/es6-modules-final.html), we had to provide a way for developers to migrate their apps from the old ways of loading code, whereby all files were evaluated eagerly during application startup.
The best solution at the time was to introduce a special `imports` directory to contain modules that should be loaded lazily (rather than eagerly), when first imported.
Most other Node applications work this way by default: every module is lazy, and therefore must be imported by another module, and evaluation starts with one "entry point" module (typically specified by the `"main"` field in `package.json`).
It should be possible for Meteor apps to opt into this behavior, and optionally get rid of their special `imports` directories. The mechanism for opting in will very likely involve putting something in your `package.json` file that specifies entry point modules for both client and server.
### Make the `meteor` command-line tool installable from npm
Installing `meteor` from npm would enable developers to use it as build tool for npm-based projects, and would simplify the Meteor release process by getting rid of the "dev bundle" (essentially the npm dependencies of the command-line tool).
@@ -116,10 +72,70 @@ Apollo is our approach to giving Meteor developers SQL and other database suppor
Even though Apollo could eventually be a complete replacement for Meteors included Mongo/DDP data stack, you should feel good about Meteors existing data system. We are currently open to ideas around performance and stability improvements.
# **Recently completed**
## Different JS bundles for modern versus legacy browsers
*Pull request: https://github.com/meteor/meteor/pull/9439*
Despite amazing progress in the latest versions of popular web browsers to support the vast majority of the ECMAScript specification, most web applications are still forced to compile their JavaScript for the oldest browsers they want to support, which means native support for the latest features is usually off-limits.
Starting in Meteor 1.6.2, Meteor will build two different client JS bundles, one for modern browsers (`web.browser`) and another for legacy browsers (`web.browser.legacy` and `web.cordova`), in addition to the server bundle which targets Node 8. Package authors can use these architectures to include files only in legacy browsers, or only in modern browsers, while also setting minimum browser versions for the native features they require. As of this writing, modern browsers are loosely defined as any browsers with native support for `async` functions and `await` expressions, which represents [more than 80% of web usage today](https://caniuse.com/#feat=async-functions).
While it was tempting to compile even more bundles for different categories of browser support, the reality of the web today is that most users have access to self-updating "evergreen" browsers, with nearly complete ECMAScript support, and the market share of evergreen browsers is only going to increase with time. For everyone else, Meteor will automatically provide the same level of compilation provided to everyone by Meteor 1.6.1 and before. It's also a lot easier to test two different bundles in representative browsers than it is to test a whole matrix of possibilities.
As a result of these changes, a typical new Meteor app will have a modern client JS bundle that is one quarter to one third the size of the legacy JS bundle. A new app created with `meteor create --release 1.6.2-beta.12 --minimal new-app` will have a modern JS bundle just 15KB in size (minified + gzip), for example.
### Eliminate the need for an `imports` directory
*Status: possible using `meteor.mainModule` in `package.json` in `1.6.2-beta.12`.*
*Pull requests: https://github.com/meteor/meteor/pull/9690, https://github.com/meteor/meteor/pull/9714, https://github.com/meteor/meteor/pull/9715*
When Meteor 1.3 first introduced a module system based on [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) and [ECMAScript module syntax](2ality.com/2014/09/es6-modules-final.html), we had to provide a way for developers to migrate their apps from the old ways of loading code, whereby all files were evaluated eagerly during application startup.
The best solution at the time was to introduce a special `imports` directory to contain modules that should be loaded lazily (rather than eagerly), when first imported.
Most other Node applications work this way by default: every module is lazy, and therefore must be imported by another module, and evaluation starts with one "entry point" module (typically specified by the `"main"` field in `package.json`).
It should be possible for Meteor apps to opt into this behavior, and optionally get rid of their special `imports` directories. The mechanism for opting in will very likely involve putting something in your `package.json` file that specifies entry point modules for both client and server.
## Make Mongo more optional
*Pull request: https://github.com/meteor/meteor/pull/8999*
Meteor has depended on Mongo for as long as the Meteor project has existed. However, we care deeply about supporting other data storage systems (especially via [GraphQL](https://www.apollodata.com/)), and would like to make it possible to avoid using Mongo altogether.
Since Meteor 1.6.2-beta.9, `meteor create --minimal minimal-app` will create an app with very few packages, without any dependency on Mongo.
### Support the latest stable version of Node
*Tracking pull request: https://github.com/meteor/meteor/pull/8728*
See [above](https://github.com/meteor/meteor/blob/devel/Roadmap.md#upgrade-to-node-8). Developers deserve to use the latest underlying technologies, and Meteor is uniquely able to smooth over any rough edges in early/experimental versions of technologies like Node. A number of developers are already using beta versions of Meteor 1.6 to deploy their apps, because the benefits outweigh the risks for them. Just as Meteor 1.5 climbed to more than 50% usage in less than two months, we expect Meteor 1.6 to become the most widely used version of Meteor soon after its release.
## Upgrade to Node 8
*Status: shipped in Meteor 1.6.*
*Pull request: https://github.com/meteor/meteor/pull/8728*
Upgrading Node will allow Meteor to take better advantage of native support for new ECMAScript features on the server, which should speed up build performance and also improve runtime performance, thanks to performance improvements in Node itself.
Perhaps even more importantly, newer versions of Node support a vastly improved debugging experience. Not only can you use native Chrome DevTools and many other debugging clients (WebStorm, VS Code, etc.) to debug your app (no more [`node-inspector`](https://www.npmjs.com/package/node-inspector)), but also the Node process runs at full speed while debugging, so you don't have to wait as long for problems to manifest themselves.
## Upgrade to npm 5
*Status: implemented since `1.6-beta.4`.*
Its been an interesting year for npm clients. Once unrivaled as the tool of choice for installing npm packages, the `npm` command-line tool faced some serious competition starting last September from an innovative tool called `yarn`, which promised fast, reproducible installs based on `yarn.lock` files.
The popularity of `yarn` led Meteor to support `meteor yarn` in addition to `meteor npm` (though you had to `meteor npm install --global yarn` first, so `npm` still had an advantage). Our own Galaxy Server and Optics apps, which are built with Meteor, switched over to `yarn` soon after its release. The appeal was undeniable.
This competition was a good thing for JavaScript developers, first because yarn solved some long-standing problems with `npm`, and later because `npm@5` responded by shipping its own implementation of some similar features, with `package-lock.json` files and automatic addition of npm install-ed packages to `package.json`.
These improvements to `npm` will benefit all Meteor developers, even those who keep using `yarn`, because package dependencies specified with `Npm.depends` are automatically installed using `npm`, and `npm@5` performs those installations much faster and more reliably.
Meteor is careful to remain agnostic about how you choose to populate your `node_modules` directories, so we fully expect that `meteor npm` and `meteor yarn` will remain equally good alternatives for that purpose.
## Dynamic `import(...)`
*Status: Shipped in 1.5*

View File

@@ -15,12 +15,7 @@ environment:
TOOL_NODE_FLAGS: --expose-gc
TIMEOUT_SCALE_FACTOR: 8
METEOR_HEADLESS: true
SELF_TEST_EXCLUDE: "\
^old cli tests|\
^minifiers can't register non-js|\
^minifiers: apps can't use|\
^compiler plugins - addAssets|\
^NULL-LEAVE-THIS-HERE-NULL$"
SELF_TEST_EXCLUDE: "^NULL-LEAVE-THIS-HERE-NULL$"
platform:
- x64
@@ -38,6 +33,14 @@ install:
test_script:
- ps: C:\projects\meteor\scripts\windows\appveyor\test.ps1
on_failure:
- ps: |
$npmLogsDir = "$($Env:AppData)\npm-cache\_logs"
If (Test-Path "$npmLogsDir") {
Get-ChildItem "${npmLogsDir}\*.log" |
% { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
}
cache:
- dev_bundle -> meteor
- .babel-cache

View File

@@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
wllgu394zq2.rrlkgpniscl

View File

@@ -11,6 +11,8 @@ less
accounts-google
accounts-github
accounts-password
underscore
accounts-facebook
standard-app-packages
facebook-config-ui
github-config-ui
google-config-ui

View File

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

View File

@@ -1,23 +1,30 @@
Meteor.users.allow({update: function () { return true; }});
Meteor.users.allow({ update: () => true });
const { ServiceConfiguration } = Package['service-configuration'];
Meteor.methods({
'removeService': service => ServiceConfiguration.configurations.remove({ service }),
})
if (Meteor.isClient) {
Accounts.STASH = _.extend({}, Accounts);
Accounts.STASH = { ...Accounts };
Accounts.STASH.loggingIn = Meteor.loggingIn;
var handleSetting = function (key, value) {
const handleSetting = (key, value) => {
if (key === "numServices") {
_.each(['facebook', 'github', 'google'],
function (serv, i) {
if (i < value)
Accounts[serv] = Accounts.STASH[serv];
else
Accounts[serv] = null;
});
const registeredServices = Accounts.oauth.serviceNames();
['facebook', 'github', 'google'].forEach((serv, i) => {
if (i < value && !registeredServices.includes(serv)) {
Accounts.oauth.registerService(serv);
} else if (i >= value && registeredServices.includes(serv)) {
Accounts.oauth.unregisterService(serv);
}
});
} else if (key === "hasPasswords") {
Accounts.password = value && Accounts.STASH.password || null;
var user = Meteor.user();
Package['accounts-password'] = value ? {} : null;
const user = Meteor.user();
if (user) {
if (! value) {
// make sure we have no username if "app" has no passwords
@@ -32,12 +39,13 @@ if (Meteor.isClient) {
} else if (key === "signupFields") {
Accounts.ui._options.passwordSignupFields = value;
} else if (key === "fakeLoggingIn") {
Meteor.loggingIn = (value ? function () { return true; } :
Meteor.loggingIn = (value ? () => true :
Accounts.STASH.loggingIn);
}
};
if (! Session.get('settings'))
const settings = Session.get('settings');
if (! settings) {
Session.set('settings', {
alignRight: false,
positioning: "relative",
@@ -47,22 +55,32 @@ if (Meteor.isClient) {
fakeLoggingIn: false,
bgcolor: 'white'
});
else
_.each(Session.get('settings'), function (v,k) {
handleSetting(k, v);
});
} else {
Object.keys(settings).forEach(key => handleSetting(key, settings[key]));
}
Template.page.settings = function () {
return Session.get('settings');
};
Template.page.helpers({
settings: () => Session.get('settings'),
settingsClass: () => {
var settings = Session.get('settings');
var classes = [];
if (settings.positioning)
classes.push('positioning-' + settings.positioning.toLowerCase());
return classes.join(' ');
},
match: kv => {
kv = keyValueFromId(kv);
if (! kv)
return false;
return Session.get('settings')[kv[0]] === kv[1];
},
dropdownAlign: function() {
var settings = this;
return settings.alignRight ? 'right' : 'left';
}
});
Template.page.settingsClass = function () {
var settings = Session.get('settings');
var classes = [];
if (settings.positioning)
classes.push('positioning-' + settings.positioning.toLowerCase());
return classes.join(' ');
};
var keyValueFromId = function (id) {
var match;
@@ -74,7 +92,7 @@ if (Meteor.isClient) {
return null;
};
var castValue = function (value) {
const castValue = value => {
if (value === "false")
value = false;
else if (value === "true")
@@ -84,32 +102,21 @@ if (Meteor.isClient) {
return value;
};
Template.radio.maybeChecked = function () {
var curValue = Session.get('settings')[this.key];
if (castValue(this.value) === curValue)
return 'checked';
return '';
};
Template.radio.helpers({
maybeChecked: function() {
var curValue = Session.get('settings')[this.key];
if (castValue(this.value) === curValue)
return 'checked';
return '';
},
});
Template.page.match = function (kv) {
kv = keyValueFromId(kv);
if (! kv)
return false;
return Session.get('settings')[kv[0]] === kv[1];
};
Template.page.dropdownAlign = function () {
var settings = this;
return settings.alignRight ? 'right' : 'left';
};
var fakeLogin = function (callback) {
const fakeLogin = callback => {
Accounts.createUser(
{username: Random.id(),
password: "password",
profile: { name: "Joe Schmoe" }},
function () {
() => {
var user = Meteor.user();
if (! user)
return;
@@ -124,7 +131,7 @@ if (Meteor.isClient) {
});
};
var exitFlows = function () {
const exitFlows = () => {
Accounts._loginButtonsSession.set('inSignupFlow', false);
Accounts._loginButtonsSession.set('inForgotPasswordFlow', false);
Accounts._loginButtonsSession.set('inChangePasswordFlow', false);
@@ -132,17 +139,17 @@ if (Meteor.isClient) {
};
Template.page.events({
'change #controlpane input[type=radio]': function (event) {
var input = event.currentTarget;
var keyValue;
'change #controlpane input[type=radio]': event => {
const input = event.currentTarget;
let keyValue;
if (input && input.id && (keyValue = keyValueFromId(input.id))) {
var key = keyValue[0];
var value = keyValue[1];
const key = keyValue[0];
const value = keyValue[1];
if (value === "false")
value = false;
else if (value === "true")
value = true;
var settings = Session.get('settings');
const settings = Session.get('settings');
settings[key] = value;
Session.set('settings', settings);
@@ -150,14 +157,15 @@ if (Meteor.isClient) {
}
},
'click #controlpane button': function (event) {
const { ServiceConfiguration } = Package['service-configuration'];
if (this.key === "fakeConfig") {
var service = this.value;
if (! ServiceConfiguration.configurations.findOne({service: service}))
const service = this.value;
if (! ServiceConfiguration.configurations.findOne({ service }))
ServiceConfiguration.configurations.insert(
{service: service, fake: true});
{ service, fake: true });
} else if (this.key === "unconfig") {
var service = this.value;
ServiceConfiguration.configurations.remove({service: service});
const service = this.value;
Meteor.call('removeService', service);
} else if (this.key === "messages") {
if (this.value === "error") {
Accounts._loginButtonsSession.errorMessage('An error occurred! Gee golly gosh.');
@@ -190,20 +198,22 @@ if (Meteor.isClient) {
exitFlows();
Accounts._loginButtonsSession.set("dropdownVisible", true);
if (! Meteor.userId())
fakeLogin();
fakeLogin(() => {});
if (this.value === "changePassword")
Accounts._loginButtonsSession.set("inChangePasswordFlow", true);
else if (this.value === "messageOnly")
Accounts._loginButtonsSession.set("inMessageOnlyFlow", true);
} else if (this.key === "modals") {
var value = this.value;
_.each([
const { value } = this;
[
'resetPasswordToken',
'enrollAccountToken',
'justVerifiedEmail'], function (k) {
Accounts._loginButtonsSession.set(
k, k.indexOf(value) >= 0 ? 'foo' : null);
});
'justVerifiedEmail'
].forEach(k => {
Accounts._loginButtonsSession.set(
k, k.indexOf(value) >= 0 ? 'foo' : null
);
});
}
}
});

View File

@@ -0,0 +1,682 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@babel/runtime": {
"version": "7.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.38.tgz",
"integrity": "sha512-ZvPtlcvH2ZRzr1U5pkmCE7U3RIun3Nf29XHem47aScmJgMuL06ulkp+4oPBee3QrUVFErDjwNWtC67BzNuxLVw==",
"requires": {
"core-js": "2.5.3",
"regenerator-runtime": "0.11.1"
}
},
"core-js": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz",
"integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4="
},
"meteor-node-stubs": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-0.3.2.tgz",
"integrity": "sha512-l93SS/HutbqBRJODO2m7hup8cYI2acF5bB39+ZvP2BX8HMmCSCXeFH7v0sr4hD7zrVvHQA5UqS0pcDYKn0VM6g==",
"requires": {
"assert": "1.4.1",
"browserify-zlib": "0.1.4",
"buffer": "4.9.1",
"console-browserify": "1.1.0",
"constants-browserify": "1.0.0",
"crypto-browserify": "3.11.1",
"domain-browser": "1.1.7",
"events": "1.1.1",
"http-browserify": "1.7.0",
"https-browserify": "0.0.1",
"os-browserify": "0.2.1",
"path-browserify": "0.0.0",
"process": "0.11.10",
"punycode": "1.4.1",
"querystring-es3": "0.2.1",
"readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502",
"stream-browserify": "2.0.1",
"string_decoder": "1.0.3",
"timers-browserify": "1.4.2",
"tty-browserify": "0.0.0",
"url": "0.11.0",
"util": "0.10.3",
"vm-browserify": "0.0.4"
},
"dependencies": {
"Base64": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz",
"integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg="
},
"asn1.js": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz",
"integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=",
"requires": {
"bn.js": "4.11.8",
"inherits": "2.0.1",
"minimalistic-assert": "1.0.0"
}
},
"assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"requires": {
"util": "0.10.3"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base64-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw=="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"brace-expansion": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browserify-aes": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.0.tgz",
"integrity": "sha512-W2bIMLYoZ9oow7TyePpMJk9l9LY7O3R61a/68bVCDOtnJynnwe3ZeW2IzzSkrQnPKNdJrxVDn3ALZNisSBwb7g==",
"requires": {
"buffer-xor": "1.0.3",
"cipher-base": "1.0.4",
"create-hash": "1.1.3",
"evp_bytestokey": "1.0.3",
"inherits": "2.0.1",
"safe-buffer": "5.1.1"
}
},
"browserify-cipher": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz",
"integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=",
"requires": {
"browserify-aes": "1.1.0",
"browserify-des": "1.0.0",
"evp_bytestokey": "1.0.3"
}
},
"browserify-des": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz",
"integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=",
"requires": {
"cipher-base": "1.0.4",
"des.js": "1.0.0",
"inherits": "2.0.1"
}
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": {
"bn.js": "4.11.8",
"randombytes": "2.0.5"
}
},
"browserify-sign": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
"requires": {
"bn.js": "4.11.8",
"browserify-rsa": "4.0.1",
"create-hash": "1.1.3",
"create-hmac": "1.1.6",
"elliptic": "6.4.0",
"inherits": "2.0.1",
"parse-asn1": "5.1.0"
}
},
"browserify-zlib": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
"integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=",
"requires": {
"pako": "0.2.9"
}
},
"buffer": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "1.2.1",
"ieee754": "1.1.8",
"isarray": "1.0.0"
}
},
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"requires": {
"inherits": "2.0.1",
"safe-buffer": "5.1.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
"integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
"requires": {
"date-now": "0.1.4"
}
},
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
},
"create-ecdh": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz",
"integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=",
"requires": {
"bn.js": "4.11.8",
"elliptic": "6.4.0"
}
},
"create-hash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
"integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=",
"requires": {
"cipher-base": "1.0.4",
"inherits": "2.0.1",
"ripemd160": "2.0.1",
"sha.js": "2.4.9"
}
},
"create-hmac": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz",
"integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=",
"requires": {
"cipher-base": "1.0.4",
"create-hash": "1.1.3",
"inherits": "2.0.1",
"ripemd160": "2.0.1",
"safe-buffer": "5.1.1",
"sha.js": "2.4.9"
}
},
"crypto-browserify": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz",
"integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==",
"requires": {
"browserify-cipher": "1.0.0",
"browserify-sign": "4.0.4",
"create-ecdh": "4.0.0",
"create-hash": "1.1.3",
"create-hmac": "1.1.6",
"diffie-hellman": "5.0.2",
"inherits": "2.0.1",
"pbkdf2": "3.0.14",
"public-encrypt": "4.0.0",
"randombytes": "2.0.5"
}
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
"integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
},
"des.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
"integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
"requires": {
"inherits": "2.0.1",
"minimalistic-assert": "1.0.0"
}
},
"diffie-hellman": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz",
"integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=",
"requires": {
"bn.js": "4.11.8",
"miller-rabin": "4.0.1",
"randombytes": "2.0.5"
}
},
"domain-browser": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
"integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw="
},
"elliptic": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
"integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
"requires": {
"bn.js": "4.11.8",
"brorand": "1.1.0",
"hash.js": "1.1.3",
"hmac-drbg": "1.0.1",
"inherits": "2.0.1",
"minimalistic-assert": "1.0.0",
"minimalistic-crypto-utils": "1.0.1"
}
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"requires": {
"md5.js": "1.3.4",
"safe-buffer": "5.1.1"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.1",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"hash-base": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
"integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=",
"requires": {
"inherits": "2.0.1"
}
},
"hash.js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
"integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
"requires": {
"inherits": "2.0.3",
"minimalistic-assert": "1.0.0"
},
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"requires": {
"hash.js": "1.1.3",
"minimalistic-assert": "1.0.0",
"minimalistic-crypto-utils": "1.0.1"
}
},
"http-browserify": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz",
"integrity": "sha1-M3la3nLfiKz7/TZ3PO/tp2RzWyA=",
"requires": {
"Base64": "0.2.1",
"inherits": "2.0.1"
}
},
"https-browserify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz",
"integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI="
},
"ieee754": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
"integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q="
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"md5.js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
"integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
"requires": {
"hash-base": "3.0.4",
"inherits": "2.0.1"
},
"dependencies": {
"hash-base": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
"requires": {
"inherits": "2.0.1",
"safe-buffer": "5.1.1"
}
}
}
},
"miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
"requires": {
"bn.js": "4.11.8",
"brorand": "1.1.0"
}
},
"minimalistic-assert": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz",
"integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "1.1.8"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1.0.2"
}
},
"os-browserify": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz",
"integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8="
},
"pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU="
},
"parse-asn1": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz",
"integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=",
"requires": {
"asn1.js": "4.9.1",
"browserify-aes": "1.1.0",
"create-hash": "1.1.3",
"evp_bytestokey": "1.0.3",
"pbkdf2": "3.0.14"
}
},
"path-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
"integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"pbkdf2": {
"version": "3.0.14",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz",
"integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==",
"requires": {
"create-hash": "1.1.3",
"create-hmac": "1.1.6",
"ripemd160": "2.0.1",
"safe-buffer": "5.1.1",
"sha.js": "2.4.9"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"public-encrypt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz",
"integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=",
"requires": {
"bn.js": "4.11.8",
"browserify-rsa": "4.0.1",
"create-hash": "1.1.3",
"parse-asn1": "5.1.0",
"randombytes": "2.0.5"
}
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"querystring-es3": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
},
"randombytes": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz",
"integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==",
"requires": {
"safe-buffer": "5.1.1"
}
},
"readable-stream": {
"version": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502",
"requires": {
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
},
"dependencies": {
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"requires": {
"glob": "7.1.2"
}
},
"ripemd160": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
"integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=",
"requires": {
"hash-base": "2.0.2",
"inherits": "2.0.1"
}
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"sha.js": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz",
"integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==",
"requires": {
"inherits": "2.0.1",
"safe-buffer": "5.1.1"
}
},
"stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
"requires": {
"inherits": "2.0.1",
"readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"requires": {
"safe-buffer": "5.1.1"
}
},
"timers-browserify": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz",
"integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=",
"requires": {
"process": "0.11.10"
}
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
},
"url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
},
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"requires": {
"inherits": "2.0.1"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"vm-browserify": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
"integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
"requires": {
"indexof": "0.0.1"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
},
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "accounts-ui-viewer",
"private": true,
"scripts": {
"start": "meteor run"
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.38",
"meteor-node-stubs": "^0.3.2"
}
}

12
meteor
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
BUNDLE_VERSION=8.8.1
# Note: Skip 8.12.0.0, as it has already been used.
BUNDLE_VERSION=8.11.4.5
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
@@ -132,7 +133,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" \
--expose-gc \
--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
${TOOL_NODE_FLAGS} \
"$METEOR" "$@"

View File

@@ -46,7 +46,10 @@ IF EXIST "%~dp0\.git" (
SET NODE_PATH=%~dp0\dev_bundle\lib\node_modules
SET BABEL_CACHE_DIR=%~dp0\.babel-cache
"%~dp0\dev_bundle\bin\node.exe" %TOOL_NODE_FLAGS% "%~dp0\tools\index.js" %*
"%~dp0\dev_bundle\bin\node.exe" ^
%TOOL_NODE_FLAGS% ^
"%~dp0\tools\index.js" %*
ENDLOCAL
EXIT /b %ERRORLEVEL%

File diff suppressed because it is too large Load Diff

View File

@@ -71,3 +71,32 @@ Tinytest.addAsync(
});
}
);
Tinytest.addAsync(
'accounts - onLogin callback receives { type: "password" } param on login',
(test, done) => {
const onLogin = Accounts.onLogin((loginDetails) => {
test.equal('password', loginDetails.type);
onLogin.stop();
removeTestUser(done);
});
logoutAndCreateUser(test, done, () => {});
}
);
Tinytest.addAsync(
'accounts - onLogin callback receives { type: "resume" } param on ' +
'reconnect, if already logged in',
(test, done) => {
logoutAndCreateUser(test, done, () => {
const onLogin = Accounts.onLogin((loginDetails) => {
test.equal('resume', loginDetails.type);
onLogin.stop();
removeTestUser(done);
});
Meteor.disconnect();
Meteor.reconnect();
});
}
);

View File

@@ -40,6 +40,33 @@ export class AccountsCommon {
bindEnvironment: false,
debugPrintExceptions: "onLogout callback"
});
// Expose for testing.
this.DEFAULT_LOGIN_EXPIRATION_DAYS = DEFAULT_LOGIN_EXPIRATION_DAYS;
this.LOGIN_UNEXPIRING_TOKEN_DAYS = LOGIN_UNEXPIRING_TOKEN_DAYS;
// Thrown when the user cancels the login process (eg, closes an oauth
// popup, declines retina scan, etc)
const lceName = 'Accounts.LoginCancelledError';
this.LoginCancelledError = Meteor.makeErrorType(
lceName,
function (description) {
this.message = description;
}
);
this.LoginCancelledError.prototype.name = lceName;
// This is used to transmit specific subclass errors over the wire. We
// should come up with a more generic way to do this (eg, with some sort of
// symbolic error code rather than a number).
this.LoginCancelledError.numericError = 0x8acdc2f;
// loginServiceConfiguration and ConfigError are maintained for backwards compatibility
Meteor.startup(() => {
const { ServiceConfiguration } = Package['service-configuration'];
this.loginServiceConfiguration = ServiceConfiguration.configurations;
this.ConfigError = ServiceConfiguration.ConfigError;
});
}
/**
@@ -55,7 +82,7 @@ export class AccountsCommon {
* @locus Anywhere
*/
user() {
var userId = this.userId();
const userId = this.userId();
return userId ? this.users.findOne(userId) : null;
}
@@ -107,8 +134,6 @@ export class AccountsCommon {
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to false.
*/
config(options) {
var self = this;
// We don't want users to accidentally only call Accounts.config on the
// client, where some of the options will have partial effects (eg removing
// the "create account" button from accounts-ui if forbidClientAccountCreation
@@ -126,32 +151,35 @@ export class AccountsCommon {
// We need to validate the oauthSecretKey option at the time
// Accounts.config is called. We also deliberately don't store the
// oauthSecretKey in Accounts._options.
if (_.has(options, "oauthSecretKey")) {
if (Meteor.isClient)
if (Object.prototype.hasOwnProperty.call(options, 'oauthSecretKey')) {
if (Meteor.isClient) {
throw new Error("The oauthSecretKey option may only be specified on the server");
if (! Package["oauth-encryption"])
}
if (! Package["oauth-encryption"]) {
throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey");
}
Package["oauth-encryption"].OAuthEncryption.loadKey(options.oauthSecretKey);
options = _.omit(options, "oauthSecretKey");
options = { ...options };
delete options.oauthSecretKey;
}
// validate option keys
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays",
const VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays",
"restrictCreationByEmailDomain", "loginExpirationInDays", "passwordResetTokenExpirationInDays",
"ambiguousErrorMessages", "bcryptRounds"];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key)) {
throw new Error("Accounts.config: Invalid key: " + key);
Object.keys(options).forEach(key => {
if (!VALID_KEYS.includes(key)) {
throw new Error(`Accounts.config: Invalid key: ${key}`);
}
});
// set values in Accounts._options
_.each(VALID_KEYS, function (key) {
VALID_KEYS.forEach(key => {
if (key in options) {
if (key in self._options) {
throw new Error("Can't set `" + key + "` more than once");
if (key in this._options) {
throw new Error(`Can't set \`${key}\` more than once`);
}
self._options[key] = options[key];
this._options[key] = options[key];
}
});
}
@@ -160,6 +188,12 @@ export class AccountsCommon {
* @summary Register a callback to be called after a login attempt succeeds.
* @locus Anywhere
* @param {Function} func The callback to be called when login is successful.
* The callback receives a single object that
* holds login details. This object contains the login
* result type (password, resume, etc.) on both the
* client and server. `onLogin` callbacks registered
* on the server also receive extra data, such
* as user details, connection information, etc.
*/
onLogin(func) {
return this._onLoginHook.register(func);
@@ -195,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) {
@@ -245,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.
@@ -263,64 +295,30 @@ var Ap = AccountsCommon.prototype;
* @locus Anywhere but publish functions
* @importFromPackage meteor
*/
Meteor.userId = function () {
return Accounts.userId();
};
Meteor.userId = () => Accounts.userId();
/**
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source.
* @locus Anywhere but publish functions
* @importFromPackage meteor
*/
Meteor.user = function () {
return Accounts.user();
};
Meteor.user = () => Accounts.user();
// how long (in days) until a login token expires
const DEFAULT_LOGIN_EXPIRATION_DAYS = 90;
// Expose for testing.
Ap.DEFAULT_LOGIN_EXPIRATION_DAYS = DEFAULT_LOGIN_EXPIRATION_DAYS;
// how long (in days) until reset password token expires
var DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS = 3;
const DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS = 3;
// how long (in days) until enrol password token expires
var DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS = 30;
const DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS = 30;
// Clients don't try to auto-login with a token that is going to expire within
// .1 * DEFAULT_LOGIN_EXPIRATION_DAYS, capped at MIN_TOKEN_LIFETIME_CAP_SECS.
// Tries to avoid abrupt disconnects from expiring tokens.
var MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour
const MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour
// how often (in milliseconds) we check for expired tokens
EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes
export const EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes
// how long we wait before logging out clients when Meteor.logoutOtherClients is
// called
CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
export const CONNECTION_CLOSE_DELAY_MS = 10 * 1000;
// A large number of expiration days (approximately 100 years worth) that is
// used when creating unexpiring tokens.
const LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100;
// Expose for testing.
Ap.LOGIN_UNEXPIRING_TOKEN_DAYS = LOGIN_UNEXPIRING_TOKEN_DAYS;
// loginServiceConfiguration and ConfigError are maintained for backwards compatibility
Meteor.startup(function () {
var ServiceConfiguration =
Package['service-configuration'].ServiceConfiguration;
Ap.loginServiceConfiguration = ServiceConfiguration.configurations;
Ap.ConfigError = ServiceConfiguration.ConfigError;
});
// Thrown when the user cancels the login process (eg, closes an oauth
// popup, declines retina scan, etc)
var lceName = 'Accounts.LoginCancelledError';
Ap.LoginCancelledError = Meteor.makeErrorType(
lceName,
function (description) {
this.message = description;
}
);
Ap.LoginCancelledError.prototype.name = lceName;
// This is used to transmit specific subclass errors over the wire. We should
// come up with a more generic way to do this (eg, with some sort of symbolic
// error code rather than a number).
Ap.LoginCancelledError.numericError = 0x8acdc2f;

View File

@@ -1,31 +0,0 @@
import {AccountsCommon} from "./accounts_common.js";
var Ap = AccountsCommon.prototype;
var defaultRateLimiterRuleId;
// Removes default rate limiting rule
Ap.removeDefaultRateLimit = function () {
const resp = DDPRateLimiter.removeRule(defaultRateLimiterRuleId);
defaultRateLimiterRuleId = null;
return resp;
};
// Add a default rule of limiting logins, creating new users and password reset
// to 5 times every 10 seconds per connection.
Ap.addDefaultRateLimit = function () {
if (!defaultRateLimiterRuleId) {
defaultRateLimiterRuleId = DDPRateLimiter.addRule({
userId: null,
clientAddress: null,
type: 'method',
name: function (name) {
return _.contains(['login', 'createUser', 'resetPassword',
'forgotPassword'], name);
},
connectionId: function (connectionId) {
return true;
}
}, 5, 10000);
}
};
Ap.addDefaultRateLimit();

View File

@@ -14,17 +14,41 @@ 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) => {
function disconnectAndReconnect(callback) {
test.equal(Meteor.status().status, "connected");
function pollUntilDisconnected() {
if (Meteor.status().status === "offline") {
Meteor.reconnect();
pollUntilReconnected();
} else {
Meteor.setTimeout(pollUntilDisconnected, 10);
}
}
function pollUntilReconnected() {
if (Meteor.status().status === "connected") {
if (typeof callback === "function") {
callback();
}
} else {
Meteor.setTimeout(pollUntilReconnected, 10);
}
}
Meteor.disconnect();
pollUntilDisconnected();
}
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,40 +58,38 @@ 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();
disconnectAndReconnect();
}
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();
Meteor.reconnect();
timeoutHandle = Meteor.setTimeout(failTest, 1000);
disconnectAndReconnect();
timeoutHandle = Meteor.setTimeout(failTest, 5000);
}
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 +105,7 @@ if (Meteor.isClient) {
// Addresses: https://github.com/meteor/meteor/issues/9140
Tinytest.addAsync(
'accounts - verify single onReconnect callback',
function (test, done) {
(test, done) => {
loginAsUser1((err) => {
test.isUndefined(err, 'Unexpected error logging in as user1');
test.equal(

File diff suppressed because it is too large Load Diff

View File

@@ -7,21 +7,20 @@ Meteor.methods({
// XXX it'd be cool to also test that the right thing happens if options
// *are* validated, but Accounts._options is global state which makes this hard
// (impossible?)
Tinytest.add('accounts - config validates keys', function (test) {
test.throws(function () {
Accounts.config({foo: "bar"});
});
});
Tinytest.add(
'accounts - config validates keys',
test => test.throws(() => Accounts.config({foo: "bar"}))
);
Tinytest.add('accounts - config - token lifetime', function (test) {
const loginExpirationInDays = Accounts._options.loginExpirationInDays;
Tinytest.add('accounts - config - token lifetime', test => {
const { loginExpirationInDays } = Accounts._options;
Accounts._options.loginExpirationInDays = 2;
test.equal(Accounts._getTokenLifetimeMs(), 2 * 24 * 60 * 60 * 1000);
Accounts._options.loginExpirationInDays = loginExpirationInDays;
});
Tinytest.add('accounts - config - unexpiring tokens', function (test) {
const loginExpirationInDays = Accounts._options.loginExpirationInDays;
Tinytest.add('accounts - config - unexpiring tokens', test => {
const { loginExpirationInDays } = Accounts._options;
// When setting loginExpirationInDays to null in the global Accounts
// config object, make sure the returned token lifetime represents an
@@ -48,7 +47,7 @@ Tinytest.add('accounts - config - unexpiring tokens', function (test) {
Accounts._options.loginExpirationInDays = loginExpirationInDays;
});
Tinytest.add('accounts - config - default token lifetime', function (test) {
Tinytest.add('accounts - config - default token lifetime', test => {
const options = Accounts._options;
Accounts._options = {};
test.equal(
@@ -58,55 +57,55 @@ Tinytest.add('accounts - config - default token lifetime', function (test) {
Accounts._options = options;
});
var idsInValidateNewUser = {};
Accounts.validateNewUser(function (user) {
const idsInValidateNewUser = {};
Accounts.validateNewUser(user => {
idsInValidateNewUser[user._id] = true;
return true;
});
Tinytest.add('accounts - validateNewUser gets passed user with _id', function (test) {
var newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId;
Tinytest.add('accounts - validateNewUser gets passed user with _id', test => {
const newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId;
test.isTrue(newUserId in idsInValidateNewUser);
});
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', function (test) {
var facebookId = Random.id();
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', test => {
const facebookId = Random.id();
// create an account with facebook
var uid1 = Accounts.updateOrCreateUserFromExternalService(
const uid1 = Accounts.updateOrCreateUserFromExternalService(
'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id;
var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users, 1);
test.equal(users[0].profile.foo, 1);
test.equal(users[0].services.facebook.monkey, 42);
const users1 = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users1, 1);
test.equal(users1[0].profile.foo, 1);
test.equal(users1[0].services.facebook.monkey, 42);
// create again with the same id, see that we get the same user.
// it should update services.facebook but not profile.
var uid2 = Accounts.updateOrCreateUserFromExternalService(
const uid2 = Accounts.updateOrCreateUserFromExternalService(
'facebook', {id: facebookId, llama: 50},
{profile: {foo: 1000, bar: 2}}).id;
test.equal(uid1, uid2);
users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users, 1);
test.equal(users[0].profile.foo, 1);
test.equal(users[0].profile.bar, undefined);
test.equal(users[0].services.facebook.llama, 50);
const users2 = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users2, 1);
test.equal(users2[0].profile.foo, 1);
test.equal(users2[0].profile.bar, undefined);
test.equal(users2[0].services.facebook.llama, 50);
// make sure we *don't* lose values not passed this call to
// updateOrCreateUserFromExternalService
test.equal(users[0].services.facebook.monkey, 42);
test.equal(users2[0].services.facebook.monkey, 42);
// cleanup
Meteor.users.remove(uid1);
});
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', function (test) {
var weiboId1 = Random.id();
var weiboId2 = Random.id();
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', test => {
const weiboId1 = Random.id();
const weiboId2 = Random.id();
// users that have different service ids get different users
var uid1 = Accounts.updateOrCreateUserFromExternalService(
const uid1 = Accounts.updateOrCreateUserFromExternalService(
'weibo', {id: weiboId1}, {profile: {foo: 1}}).id;
var uid2 = Accounts.updateOrCreateUserFromExternalService(
const uid2 = Accounts.updateOrCreateUserFromExternalService(
'weibo', {id: weiboId2}, {profile: {bar: 2}}).id;
test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2);
test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).profile.foo, 1);
@@ -119,75 +118,73 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', functio
Meteor.users.remove(uid2);
});
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', function (test) {
var twitterIdOld = parseInt(Random.hexString(4), 16);
var twitterIdNew = ''+twitterIdOld;
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', test => {
const twitterIdOld = parseInt(Random.hexString(4), 16);
const twitterIdNew = ''+twitterIdOld;
// create an account with twitter using the old ID format of integer
var uid1 = Accounts.updateOrCreateUserFromExternalService(
const uid1 = Accounts.updateOrCreateUserFromExternalService(
'twitter', {id: twitterIdOld, monkey: 42}, {profile: {foo: 1}}).id;
var users = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch();
test.length(users, 1);
test.equal(users[0].profile.foo, 1);
test.equal(users[0].services.twitter.monkey, 42);
const users1 = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch();
test.length(users1, 1);
test.equal(users1[0].profile.foo, 1);
test.equal(users1[0].services.twitter.monkey, 42);
// Update the account with the new ID format of string
// test that the existing user is found, and that the ID
// gets updated to a string value
var uid2 = Accounts.updateOrCreateUserFromExternalService(
const uid2 = Accounts.updateOrCreateUserFromExternalService(
'twitter', {id: twitterIdNew, monkey: 42}, {profile: {foo: 1}}).id;
test.equal(uid1, uid2);
users = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch();
test.length(users, 1);
const users2 = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch();
test.length(users2, 1);
// cleanup
Meteor.users.remove(uid1);
});
Tinytest.add('accounts - insertUserDoc username', function (test) {
var userIn = {
Tinytest.add('accounts - insertUserDoc username', test => {
const userIn = {
username: Random.id()
};
// user does not already exist. create a user object with fields set.
var userId = Accounts.insertUserDoc(
const userId = Accounts.insertUserDoc(
{profile: {name: 'Foo Bar'}},
userIn
);
var userOut = Meteor.users.findOne(userId);
const userOut = Meteor.users.findOne(userId);
test.equal(typeof userOut.createdAt, 'object');
test.equal(userOut.profile.name, 'Foo Bar');
test.equal(userOut.username, userIn.username);
// run the hook again. now the user exists, so it throws an error.
test.throws(function () {
Accounts.insertUserDoc(
{profile: {name: 'Foo Bar'}},
userIn
);
}, 'Username already exists.');
test.throws(
() => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn),
'Username already exists.'
);
// cleanup
Meteor.users.remove(userId);
});
Tinytest.add('accounts - insertUserDoc email', function (test) {
var email1 = Random.id();
var email2 = Random.id();
var email3 = Random.id();
var userIn = {
Tinytest.add('accounts - insertUserDoc email', test => {
const email1 = Random.id();
const email2 = Random.id();
const email3 = Random.id();
const userIn = {
emails: [{address: email1, verified: false},
{address: email2, verified: true}]
};
// user does not already exist. create a user object with fields set.
var userId = Accounts.insertUserDoc(
const userId = Accounts.insertUserDoc(
{profile: {name: 'Foo Bar'}},
userIn
);
var userOut = Meteor.users.findOne(userId);
const userOut = Meteor.users.findOne(userId);
test.equal(typeof userOut.createdAt, 'object');
test.equal(userOut.profile.name, 'Foo Bar');
@@ -195,32 +192,28 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
// run the hook again with the exact same emails.
// run the hook again. now the user exists, so it throws an error.
test.throws(function () {
Accounts.insertUserDoc(
{profile: {name: 'Foo Bar'}},
userIn
);
}, 'Email already exists.');
test.throws(
() => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn),
'Email already exists.'
);
// now with only one of them.
test.throws(function () {
Accounts.insertUserDoc(
{}, {emails: [{address: email1}]}
);
}, 'Email already exists.');
test.throws(() =>
Accounts.insertUserDoc({}, {emails: [{address: email1}]}),
'Email already exists.'
);
test.throws(function () {
Accounts.insertUserDoc(
{}, {emails: [{address: email2}]}
);
}, 'Email already exists.');
test.throws(() =>
Accounts.insertUserDoc({}, {emails: [{address: email2}]}),
'Email already exists.'
);
// a third email works.
var userId3 = Accounts.insertUserDoc(
const userId3 = Accounts.insertUserDoc(
{}, {emails: [{address: email3}]}
);
var user3 = Meteor.users.findOne(userId3);
const user3 = Meteor.users.findOne(userId3);
test.equal(typeof user3.createdAt, 'object');
// cleanup
@@ -229,12 +222,12 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
});
// More token expiration tests are in accounts-password
Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) {
var userIn = { username: Random.id() };
var userId = Accounts.insertUserDoc({ profile: {
Tinytest.addAsync('accounts - expire numeric token', (test, onComplete) => {
const userIn = { username: Random.id() };
const userId = Accounts.insertUserDoc({ profile: {
name: 'Foo Bar'
} }, userIn);
var date = new Date(new Date() - 5000);
const date = new Date(new Date() - 5000);
Meteor.users.update(userId, {
$set: {
"services.resume.loginTokens": [{
@@ -246,10 +239,11 @@ Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete)
}]
}
});
var observe = Meteor.users.find(userId).observe({
changed: function (newUser) {
const observe = Meteor.users.find(userId).observe({
changed: newUser => {
if (newUser.services && newUser.services.resume &&
_.isEmpty(newUser.services.resume.loginTokens)) {
(!newUser.services.resume.loginTokens ||
newUser.services.resume.loginTokens.length === 0)) {
observe.stop();
onComplete();
}
@@ -261,45 +255,43 @@ Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete)
// Login tokens used to be stored unhashed in the database. We want
// to make sure users can still login after upgrading.
var insertUnhashedLoginToken = function (userId, stampedToken) {
const insertUnhashedLoginToken = (userId, stampedToken) => {
Meteor.users.update(
userId,
{$push: {'services.resume.loginTokens': stampedToken}}
);
};
Tinytest.addAsync('accounts - login token', function (test, onComplete) {
Tinytest.addAsync('accounts - login token', (test, onComplete) => {
// Test that we can login when the database contains a leftover
// old style unhashed login token.
var userId1 = Accounts.insertUserDoc({}, {username: Random.id()});
var stampedToken = Accounts._generateStampedLoginToken();
insertUnhashedLoginToken(userId1, stampedToken);
var connection = DDP.connect(Meteor.absoluteUrl());
connection.call('login', {resume: stampedToken.token});
const userId1 = Accounts.insertUserDoc({}, {username: Random.id()});
const stampedToken1 = Accounts._generateStampedLoginToken();
insertUnhashedLoginToken(userId1, stampedToken1);
let connection = DDP.connect(Meteor.absoluteUrl());
connection.call('login', {resume: stampedToken1.token});
connection.disconnect();
// Steal the unhashed token from the database and use it to login.
// This is a sanity check so that when we *can't* login with a
// stolen *hashed* token, we know it's not a problem with the test.
var userId2 = Accounts.insertUserDoc({}, {username: Random.id()});
const userId2 = Accounts.insertUserDoc({}, {username: Random.id()});
insertUnhashedLoginToken(userId2, Accounts._generateStampedLoginToken());
var stolenToken = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token;
test.isTrue(stolenToken);
const stolenToken1 = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token;
test.isTrue(stolenToken1);
connection = DDP.connect(Meteor.absoluteUrl());
connection.call('login', {resume: stolenToken});
connection.call('login', {resume: stolenToken1});
connection.disconnect();
// Now do the same thing, this time with a stolen hashed token.
var userId3 = Accounts.insertUserDoc({}, {username: Random.id()});
const userId3 = Accounts.insertUserDoc({}, {username: Random.id()});
Accounts._insertLoginToken(userId3, Accounts._generateStampedLoginToken());
stolenToken = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken;
test.isTrue(stolenToken);
const stolenToken2 = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken;
test.isTrue(stolenToken2);
connection = DDP.connect(Meteor.absoluteUrl());
// evil plan foiled
test.throws(
function () {
connection.call('login', {resume: stolenToken});
},
() => connection.call('login', {resume: stolenToken2}),
/You\'ve been logged out by the server/
);
connection.disconnect();
@@ -307,21 +299,21 @@ Tinytest.addAsync('accounts - login token', function (test, onComplete) {
// Old style unhashed tokens are replaced by hashed tokens when
// encountered. This means that after someone logins once, the
// old unhashed token is no longer available to be stolen.
var userId4 = Accounts.insertUserDoc({}, {username: Random.id()});
var stampedToken = Accounts._generateStampedLoginToken();
insertUnhashedLoginToken(userId4, stampedToken);
const userId4 = Accounts.insertUserDoc({}, {username: Random.id()});
const stampedToken2 = Accounts._generateStampedLoginToken();
insertUnhashedLoginToken(userId4, stampedToken2);
connection = DDP.connect(Meteor.absoluteUrl());
connection.call('login', {resume: stampedToken.token});
connection.call('login', {resume: stampedToken2.token});
connection.disconnect();
// The token is no longer available to be stolen.
stolenToken = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token;
test.isFalse(stolenToken);
const stolenToken3 = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token;
test.isFalse(stolenToken3);
// After the upgrade, the client can still login with their original
// unhashed login token.
connection = DDP.connect(Meteor.absoluteUrl());
connection.call('login', {resume: stampedToken.token});
connection.call('login', {resume: stampedToken2.token});
connection.disconnect();
onComplete();
@@ -329,13 +321,13 @@ Tinytest.addAsync('accounts - login token', function (test, onComplete) {
Tinytest.addAsync(
'accounts - connection data cleaned up',
function (test, onComplete) {
(test, onComplete) => {
makeTestConnection(
test,
function (clientConn, serverConn) {
(clientConn, serverConn) => {
// onClose callbacks are called in order, so we run after the
// close callback in accounts.
serverConn.onClose(function () {
serverConn.onClose(() => {
test.isFalse(Accounts._getAccountData(serverConn.id, 'connection'));
onComplete();
});
@@ -348,20 +340,18 @@ Tinytest.addAsync(
}
);
Tinytest.add(
'accounts - get new token',
function (test) {
Tinytest.add('accounts - get new token', test => {
// Test that the `getNewToken` method returns us a valid token, with
// the same expiration as our original token.
var userId = Accounts.insertUserDoc({}, { username: Random.id() });
var stampedToken = Accounts._generateStampedLoginToken();
const userId = Accounts.insertUserDoc({}, { username: Random.id() });
const stampedToken = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(userId, stampedToken);
var conn = DDP.connect(Meteor.absoluteUrl());
const conn = DDP.connect(Meteor.absoluteUrl());
conn.call('login', { resume: stampedToken.token });
test.equal(conn.call('getCurrentLoginToken'),
Accounts._hashLoginToken(stampedToken.token));
var newTokenResult = conn.call('getNewToken');
const newTokenResult = conn.call('getNewToken');
test.equal(newTokenResult.tokenExpires,
Accounts._tokenExpiration(stampedToken.when));
test.equal(conn.call('getCurrentLoginToken'),
@@ -370,48 +360,41 @@ Tinytest.add(
// A second connection should be able to log in with the new token
// we got.
var secondConn = DDP.connect(Meteor.absoluteUrl());
const secondConn = DDP.connect(Meteor.absoluteUrl());
secondConn.call('login', { resume: newTokenResult.token });
secondConn.disconnect();
}
);
Tinytest.addAsync(
'accounts - remove other tokens',
function (test, onComplete) {
Tinytest.addAsync('accounts - remove other tokens', (test, onComplete) => {
// Test that the `removeOtherTokens` method removes all tokens other
// than the caller's token, thereby logging out and closing other
// connections.
var userId = Accounts.insertUserDoc({}, { username: Random.id() });
var stampedTokens = [];
var conns = [];
const userId = Accounts.insertUserDoc({}, { username: Random.id() });
const stampedTokens = [];
const conns = [];
_.times(2, function (i) {
for(let i = 0; i < 2; i++) {
stampedTokens.push(Accounts._generateStampedLoginToken());
Accounts._insertLoginToken(userId, stampedTokens[i]);
var conn = DDP.connect(Meteor.absoluteUrl());
const conn = DDP.connect(Meteor.absoluteUrl());
conn.call('login', { resume: stampedTokens[i].token });
test.equal(conn.call('getCurrentLoginToken'),
Accounts._hashLoginToken(stampedTokens[i].token));
conns.push(conn);
});
};
conns[0].call('removeOtherTokens');
simplePoll(
function () {
var tokens = _.map(conns, function (conn) {
return conn.call('getCurrentLoginToken');
});
simplePoll(() => {
const tokens = conns.map(conn => conn.call('getCurrentLoginToken'));
return ! tokens[1] &&
tokens[0] === Accounts._hashLoginToken(stampedTokens[0].token);
},
function () { // success
_.each(conns, function (conn) {
conn.disconnect();
});
() => { // success
conns.forEach(conn => conn.disconnect());
onComplete();
},
function () { // timed out
() => { // timed out
throw new Error("accounts - remove other tokens timed out");
}
);
@@ -420,31 +403,31 @@ Tinytest.addAsync(
Tinytest.add(
'accounts - hook callbacks can access Meteor.userId()',
function (test) {
var userId = Accounts.insertUserDoc({}, { username: Random.id() });
var stampedToken = Accounts._generateStampedLoginToken();
test => {
const userId = Accounts.insertUserDoc({}, { username: Random.id() });
const stampedToken = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(userId, stampedToken);
var validateStopper = Accounts.validateLoginAttempt(function(attempt) {
const validateStopper = Accounts.validateLoginAttempt(attempt => {
test.equal(Meteor.userId(), validateAttemptExpectedUserId, "validateLoginAttempt");
return true;
});
var onLoginStopper = Accounts.onLogin(function(attempt) {
test.equal(Meteor.userId(), onLoginExpectedUserId, "onLogin");
});
var onLogoutStopper = Accounts.onLogout(function(logoutContext) {
const onLoginStopper = Accounts.onLogin(attempt =>
test.equal(Meteor.userId(), onLoginExpectedUserId, "onLogin")
);
const onLogoutStopper = Accounts.onLogout(logoutContext => {
test.equal(logoutContext.user._id, onLogoutExpectedUserId, "onLogout");
test.instanceOf(logoutContext.connection, Object);
});
var onLoginFailureStopper = Accounts.onLoginFailure(function(attempt) {
test.equal(Meteor.userId(), onLoginFailureExpectedUserId, "onLoginFailure");
});
const onLoginFailureStopper = Accounts.onLoginFailure(attempt =>
test.equal(Meteor.userId(), onLoginFailureExpectedUserId, "onLoginFailure")
);
var conn = DDP.connect(Meteor.absoluteUrl());
const conn = DDP.connect(Meteor.absoluteUrl());
// On a new connection, Meteor.userId() should be null until logged in.
var validateAttemptExpectedUserId = null;
var onLoginExpectedUserId = userId;
let validateAttemptExpectedUserId = null;
const onLoginExpectedUserId = userId;
conn.call('login', { resume: stampedToken.token });
// Now that the user is logged in on the connection, Meteor.userId() should
@@ -453,11 +436,11 @@ Tinytest.add(
conn.call('login', { resume: stampedToken.token });
// Trigger onLoginFailure callbacks
var onLoginFailureExpectedUserId = userId;
test.throws(function() { conn.call('login', { resume: "bogus" }) }, '403');
const onLoginFailureExpectedUserId = userId;
test.throws(() => conn.call('login', { resume: "bogus" }), '403');
// Trigger onLogout callbacks
var onLogoutExpectedUserId = userId;
const onLogoutExpectedUserId = userId;
conn.call('logout');
conn.disconnect();
@@ -470,7 +453,7 @@ Tinytest.add(
Tinytest.add(
'accounts - verify onExternalLogin hook can update oauth user profiles',
function (test) {
test => {
// Verify user profile data is saved properly when not using the
// onExternalLogin hook.
let facebookId = Random.id();

View File

@@ -1,17 +1,16 @@
import {AccountsTest} from "meteor/accounts-base";
import { AccountsTest } from "./accounts_client.js";
Tinytest.add("accounts - parse urls for accounts-password",
function (test) {
var actions = ["reset-password", "verify-email", "enroll-account"];
Tinytest.add("accounts - parse urls for accounts-password", test => {
const actions = ["reset-password", "verify-email", "enroll-account"];
// make sure the callback was called the right number of times
var actionsParsed = [];
const actionsParsed = [];
_.each(actions, function (hashPart) {
var fakeToken = "asdf";
actions.forEach(hashPart => {
const fakeToken = "asdf";
var hashTokenOnly = "#/" + hashPart + "/" + fakeToken;
AccountsTest.attemptToMatchHash(hashTokenOnly, function (token, action) {
const hashTokenOnly = `#/${hashPart}/${fakeToken}`;
AccountsTest.attemptToMatchHash(hashTokenOnly, (token, action) => {
test.equal(token, fakeToken);
test.equal(action, hashPart);

View File

@@ -1,6 +1,7 @@
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
@@ -17,10 +18,11 @@ Accounts = new AccountsClient();
Meteor.users = Accounts.users;
export {
// 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.
Accounts,
AccountsClient,
AccountsTest,
// For backwards compatibility. Note that exporting an object as the
// default export is *not* the same as exporting its properties as named
// exports, as was previously assumed.
exports as default,
};

View File

@@ -1,185 +0,0 @@
import {AccountsClient} from "./accounts_client.js";
var Ap = AccountsClient.prototype;
// This file deals with storing a login token and user id in the
// browser's localStorage facility. It polls local storage every few
// seconds to synchronize login state between multiple tabs in the same
// browser.
// Login with a Meteor access token. This is the only public function
// here.
Meteor.loginWithToken = function (token, callback) {
return Accounts.loginWithToken(token, callback);
};
Ap.loginWithToken = function (token, callback) {
this.callLoginMethod({
methodArguments: [{
resume: token
}],
userCallback: callback
});
};
// Semi-internal API. Call this function to re-enable auto login after
// if it was disabled at startup.
Ap._enableAutoLogin = function () {
this._autoLoginEnabled = true;
this._pollStoredLoginToken();
};
///
/// STORING
///
// Call this from the top level of the test file for any test that does
// logging in and out, to protect multiple tabs running the same tests
// simultaneously from interfering with each others' localStorage.
Ap._isolateLoginTokenForTest = function () {
this.LOGIN_TOKEN_KEY = this.LOGIN_TOKEN_KEY + Random.id();
this.USER_ID_KEY = this.USER_ID_KEY + Random.id();
};
Ap._storeLoginToken = function (userId, token, tokenExpires) {
Meteor._localStorage.setItem(this.USER_ID_KEY, userId);
Meteor._localStorage.setItem(this.LOGIN_TOKEN_KEY, token);
if (! tokenExpires)
tokenExpires = this._tokenExpiration(new Date());
Meteor._localStorage.setItem(this.LOGIN_TOKEN_EXPIRES_KEY, tokenExpires);
// to ensure that the localstorage poller doesn't end up trying to
// connect a second time
this._lastLoginTokenWhenPolled = token;
};
Ap._unstoreLoginToken = function () {
Meteor._localStorage.removeItem(this.USER_ID_KEY);
Meteor._localStorage.removeItem(this.LOGIN_TOKEN_KEY);
Meteor._localStorage.removeItem(this.LOGIN_TOKEN_EXPIRES_KEY);
// to ensure that the localstorage poller doesn't end up trying to
// connect a second time
this._lastLoginTokenWhenPolled = null;
};
// This is private, but it is exported for now because it is used by a
// test in accounts-password.
//
Ap._storedLoginToken = function () {
return Meteor._localStorage.getItem(this.LOGIN_TOKEN_KEY);
};
Ap._storedLoginTokenExpires = function () {
return Meteor._localStorage.getItem(this.LOGIN_TOKEN_EXPIRES_KEY);
};
Ap._storedUserId = function () {
return Meteor._localStorage.getItem(this.USER_ID_KEY);
};
Ap._unstoreLoginTokenIfExpiresSoon = function () {
var tokenExpires = this._storedLoginTokenExpires();
if (tokenExpires && this._tokenExpiresSoon(new Date(tokenExpires))) {
this._unstoreLoginToken();
}
};
///
/// AUTO-LOGIN
///
Ap._initLocalStorage = function () {
var self = this;
// Key names to use in localStorage
self.LOGIN_TOKEN_KEY = "Meteor.loginToken";
self.LOGIN_TOKEN_EXPIRES_KEY = "Meteor.loginTokenExpires";
self.USER_ID_KEY = "Meteor.userId";
var rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
if (rootUrlPathPrefix || this.connection !== Meteor.connection) {
// We want to keep using the same keys for existing apps that do not
// set a custom ROOT_URL_PATH_PREFIX, so that most users will not have
// to log in again after an app updates to a version of Meteor that
// contains this code, but it's generally preferable to namespace the
// keys so that connections from distinct apps to distinct DDP URLs
// will be distinct in Meteor._localStorage.
var namespace = ":" + this.connection._stream.rawUrl;
if (rootUrlPathPrefix) {
namespace += ":" + rootUrlPathPrefix;
}
self.LOGIN_TOKEN_KEY += namespace;
self.LOGIN_TOKEN_EXPIRES_KEY += namespace;
self.USER_ID_KEY += namespace;
}
if (self._autoLoginEnabled) {
// Immediately try to log in via local storage, so that any DDP
// messages are sent after we have established our user account
self._unstoreLoginTokenIfExpiresSoon();
var token = self._storedLoginToken();
if (token) {
// On startup, optimistically present us as logged in while the
// request is in flight. This reduces page flicker on startup.
var userId = self._storedUserId();
userId && self.connection.setUserId(userId);
self.loginWithToken(token, function (err) {
if (err) {
Meteor._debug("Error logging in with token: " + err);
self.makeClientLoggedOut();
}
self._pageLoadLogin({
type: "resume",
allowed: !err,
error: err,
methodName: "login",
// XXX This is duplicate code with loginWithToken, but
// loginWithToken can also be called at other times besides
// page load.
methodArguments: [{resume: token}]
});
});
}
}
// Poll local storage every 3 seconds to login if someone logged in in
// another tab
self._lastLoginTokenWhenPolled = token;
if (self._pollIntervalTimer) {
// Unlikely that _initLocalStorage will be called more than once for
// the same AccountsClient instance, but just in case...
clearInterval(self._pollIntervalTimer);
}
self._pollIntervalTimer = setInterval(function () {
self._pollStoredLoginToken();
}, 3000);
};
Ap._pollStoredLoginToken = function () {
var self = this;
if (! self._autoLoginEnabled) {
return;
}
var currentLoginToken = self._storedLoginToken();
// != instead of !== just to make sure undefined and null are treated the same
if (self._lastLoginTokenWhenPolled != currentLoginToken) {
if (currentLoginToken) {
self.loginWithToken(currentLoginToken, function (err) {
if (err) {
self.makeClientLoggedOut();
}
});
} else {
self.logout();
}
}
self._lastLoginTokenWhenPolled = currentLoginToken;
};

View File

@@ -1,10 +1,9 @@
Package.describe({
summary: "A user account system",
version: "1.4.0"
version: "1.4.3",
});
Package.onUse(function (api) {
api.use('underscore', ['client', 'server']);
Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use('ddp-rate-limiter');
api.use('localstorage', 'client');
@@ -50,7 +49,7 @@ Package.onUse(function (api) {
api.mainModule('client_main.js', 'client');
});
Package.onTest(function (api) {
Package.onTest(api => {
api.use([
'accounts-base',
'ecmascript',
@@ -58,7 +57,6 @@ Package.onTest(function (api) {
'random',
'test-helpers',
'oauth-encryption',
'underscore',
'ddp',
'accounts-password'
]);

View File

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

View File

@@ -1,169 +0,0 @@
import {AccountsClient} from "./accounts_client.js";
var Ap = AccountsClient.prototype;
// All of the special hash URLs we support for accounts interactions
var accountsPaths = ["reset-password", "verify-email", "enroll-account"];
var savedHash = window.location.hash;
Ap._initUrlMatching = function () {
// By default, allow the autologin process to happen.
this._autoLoginEnabled = true;
// We only support one callback per URL.
this._accountsCallbacks = {};
// Try to match the saved value of window.location.hash.
this._attemptToMatchHash();
};
// Separate out this functionality for testing
Ap._attemptToMatchHash = function () {
attemptToMatchHash(this, savedHash, defaultSuccessHandler);
};
// Note that both arguments are optional and are currently only passed by
// accounts_url_tests.js.
function attemptToMatchHash(accounts, hash, success) {
_.each(accountsPaths, function (urlPart) {
var token;
var tokenRegex = new RegExp("^\\#\\/" + urlPart + "\\/(.*)$");
var match = hash.match(tokenRegex);
if (match) {
token = match[1];
// XXX COMPAT WITH 0.9.3
if (urlPart === "reset-password") {
accounts._resetPasswordToken = token;
} else if (urlPart === "verify-email") {
accounts._verifyEmailToken = token;
} else if (urlPart === "enroll-account") {
accounts._enrollAccountToken = token;
}
} else {
return;
}
// If no handlers match the hash, then maybe it's meant to be consumed
// by some entirely different code, so we only clear it the first time
// a handler successfully matches. Note that later handlers reuse the
// savedHash, so clearing window.location.hash here will not interfere
// with their needs.
window.location.hash = "";
// Do some stuff with the token we matched
success.call(accounts, token, urlPart);
});
}
function defaultSuccessHandler(token, urlPart) {
var self = this;
// put login in a suspended state to wait for the interaction to finish
self._autoLoginEnabled = false;
// wait for other packages to register callbacks
Meteor.startup(function () {
// if a callback has been registered for this kind of token, call it
if (self._accountsCallbacks[urlPart]) {
self._accountsCallbacks[urlPart](token, function () {
self._enableAutoLogin();
});
}
});
}
// Export for testing
export var AccountsTest = {
attemptToMatchHash: function (hash, success) {
return attemptToMatchHash(Accounts, hash, success);
}
};
// XXX these should be moved to accounts-password eventually. Right now
// this is prevented by the need to set autoLoginEnabled=false, but in
// some bright future we won't need to do that anymore.
/**
* @summary Register a function to call when a reset password link is clicked
* in an email sent by
* [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
* @name onResetPasswordLink
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
* [`Accounts.resetPassword`](#accounts_resetpassword).
* 2. `done`: A function to call when the password reset UI flow is complete. The normal
* login process is suspended until this function is called, so that the
* password for user A can be reset even if user B was logged in.
* @locus Client
*/
Ap.onResetPasswordLink = function (callback) {
if (this._accountsCallbacks["reset-password"]) {
Meteor._debug("Accounts.onResetPasswordLink was called more than once. " +
"Only one callback added will be executed.");
}
this._accountsCallbacks["reset-password"] = callback;
};
/**
* @summary Register a function to call when an email verification link is
* clicked in an email sent by
* [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
* @name onEmailVerificationLink
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: An email verification token that can be passed to
* [`Accounts.verifyEmail`](#accounts_verifyemail).
* 2. `done`: A function to call when the email verification UI flow is complete.
* The normal login process is suspended until this function is called, so
* that the user can be notified that they are verifying their email before
* being logged in.
* @locus Client
*/
Ap.onEmailVerificationLink = function (callback) {
if (this._accountsCallbacks["verify-email"]) {
Meteor._debug("Accounts.onEmailVerificationLink was called more than once. " +
"Only one callback added will be executed.");
}
this._accountsCallbacks["verify-email"] = callback;
};
/**
* @summary Register a function to call when an account enrollment link is
* clicked in an email sent by
* [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
* @name onEnrollmentLink
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
* [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly
* enrolled account a password.
* 2. `done`: A function to call when the enrollment UI flow is complete.
* The normal login process is suspended until this function is called, so that
* user A can be enrolled even if user B was logged in.
* @locus Client
*/
Ap.onEnrollmentLink = function (callback) {
if (this._accountsCallbacks["enroll-account"]) {
Meteor._debug("Accounts.onEnrollmentLink was called more than once. " +
"Only one callback added will be executed.");
}
this._accountsCallbacks["enroll-account"] = callback;
};

View File

@@ -1,17 +0,0 @@
import {AccountsServer} from "./accounts_server.js";
// XXX These should probably not actually be public?
AccountsServer.prototype.urls = {
resetPassword: function (token) {
return Meteor.absoluteUrl('#/reset-password/' + token);
},
verifyEmail: function (token) {
return Meteor.absoluteUrl('#/verify-email/' + token);
},
enrollAccount: function (token) {
return Meteor.absoluteUrl('#/enroll-account/' + token);
}
};

View File

@@ -1,20 +1,19 @@
Accounts.oauth.registerService('facebook');
if (Meteor.isClient) {
const loginWithFacebook = function(options, callback) {
const loginWithFacebook = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Facebook.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('facebook', loginWithFacebook);
Meteor.loginWithFacebook = function () {
return Accounts.applyLoginFunction('facebook', arguments);
};
Meteor.loginWithFacebook =
(...args) => Accounts.applyLoginFunction('facebook', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately

View File

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

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: "Login service for Facebook accounts",
version: "1.3.0"
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.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,20 +1,19 @@
Accounts.oauth.registerService('github');
if (Meteor.isClient) {
const loginWithGithub = function(options, callback) {
const loginWithGithub = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Github.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('github', loginWithGithub);
Meteor.loginWithGithub = function () {
return Accounts.applyLoginFunction('github', arguments);
};
Meteor.loginWithGithub =
(...args) => Accounts.applyLoginFunction('github', args);
} else {
Accounts.addAutopublishFields({
// not sure whether the github api can be used from the browser,

View File

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

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: 'Login service for Github accounts',
version: '1.4.0'
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.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,7 +1,7 @@
Accounts.oauth.registerService('google');
if (Meteor.isClient) {
const loginWithGoogle = function(options, callback) {
const loginWithGoogle = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
@@ -23,31 +23,34 @@ if (Meteor.isClient) {
// accounts-base/accounts_server.js still checks server-side that the server
// has the proper email address after the OAuth conversation.
if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') {
options = _.extend({}, options || {});
options.loginUrlParameters = _.extend({}, options.loginUrlParameters || {});
options = { ...options };
options.loginUrlParameters = { ...options.loginUrlParameters };
options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Google.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('google', loginWithGoogle);
Meteor.loginWithGoogle = function () {
return Accounts.applyLoginFunction('google', arguments);
};
Meteor.loginWithGoogle =
(...args) => Accounts.applyLoginFunction('google', args);
} else {
Accounts.addAutopublishFields({
forLoggedInUser: _.map(
forLoggedInUser:
// publish access token since it can be used from the client (if
// transmitted over ssl or on
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
// refresh token probably shouldn't be sent down.
Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token
function (subfield) { return 'services.google.' + subfield; }),
Google.whitelistedFields.concat(['accessToken', 'expiresAt']).map(
subfield => `services.google.${subfield}` // don't publish refresh token
),
forOtherUsers: _.map(
forOtherUsers:
// even with autopublish, no legitimate web app should be
// publishing all users' emails
_.without(Google.whitelistedFields, 'email', 'verified_email'),
function (subfield) { return 'services.google.' + subfield; })
Google.whitelistedFields.filter(
field => field !== 'email' && field !== 'verified_email'
).map(
subfield => `services.google${subfield}`
),
});
}

View File

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

View File

@@ -1,10 +1,10 @@
Package.describe({
summary: "Login service for Google accounts",
version: "1.3.0"
version: "1.3.2",
});
Package.onUse(function(api) {
api.use(['underscore', 'random']);
Package.onUse(api => {
api.use(['ecmascript']);
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,20 +1,19 @@
Accounts.oauth.registerService('meetup');
if (Meteor.isClient) {
const loginWithMeetup = function(options, callback) {
const loginWithMeetup = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Meetup.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('meetup', loginWithMeetup);
Meteor.loginWithMeetup = function () {
return Accounts.applyLoginFunction('meetup', arguments);
};
Meteor.loginWithMeetup =
(...args) => Accounts.applyLoginFunction('meetup', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately

View File

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

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: 'Login service for Meetup accounts',
version: '1.4.0'
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.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,21 +1,20 @@
Accounts.oauth.registerService("meteor-developer");
if (Meteor.isClient) {
const loginWithMeteorDeveloperAccount = function (options, callback) {
const loginWithMeteorDeveloperAccount = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback =
const credentialRequestCompleteCallback =
Accounts.oauth.credentialRequestCompleteHandler(callback);
MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('meteor-developer', loginWithMeteorDeveloperAccount);
Meteor.loginWithMeteorDeveloperAccount = function () {
return Accounts.applyLoginFunction('meteor-developer', arguments);
};
Meteor.loginWithMeteorDeveloperAccount = (...args) =>
Accounts.applyLoginFunction('meteor-developer', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately be used

View File

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

View File

@@ -1,10 +1,10 @@
Package.describe({
summary: 'Login service for Meteor developer accounts',
version: '1.4.0'
version: '1.4.2',
});
Package.onUse(function (api) {
api.use(['underscore', 'random']);
Package.onUse(api => {
api.use(['ecmascript']);
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -19,7 +19,7 @@
// Allow server to specify a specify subclass of errors. We should come
// up with a more generic way to do this!
var convertError = function (err) {
const convertError = err => {
if (err && err instanceof Meteor.Error &&
err.error === Accounts.LoginCancelledError.numericError)
return new Accounts.LoginCancelledError(err.reason);
@@ -34,8 +34,8 @@ var convertError = function (err) {
// credentialSecret for a successful login is stored in session
// storage.
Meteor.startup(function () {
var oauth = OAuth.getDataAfterRedirect();
Meteor.startup(() => {
const oauth = OAuth.getDataAfterRedirect();
if (! oauth)
return;
@@ -43,12 +43,13 @@ Meteor.startup(function () {
// successfully. However we still call the login method anyway to
// retrieve the error if the login was unsuccessful.
var methodName = 'login';
var methodArguments = [{oauth: _.pick(oauth, 'credentialToken', 'credentialSecret')}];
const methodName = 'login';
const { credentialToken, credentialSecret } = oauth;
const methodArguments = [{ oauth: { credentialToken, credentialSecret } }];
Accounts.callLoginMethod({
methodArguments: methodArguments,
userCallback: function (err) {
methodArguments,
userCallback: err => {
// The redirect login flow is complete. Construct an
// `attemptInfo` object with the login result, and report back
// to the code which initiated the login attempt
@@ -58,8 +59,8 @@ Meteor.startup(function () {
type: oauth.loginService,
allowed: !err,
error: err,
methodName: methodName,
methodArguments: methodArguments
methodName,
methodArguments,
});
}
});
@@ -69,24 +70,20 @@ Meteor.startup(function () {
// Send an OAuth login method to the server. If the user authorized
// access in the popup this should log the user in, otherwise
// nothing should happen.
Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) {
var credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null;
Accounts.oauth.tryLoginAfterPopupClosed = (credentialToken, callback) => {
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null;
Accounts.callLoginMethod({
methodArguments: [{oauth: {
credentialToken: credentialToken,
credentialSecret: credentialSecret
}}],
userCallback: callback && function (err) {
callback(convertError(err));
}});
methodArguments: [{oauth: { credentialToken, credentialSecret }}],
userCallback: callback && (err => callback(convertError(err))),
});
};
Accounts.oauth.credentialRequestCompleteHandler = function(callback) {
return function (credentialTokenOrError) {
Accounts.oauth.credentialRequestCompleteHandler = callback =>
credentialTokenOrError => {
if(credentialTokenOrError && credentialTokenOrError instanceof Error) {
callback && callback(credentialTokenOrError);
} else {
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
}
};
};
}

View File

@@ -1,12 +1,13 @@
Accounts.oauth = {};
var services = {};
const services = {};
const hasOwn = Object.prototype.hasOwnProperty;
// Helper for registering OAuth based accounts packages.
// On the server, adds an index to the user collection.
Accounts.oauth.registerService = function (name) {
if (_.has(services, name))
throw new Error("Duplicate service: " + name);
Accounts.oauth.registerService = name => {
if (hasOwn.call(services, name))
throw new Error(`Duplicate service: ${name}`);
services[name] = true;
if (Meteor.server) {
@@ -14,8 +15,7 @@ Accounts.oauth.registerService = function (name) {
// so this should be a unique index. You might want to add indexes for other
// fields returned by your service (eg services.github.login) but you can do
// that in your app.
Meteor.users._ensureIndex('services.' + name + '.id',
{unique: 1, sparse: 1});
Meteor.users._ensureIndex(`services.${name}.id`, {unique: 1, sparse: 1});
}
};
@@ -24,12 +24,10 @@ Accounts.oauth.registerService = function (name) {
// contain it.
// It's worth noting that already logged in users will remain logged in unless
// you manually expire their sessions.
Accounts.oauth.unregisterService = function (name) {
if (!_.has(services, name))
throw new Error("Service not found: " + name);
Accounts.oauth.unregisterService = name => {
if (!hasOwn.call(services, name))
throw new Error(`Service not found: ${name}`);
delete services[name];
};
Accounts.oauth.serviceNames = function () {
return _.keys(services);
};
Accounts.oauth.serviceNames = () => Object.keys(services);

View File

@@ -1,6 +1,6 @@
// Listen to calls to `login` with an oauth option set. This is where
// users actually get logged in to meteor via oauth.
Accounts.registerLoginHandler(function (options) {
Accounts.registerLoginHandler(options => {
if (!options.oauth)
return undefined; // don't handle
@@ -13,7 +13,7 @@ Accounts.registerLoginHandler(function (options) {
credentialSecret: Match.OneOf(null, String)
});
var result = OAuth.retrieveCredential(options.oauth.credentialToken,
const result = OAuth.retrieveCredential(options.oauth.credentialToken,
options.oauth.credentialSecret);
if (!result) {
@@ -42,14 +42,14 @@ Accounts.registerLoginHandler(function (options) {
// to the user.
throw result;
else {
if (!_.contains(Accounts.oauth.serviceNames(), result.serviceName)) {
if (! Accounts.oauth.serviceNames().includes(result.serviceName)) {
// serviceName was not found in the registered services list.
// This could happen because the service never registered itself or
// unregisterService was called on it.
return { type: "oauth",
error: new Meteor.Error(
Accounts.LoginCancelledError.numericError,
"No registered oauth service found for: " + result.serviceName) };
`No registered oauth service found for: ${result.serviceName}`) };
}
return Accounts.updateOrCreateUserFromExternalService(result.serviceName, result.serviceData, result.options);

View File

@@ -1,14 +1,12 @@
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']);
api.use(['accounts-base', 'ecmascript'], ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('oauth');
@@ -19,6 +17,6 @@ Package.onUse(function (api) {
});
Package.onTest(function (api) {
Package.onTest(api => {
api.addFiles("oauth_tests.js", 'server');
});

View File

@@ -1,7 +1,6 @@
function greet(welcomeMsg) {
return function(user, url) {
var greeting = (user.profile && user.profile.name) ?
("Hello " + user.profile.name + ",") : "Hello,";
const greet = welcomeMsg => (user, url) => {
const greeting = (user.profile && user.profile.name) ?
(`Hello ${user.profile.name},`) : "Hello,";
return `${greeting}
${welcomeMsg}, simply click the link below.
@@ -10,8 +9,7 @@ ${url}
Thanks.
`;
};
}
};
/**
* @summary Options to customize emails sent from the Accounts system.
@@ -23,21 +21,15 @@ Accounts.emailTemplates = {
siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''),
resetPassword: {
subject: function(user) {
return "How to reset your password on " + Accounts.emailTemplates.siteName;
},
text: greet("To reset your password")
subject: () => `How to reset your password on ${Accounts.emailTemplates.siteName}`,
text: greet("To reset your password"),
},
verifyEmail: {
subject: function(user) {
return "How to verify email address on " + Accounts.emailTemplates.siteName;
},
text: greet("To verify your account email")
subject: () => `How to verify email address on ${Accounts.emailTemplates.siteName}`,
text: greet("To verify your account email"),
},
enrollAccount: {
subject: function(user) {
return "An account has been created for you on " + Accounts.emailTemplates.siteName;
},
text: greet("To start using the service")
}
subject: () => `An account has been created for you on ${Accounts.emailTemplates.siteName}`,
text: greet("To start using the service"),
},
};

View File

@@ -1,6 +1,6 @@
var resetPasswordToken;
var verifyEmailToken;
var enrollAccountToken;
let resetPasswordToken;
let verifyEmailToken;
let enrollAccountToken;
Accounts._isolateLoginTokenForTest();
@@ -11,10 +11,10 @@ if (Meteor.isServer) {
testAsyncMulti("accounts emails - reset password flow", [
function (test, expect) {
this.randomSuffix = Random.id();
this.email = "Ada-intercept@example.com" + this.randomSuffix;
this.email = `Ada-intercept@example.com${this.randomSuffix}`;
// Create the user with another email and add the tested for email later,
// so we can test whether forgotPassword respects the passed in email
Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'},
Accounts.createUser({email: `another@example.com${this.randomSuffix}`, password: 'foobar'},
expect((error) => {
test.equal(error, undefined);
Meteor.call("addEmailForTestAndVerify", this.email);
@@ -31,10 +31,10 @@ testAsyncMulti("accounts emails - reset password flow", [
test.equal(error, undefined);
test.notEqual(result, undefined);
test.equal(result.length, 2); // the first is the email verification
var options = result[1];
const options = result[1];
var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)");
var match = options.text.match(re);
const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const match = options.text.match(re);
test.isTrue(match);
resetPasswordToken = match[1];
test.isTrue(options.html.match(re));
@@ -73,17 +73,17 @@ testAsyncMulti(`accounts emails - \
reset password flow with case insensitive email`, [
function (test, expect) {
this.randomSuffix = Random.id();
this.email = "Ada-intercept@example.com" + this.randomSuffix;
this.email = `Ada-intercept@example.com${this.randomSuffix}`;
// Create the user with another email and add the tested for email later,
// so we can test whether forgotPassword respects the passed in email
Accounts.createUser({email: "another@example.com" + this.randomSuffix, password: 'foobar'},
Accounts.createUser({email: `another@example.com${this.randomSuffix}`, password: 'foobar'},
expect((error) => {
test.equal(error, undefined);
Meteor.call("addEmailForTestAndVerify", this.email);
}));
},
function (test, expect) {
Accounts.forgotPassword({email: "ada-intercept@example.com" + this.randomSuffix}, expect((error) => {
Accounts.forgotPassword({email: `ada-intercept@example.com${this.randomSuffix}`}, expect(error => {
test.equal(error, undefined);
}));
},
@@ -93,10 +93,10 @@ reset password flow with case insensitive email`, [
test.equal(error, undefined);
test.notEqual(result, undefined);
test.equal(result.length, 2); // the first is the email verification
var options = result[1];
const options = result[1];
var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)");
var match = options.text.match(re);
const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const match = options.text.match(re);
test.isTrue(match);
resetPasswordToken = match[1];
test.isTrue(options.html.match(re));
@@ -131,16 +131,16 @@ reset password flow with case insensitive email`, [
}
]);
var getVerifyEmailToken = function (email, test, expect) {
const getVerifyEmailToken = (email, test, expect) => {
Accounts.connection.call(
"getInterceptedEmails", email, expect((error, result) => {
test.equal(error, undefined);
test.notEqual(result, undefined);
test.equal(result.length, 1);
var options = result[0];
const options = result[0];
var re = new RegExp(Meteor.absoluteUrl() + "#/verify-email/(\\S*)");
var match = options.text.match(re);
const re = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`);
const match = options.text.match(re);
test.isTrue(match);
verifyEmailToken = match[1];
test.isTrue(options.html.match(re));
@@ -150,22 +150,20 @@ var getVerifyEmailToken = function (email, test, expect) {
}));
};
var loggedIn = function (test, expect) {
return expect((error) => {
const loggedIn = (test, expect) => expect((error) => {
test.equal(error, undefined);
test.isTrue(Meteor.user());
});
};
testAsyncMulti("accounts emails - verify email flow", [
function (test, expect) {
this.email = Random.id() + "-intercept@example.com";
this.email = `${Random.id()}-intercept@example.com`;
const emailId = Random.id();
this.anotherEmail = emailId.toLowerCase() + "-intercept@example.com";
this.anotherEmail = `${emailId.toLowerCase()}-intercept@example.com`;
// Add the same email as 'anotherEmail' but in upper case in order to check if
// the verification token will be removed for the email in upperCase and in
// lowerCase.
this.anotherEmailCaps = emailId.toUpperCase() +"-INTERCEPT@example.com";
this.anotherEmailCaps = `${emailId.toUpperCase()}-INTERCEPT@example.com`;
Accounts.createUser(
{email: this.email, password: 'foobar'},
loggedIn(test, expect));
@@ -175,7 +173,7 @@ testAsyncMulti("accounts emails - verify email flow", [
test.equal(Meteor.user().emails[0].address, this.email);
test.isFalse(Meteor.user().emails[0].verified);
// We should NOT be publishing things like verification tokens!
test.isFalse(_.has(Meteor.user(), 'services'));
test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user(), 'services'));
},
function (test, expect) {
getVerifyEmailToken(this.email, test, expect);
@@ -262,32 +260,32 @@ testAsyncMulti("accounts emails - verify email flow", [
}
]);
var getEnrollAccountToken = function (email, test, expect) {
const getEnrollAccountToken = (email, test, expect) =>
Accounts.connection.call(
"getInterceptedEmails", email, expect((error, result) => {
test.equal(error, undefined);
test.notEqual(result, undefined);
test.equal(result.length, 1);
var options = result[0];
const options = result[0];
var re = new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)")
var match = options.text.match(re);
const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`)
const match = options.text.match(re);
test.isTrue(match);
enrollAccountToken = match[1];
test.isTrue(options.html.match(re));
test.equal(options.from, 'test@meteor.com');
test.equal(options.headers['My-Custom-Header'], 'Cool');
}));
};
})
);
testAsyncMulti("accounts emails - enroll account flow", [
function (test, expect) {
this.email = Random.id() + "-intercept@example.com";
this.email = `${Random.id()}-intercept@example.com`;
Accounts.connection.call("createUserOnServer", this.email,
expect((error, result) => {
test.isFalse(error);
var user = result;
const user = result;
test.equal(user.emails.length, 1);
test.equal(user.emails[0].address, this.email);
test.isFalse(user.emails[0].verified);

View File

@@ -3,30 +3,26 @@
// the string "intercept", storing them in an array that can then
// be retrieved using the getInterceptedEmails method
//
var interceptedEmails = {}; // (email address) -> (array of options)
const interceptedEmails = {}; // (email address) -> (array of options)
// add html email templates that just contain the url
Accounts.emailTemplates.resetPassword.html =
Accounts.emailTemplates.enrollAccount.html =
Accounts.emailTemplates.verifyEmail.html = function (user, url) {
return url;
};
Accounts.emailTemplates.verifyEmail.html = (user, url) => url;
// override the from address
Accounts.emailTemplates.resetPassword.from =
Accounts.emailTemplates.enrollAccount.from =
Accounts.emailTemplates.verifyEmail.from = function (user) {
return 'test@meteor.com';
};
Accounts.emailTemplates.verifyEmail.from = user => 'test@meteor.com';
// add a custom header to check against
Accounts.emailTemplates.headers = {
'My-Custom-Header' : 'Cool'
};
EmailTest.hookSend(function (options) {
var to = options.to;
if (!to || to.toUpperCase().indexOf('INTERCEPT') === -1) {
EmailTest.hookSend(options => {
const { to } = options;
if (!to || !to.toUpperCase().includes('INTERCEPT')) {
return true; // go ahead and send
} else {
if (!interceptedEmails[to])
@@ -38,22 +34,22 @@ EmailTest.hookSend(function (options) {
});
Meteor.methods({
getInterceptedEmails: function (email) {
getInterceptedEmails: email => {
check(email, String);
return interceptedEmails[email];
},
addEmailForTestAndVerify: function (email) {
addEmailForTestAndVerify: email => {
check(email, String);
Meteor.users.update(
{_id: this.userId},
{_id: Accounts.userId()},
{$push: {emails: {address: email, verified: false}}});
Accounts.sendVerificationEmail(this.userId, email);
Accounts.sendVerificationEmail(Accounts.userId(), email);
},
createUserOnServer: function (email) {
createUserOnServer: email => {
check(email, String);
var userId = Accounts.createUser({email: email});
const userId = Accounts.createUser({ email });
Accounts.sendEnrollmentEmail(userId);
return Meteor.users.findOne(userId);
}

View File

@@ -5,10 +5,10 @@ Package.describe({
// 2.2.x in the future. The version was also bumped to 2.0.0 temporarily
// during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2
// through -beta.5 and -rc.0 have already been published.
version: "1.5.0"
version: "1.5.1"
});
Package.onUse(function(api) {
Package.onUse(api => {
api.use('npm-bcrypt', 'server');
api.use([
@@ -22,10 +22,9 @@ Package.onUse(function(api) {
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('email', ['server']);
api.use('random', ['server']);
api.use('check');
api.use('underscore');
api.use('email', 'server');
api.use('random', 'server');
api.use('check', 'server');
api.use('ecmascript');
api.addFiles('email_templates.js', 'server');
@@ -33,10 +32,9 @@ Package.onUse(function(api) {
api.addFiles('password_client.js', 'client');
});
Package.onTest(function(api) {
Package.onTest(api => {
api.use(['accounts-password', 'tinytest', 'test-helpers', 'tracker',
'accounts-base', 'random', 'email', 'underscore', 'check',
'ddp', 'ecmascript']);
'accounts-base', 'random', 'email', 'check', 'ddp', 'ecmascript']);
api.addFiles('password_tests_setup.js', 'server');
api.addFiles('password_tests.js', ['client', 'server']);
api.addFiles('email_tests_setup.js', 'server');

View File

@@ -1,5 +1,5 @@
// Used in the various functions below to handle errors consistently
function reportError(error, callback) {
const reportError = (error, callback) => {
if (callback) {
callback(error);
} else {
@@ -30,9 +30,9 @@ function reportError(error, callback) {
* on failure.
* @importFromPackage meteor
*/
Meteor.loginWithPassword = function (selector, password, callback) {
Meteor.loginWithPassword = (selector, password, callback) => {
if (typeof selector === 'string')
if (selector.indexOf('@') === -1)
if (!selector.includes('@'))
selector = {username: selector};
else
selector = {email: selector};
@@ -42,7 +42,7 @@ Meteor.loginWithPassword = function (selector, password, callback) {
user: selector,
password: Accounts._hashPassword(password)
}],
userCallback: function (error, result) {
userCallback: (error, result) => {
if (error && error.error === 400 &&
error.reason === 'old password format') {
// The "reason" string should match the error thrown in the
@@ -72,12 +72,11 @@ Meteor.loginWithPassword = function (selector, password, callback) {
});
};
Accounts._hashPassword = function (password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
};
};
Accounts._hashPassword = password => ({
digest: SHA256(password),
algorithm: "sha-256"
});
// XXX COMPAT WITH 0.8.1.3
// The server requested an upgrade from the old SRP password format,
@@ -86,8 +85,8 @@ Accounts._hashPassword = function (password) {
// us to upgrade from SRP to bcrypt.
// - userSelector: selector to retrieve the user object
// - plaintextPassword: the password as a string
var srpUpgradePath = function (options, callback) {
var details;
const srpUpgradePath = (options, callback) => {
let details;
try {
details = EJSON.parse(options.upgradeError.details);
} catch (e) {}
@@ -99,7 +98,7 @@ var srpUpgradePath = function (options, callback) {
Accounts.callLoginMethod({
methodArguments: [{
user: options.userSelector,
srp: SHA256(details.identity + ":" + options.plaintextPassword),
srp: SHA256(`${details.identity}:${options.plaintextPassword}`),
password: Accounts._hashPassword(options.plaintextPassword)
}],
userCallback: callback
@@ -120,8 +119,8 @@ var srpUpgradePath = function (options, callback) {
* @param {Function} [callback] Client only, optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
* @importFromPackage accounts-base
*/
Accounts.createUser = function (options, callback) {
options = _.clone(options); // we'll be modifying options
Accounts.createUser = (options, callback) => {
options = { ...options }; // we'll be modifying options
if (typeof options.password !== 'string')
throw new Error("options.password must be a string");
@@ -155,12 +154,15 @@ Accounts.createUser = function (options, callback) {
* @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
* @importFromPackage accounts-base
*/
Accounts.changePassword = function (oldPassword, newPassword, callback) {
Accounts.changePassword = (oldPassword, newPassword, callback) => {
if (!Meteor.user()) {
return reportError(new Error("Must be logged in to change password."), callback);
}
check(newPassword, String);
if (!newPassword instanceof String) {
return reportError(new Meteor.Error(400, "Password must be a string"), callback);
}
if (!newPassword) {
return reportError(new Meteor.Error(400, "Password may not be empty"), callback);
}
@@ -169,7 +171,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
'changePassword',
[oldPassword ? Accounts._hashPassword(oldPassword) : null,
Accounts._hashPassword(newPassword)],
function (error, result) {
(error, result) => {
if (error || !result) {
if (error && error.error === 400 &&
error.reason === 'old password format') {
@@ -180,7 +182,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
upgradeError: error,
userSelector: { id: Meteor.userId() },
plaintextPassword: oldPassword
}, function (err) {
}, err => {
if (err) {
reportError(err, callback);
} else {
@@ -216,7 +218,7 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
* @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
* @importFromPackage accounts-base
*/
Accounts.forgotPassword = function(options, callback) {
Accounts.forgotPassword = (options, callback) => {
if (!options.email) {
return reportError(new Meteor.Error(400, "Must pass options.email"), callback);
}
@@ -243,9 +245,14 @@ Accounts.forgotPassword = function(options, callback) {
* @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
* @importFromPackage accounts-base
*/
Accounts.resetPassword = function(token, newPassword, callback) {
check(token, String);
check(newPassword, String);
Accounts.resetPassword = (token, newPassword, callback) => {
if (!token instanceof String) {
return reportError(new Meteor.Error(400, "Token must be a string"), callback);
}
if (!newPassword instanceof String) {
return reportError(new Meteor.Error(400, "Password must be a string"), callback);
}
if (!newPassword) {
return reportError(new Meteor.Error(400, "Password may not be empty"), callback);
@@ -270,7 +277,7 @@ Accounts.resetPassword = function(token, newPassword, callback) {
* @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
* @importFromPackage accounts-base
*/
Accounts.verifyEmail = function(token, callback) {
Accounts.verifyEmail = (token, callback) => {
if (!token) {
return reportError(new Meteor.Error(400, "Need to pass token"), callback);
}

View File

@@ -1,8 +1,11 @@
/// BCRYPT
var bcrypt = NpmModuleBcrypt;
var bcryptHash = Meteor.wrapAsync(bcrypt.hash);
var bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
const bcrypt = NpmModuleBcrypt;
const bcryptHash = Meteor.wrapAsync(bcrypt.hash);
const bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
// Utility for grabbing user
const getUserById = id => Meteor.users.findOne(id);
// User records have a 'services.password.bcrypt' field on them to hold
// their hashed passwords (unless they have a 'services.password.srp'
@@ -22,14 +25,14 @@ var bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
// "sha-256" and then passes the digest to bcrypt.
Accounts._bcryptRounds = Accounts._options.bcryptRounds || 10;
Accounts._bcryptRounds = () => Accounts._options.bcryptRounds || 10;
// Given a 'password' from the client, extract the string that we should
// bcrypt. 'password' can be one of:
// - 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,9 +50,9 @@ 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);
return bcryptHash(password, Accounts._bcryptRounds());
};
// Extract the number of rounds used in the specified bcrypt hash.
@@ -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
};
@@ -81,13 +84,13 @@ Accounts._checkPassword = function (user, password) {
if (! bcryptCompare(formattedPassword, hash)) {
result.error = handleError("Incorrect password", false);
} else if (hash && Accounts._bcryptRounds != hashRounds) {
} else if (hash && Accounts._bcryptRounds() != hashRounds) {
// The password checks out, but the user's bcrypt hash needs to be updated.
Meteor.defer(() => {
Meteor.users.update({ _id: user._id }, {
$set: {
'services.password.bcrypt':
bcryptHash(formattedPassword, Accounts._bcryptRounds)
bcryptHash(formattedPassword, Accounts._bcryptRounds())
}
});
});
@@ -95,7 +98,7 @@ Accounts._checkPassword = function (user, password) {
return result;
};
var checkPassword = Accounts._checkPassword;
const checkPassword = Accounts._checkPassword;
///
/// ERROR HANDLER
@@ -117,14 +120,14 @@ const handleError = (msg, throwError = true) => {
/// LOGIN
///
Accounts._findUserByQuery = function (query) {
var user = null;
Accounts._findUserByQuery = query => {
let user = null;
if (query.id) {
user = Meteor.users.findOne({ _id: query.id });
user = getUserById(query.id);
} else {
var fieldName;
var fieldValue;
let fieldName;
let fieldValue;
if (query.username) {
fieldName = 'username';
fieldValue = query.username;
@@ -134,13 +137,13 @@ Accounts._findUserByQuery = function (query) {
} else {
throw new Error("shouldn't happen (validation missed something)");
}
var selector = {};
let selector = {};
selector[fieldName] = fieldValue;
user = Meteor.users.findOne(selector);
// If user is not found, try a case insensitive lookup
if (!user) {
selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue);
var candidateUsers = Meteor.users.find(selector).fetch();
const candidateUsers = Meteor.users.find(selector).fetch();
// No match if multiple candidates are found
if (candidateUsers.length === 1) {
user = candidateUsers[0];
@@ -161,11 +164,8 @@ Accounts._findUserByQuery = function (query) {
* @returns {Object} A user if found, else null
* @importFromPackage accounts-base
*/
Accounts.findUserByUsername = function (username) {
return Accounts._findUserByQuery({
username: username
});
};
Accounts.findUserByUsername =
username => Accounts._findUserByQuery({ username });
/**
* @summary Finds the user with the specified email.
@@ -177,11 +177,7 @@ Accounts.findUserByUsername = function (username) {
* @returns {Object} A user if found, else null
* @importFromPackage accounts-base
*/
Accounts.findUserByEmail = function (email) {
return Accounts._findUserByQuery({
email: email
});
};
Accounts.findUserByEmail = email => Accounts._findUserByQuery({ email });
// Generates a MongoDB selector that can be used to perform a fast case
// insensitive lookup for the given fieldName and string. Since MongoDB does
@@ -192,48 +188,48 @@ Accounts.findUserByEmail = function (email) {
// http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use),
// this has been found to greatly improve performance (from 1200ms to 5ms in a
// test with 1.000.000 users).
var selectorForFastCaseInsensitiveLookup = function (fieldName, string) {
const selectorForFastCaseInsensitiveLookup = (fieldName, string) => {
// Performance seems to improve up to 4 prefix characters
var prefix = string.substring(0, Math.min(string.length, 4));
var orClause = _.map(generateCasePermutationsForString(prefix),
function (prefixPermutation) {
var selector = {};
const prefix = string.substring(0, Math.min(string.length, 4));
const orClause = generateCasePermutationsForString(prefix).map(
prefixPermutation => {
const selector = {};
selector[fieldName] =
new RegExp('^' + Meteor._escapeRegExp(prefixPermutation));
new RegExp(`^${Meteor._escapeRegExp(prefixPermutation)}`);
return selector;
});
var caseInsensitiveClause = {};
const caseInsensitiveClause = {};
caseInsensitiveClause[fieldName] =
new RegExp('^' + Meteor._escapeRegExp(string) + '$', 'i')
new RegExp(`^${Meteor._escapeRegExp(string)}$`, 'i')
return {$and: [{$or: orClause}, caseInsensitiveClause]};
}
// Generates permutations of all case variations of a given string.
var generateCasePermutationsForString = function (string) {
var permutations = [''];
for (var i = 0; i < string.length; i++) {
var ch = string.charAt(i);
permutations = _.flatten(_.map(permutations, function (prefix) {
var lowerCaseChar = ch.toLowerCase();
var upperCaseChar = ch.toUpperCase();
const generateCasePermutationsForString = string => {
let permutations = [''];
for (let i = 0; i < string.length; i++) {
const ch = string.charAt(i);
permutations = [].concat(...(permutations.map(prefix => {
const lowerCaseChar = ch.toLowerCase();
const upperCaseChar = ch.toUpperCase();
// Don't add unneccesary permutations when ch is not a letter
if (lowerCaseChar === upperCaseChar) {
return [prefix + ch];
} else {
return [prefix + lowerCaseChar, prefix + upperCaseChar];
}
}));
})));
}
return permutations;
}
var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) {
const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => {
// Some tests need the ability to add users with the same case insensitive
// value, hence the _skipCaseInsensitiveChecksForTest check
var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue);
const skipCheck = Object.prototype.hasOwnProperty.call(Accounts._skipCaseInsensitiveChecksForTest, fieldValue);
if (fieldValue && !skipCheck) {
var matchedUsers = Meteor.users.find(
const matchedUsers = Meteor.users.find(
selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch();
if (matchedUsers.length > 0 &&
@@ -242,29 +238,29 @@ var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldV
// Otherwise, check to see if there are multiple matches or a match
// that is not us
(matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) {
handleError(displayName + " already exists.");
handleError(`${displayName} already exists.`);
}
}
};
// XXX maybe this belongs in the check package
var NonEmptyString = Match.Where(function (x) {
const NonEmptyString = Match.Where(x => {
check(x, String);
return x.length > 0;
});
var userQueryValidator = Match.Where(function (user) {
const userQueryValidator = Match.Where(user => {
check(user, {
id: Match.Optional(NonEmptyString),
username: Match.Optional(NonEmptyString),
email: Match.Optional(NonEmptyString)
});
if (_.keys(user).length !== 1)
if (Object.keys(user).length !== 1)
throw new Match.Error("User property must have exactly one field");
return true;
});
var passwordValidator = Match.OneOf(
const passwordValidator = Match.OneOf(
String,
{ digest: String, algorithm: String }
);
@@ -283,7 +279,7 @@ var passwordValidator = Match.OneOf(
//
// Note that neither password option is secure without SSL.
//
Accounts.registerLoginHandler("password", function (options) {
Accounts.registerLoginHandler("password", options => {
if (! options.password || options.srp)
return undefined; // don't handle
@@ -293,7 +289,7 @@ Accounts.registerLoginHandler("password", function (options) {
});
var user = Accounts._findUserByQuery(options.user);
const user = Accounts._findUserByQuery(options.user);
if (!user) {
handleError("User not found");
}
@@ -309,8 +305,8 @@ Accounts.registerLoginHandler("password", function (options) {
// not upgraded to bcrypt yet. We don't attempt to tell the client
// to upgrade to bcrypt, because it might be a standalone DDP
// client doesn't know how to do such a thing.
var verifier = user.services.password.srp;
var newVerifier = SRP.generateVerifier(options.password, {
const verifier = user.services.password.srp;
const newVerifier = SRP.generateVerifier(options.password, {
identity: verifier.identity, salt: verifier.salt});
if (verifier.verifier !== newVerifier.verifier) {
@@ -351,7 +347,7 @@ Accounts.registerLoginHandler("password", function (options) {
// try the SRP upgrade path.
//
// XXX COMPAT WITH 0.8.1.3
Accounts.registerLoginHandler("password", function (options) {
Accounts.registerLoginHandler("password", options => {
if (!options.srp || !options.password) {
return undefined; // don't handle
}
@@ -362,7 +358,7 @@ Accounts.registerLoginHandler("password", function (options) {
password: passwordValidator
});
var user = Accounts._findUserByQuery(options.user);
const user = Accounts._findUserByQuery(options.user);
if (!user) {
handleError("User not found");
}
@@ -377,8 +373,8 @@ Accounts.registerLoginHandler("password", function (options) {
handleError("User has no password set");
}
var v1 = user.services.password.srp.verifier;
var v2 = SRP.generateVerifier(
const v1 = user.services.password.srp.verifier;
const v2 = SRP.generateVerifier(
null,
{
hashedIdentityAndPassword: options.srp,
@@ -393,7 +389,7 @@ Accounts.registerLoginHandler("password", function (options) {
}
// Upgrade to bcrypt on successful login.
var salted = hashPassword(options.password);
const salted = hashPassword(options.password);
Meteor.users.update(
user._id,
{
@@ -419,16 +415,16 @@ Accounts.registerLoginHandler("password", function (options) {
* @param {String} newUsername A new username for the user.
* @importFromPackage accounts-base
*/
Accounts.setUsername = function (userId, newUsername) {
Accounts.setUsername = (userId, newUsername) => {
check(userId, NonEmptyString);
check(newUsername, NonEmptyString);
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user) {
handleError("User not found");
}
var oldUsername = user.username;
const oldUsername = user.username;
// Perform a case insensitive check for duplicates before update
checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id);
@@ -469,7 +465,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
throw new Meteor.Error(401, "Must be logged in");
}
var user = Meteor.users.findOne(this.userId);
const user = getUserById(this.userId);
if (!user) {
handleError("User not found");
}
@@ -486,18 +482,18 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
}));
}
var result = checkPassword(user, oldPassword);
const result = checkPassword(user, oldPassword);
if (result.error) {
throw result.error;
}
var hashed = hashPassword(newPassword);
const hashed = hashPassword(newPassword);
// It would be better if this removed ALL existing tokens and replaced
// the token for the current connection with a new one, but that would
// be tricky, so we'll settle for just replacing all tokens other than
// the one for the current connection.
var currentToken = Accounts._getLoginToken(this.connection.id);
const currentToken = Accounts._getLoginToken(this.connection.id);
Meteor.users.update(
{ _id: this.userId },
{
@@ -524,15 +520,15 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
* @param {Object} options.logout Logout all current connections with this userId (default: true)
* @importFromPackage accounts-base
*/
Accounts.setPassword = function (userId, newPlaintextPassword, options) {
options = _.extend({logout: true}, options);
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
options = { logout: true , ...options };
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user) {
throw new Meteor.Error(403, "User not found");
}
var update = {
const update = {
$unset: {
'services.password.srp': 1, // XXX COMPAT WITH 0.8.1.3
'services.password.reset': 1
@@ -552,20 +548,23 @@ Accounts.setPassword = function (userId, newPlaintextPassword, options) {
/// RESETTING VIA EMAIL
///
// Utility for plucking addresses from emails
const pluckAddresses = (emails = []) => emails.map(email => email.address);
// Method called by a user to request a password reset email. This is
// the start of the reset process.
Meteor.methods({forgotPassword: function (options) {
Meteor.methods({forgotPassword: options => {
check(options, {email: String});
var user = Accounts.findUserByEmail(options.email);
const user = Accounts.findUserByEmail(options.email);
if (!user) {
handleError("User not found");
}
const emails = _.pluck(user.emails || [], 'address');
const caseSensitiveEmail = _.find(emails, email => {
return email.toLowerCase() === options.email.toLowerCase();
});
const emails = pluckAddresses(user.emails);
const caseSensitiveEmail = emails.find(
email => email.toLowerCase() === options.email.toLowerCase()
);
Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail);
}});
@@ -580,9 +579,9 @@ Meteor.methods({forgotPassword: function (options) {
* @returns {Object} Object with {email, user, token} values.
* @importFromPackage accounts-base
*/
Accounts.generateResetToken = function (userId, email, reason, extraTokenData) {
Accounts.generateResetToken = (userId, email, reason, extraTokenData) => {
// Make sure the user exists, and email is one of their addresses.
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user) {
handleError("Can't find user");
}
@@ -593,14 +592,15 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) {
}
// make sure we have a valid email
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) {
if (!email ||
!(pluckAddresses(user.emails).includes(email))) {
handleError("No such email for user.");
}
var token = Random.secret();
var tokenRecord = {
token: token,
email: email,
const token = Random.secret();
const tokenRecord = {
token,
email,
when: new Date()
};
@@ -614,7 +614,7 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) {
}
if (extraTokenData) {
_.extend(tokenRecord, extraTokenData);
Object.assign(tokenRecord, extraTokenData);
}
Meteor.users.update({_id: user._id}, {$set: {
@@ -636,16 +636,16 @@ Accounts.generateResetToken = function (userId, email, reason, extraTokenData) {
* @returns {Object} Object with {email, user, token} values.
* @importFromPackage accounts-base
*/
Accounts.generateVerificationToken = function (userId, email, extraTokenData) {
Accounts.generateVerificationToken = (userId, email, extraTokenData) => {
// Make sure the user exists, and email is one of their addresses.
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user) {
handleError("Can't find user");
}
// pick the first unverified email if we weren't passed an email.
if (!email) {
var emailRecord = _.find(user.emails || [], function (e) { return !e.verified; });
const emailRecord = (user.emails || []).find(e => !e.verified);
email = (emailRecord || {}).address;
if (!email) {
@@ -654,20 +654,21 @@ Accounts.generateVerificationToken = function (userId, email, extraTokenData) {
}
// make sure we have a valid email
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) {
if (!email ||
!(pluckAddresses(user.emails).includes(email))) {
handleError("No such email for user.");
}
var token = Random.secret();
var tokenRecord = {
token: token,
const token = Random.secret();
const tokenRecord = {
token,
// TODO: This should probably be renamed to "email" to match reset token record.
address: email,
when: new Date()
};
if (extraTokenData) {
_.extend(tokenRecord, extraTokenData);
Object.assign(tokenRecord, extraTokenData);
}
Meteor.users.update({_id: user._id}, {$push: {
@@ -695,8 +696,8 @@ Accounts.generateVerificationToken = function (userId, email, extraTokenData) {
* @returns {Object} Options which can be passed to `Email.send`.
* @importFromPackage accounts-base
*/
Accounts.generateOptionsForEmail = function (email, user, url, reason) {
var options = {
Accounts.generateOptionsForEmail = (email, user, url, reason) => {
const options = {
to: email,
from: Accounts.emailTemplates[reason].from
? Accounts.emailTemplates[reason].from(user)
@@ -731,7 +732,7 @@ Accounts.generateOptionsForEmail = function (email, user, url, reason) {
* @returns {Object} Object with {email, user, token, url, options} values.
* @importFromPackage accounts-base
*/
Accounts.sendResetPasswordEmail = function (userId, email, extraTokenData) {
Accounts.sendResetPasswordEmail = (userId, email, extraTokenData) => {
const {email: realEmail, user, token} =
Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData);
const url = Accounts.urls.resetPassword(token);
@@ -757,7 +758,7 @@ Accounts.sendResetPasswordEmail = function (userId, email, extraTokenData) {
* @returns {Object} Object with {email, user, token, url, options} values.
* @importFromPackage accounts-base
*/
Accounts.sendEnrollmentEmail = function (userId, email, extraTokenData) {
Accounts.sendEnrollmentEmail = (userId, email, extraTokenData) => {
const {email: realEmail, user, token} =
Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData);
const url = Accounts.urls.enrollAccount(token);
@@ -769,56 +770,54 @@ Accounts.sendEnrollmentEmail = function (userId, email, extraTokenData) {
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change
// the users password, and log them in.
Meteor.methods({resetPassword: function (token, newPassword) {
var self = this;
Meteor.methods({resetPassword: function (...args) {
const token = args[0];
const newPassword = args[1];
return Accounts._loginMethod(
self,
this,
"resetPassword",
arguments,
args,
"password",
function () {
() => {
check(token, String);
check(newPassword, passwordValidator);
var user = Meteor.users.findOne({
const user = Meteor.users.findOne({
"services.password.reset.token": token});
if (!user) {
throw new Meteor.Error(403, "Token expired");
}
var when = user.services.password.reset.when;
var reason = user.services.password.reset.reason;
var tokenLifetimeMs = Accounts._getPasswordResetTokenLifetimeMs();
const { when, reason, email } = user.services.password.reset;
let tokenLifetimeMs = Accounts._getPasswordResetTokenLifetimeMs();
if (reason === "enroll") {
tokenLifetimeMs = Accounts._getPasswordEnrollTokenLifetimeMs();
}
var currentTimeMs = Date.now();
const currentTimeMs = Date.now();
if ((currentTimeMs - when) > tokenLifetimeMs)
throw new Meteor.Error(403, "Token expired");
var email = user.services.password.reset.email;
if (!_.include(_.pluck(user.emails || [], 'address'), email))
if (!(pluckAddresses(user.emails).includes(email)))
return {
userId: user._id,
error: new Meteor.Error(403, "Token has invalid email address")
};
var hashed = hashPassword(newPassword);
const hashed = hashPassword(newPassword);
// NOTE: We're about to invalidate tokens on the user, who we might be
// logged in as. Make sure to avoid logging ourselves out if this
// happens. But also make sure not to leave the connection in a state
// of having a bad token set if things fail.
var oldToken = Accounts._getLoginToken(self.connection.id);
Accounts._setLoginToken(user._id, self.connection, null);
var resetToOldToken = function () {
Accounts._setLoginToken(user._id, self.connection, oldToken);
};
const oldToken = Accounts._getLoginToken(this.connection.id);
Accounts._setLoginToken(user._id, this.connection, null);
const resetToOldToken = () =>
Accounts._setLoginToken(user._id, this.connection, oldToken);
try {
// Update the user record by:
// - Changing the password to the new one
// - Forgetting about the reset token that was just used
// - Verifying their email, since they got the password reset via email.
var affectedRecords = Meteor.users.update(
const affectedRecords = Meteor.users.update(
{
_id: user._id,
'emails.address': email,
@@ -864,7 +863,7 @@ Meteor.methods({resetPassword: function (token, newPassword) {
* @returns {Object} Object with {email, user, token, url, options} values.
* @importFromPackage accounts-base
*/
Accounts.sendVerificationEmail = function (userId, email, extraTokenData) {
Accounts.sendVerificationEmail = (userId, email, extraTokenData) => {
// XXX Also generate a link using which someone can delete this
// account if they own said address but weren't those who created
// this account.
@@ -879,34 +878,33 @@ Accounts.sendVerificationEmail = function (userId, email, extraTokenData) {
// Take token from sendVerificationEmail, mark the email as verified,
// and log them in.
Meteor.methods({verifyEmail: function (token) {
var self = this;
Meteor.methods({verifyEmail: function (...args) {
const token = args[0];
return Accounts._loginMethod(
self,
this,
"verifyEmail",
arguments,
args,
"password",
function () {
() => {
check(token, String);
var user = Meteor.users.findOne(
const user = Meteor.users.findOne(
{'services.email.verificationTokens.token': token});
if (!user)
throw new Meteor.Error(403, "Verify email link expired");
var tokenRecord = _.find(user.services.email.verificationTokens,
function (t) {
return t.token == token;
});
const tokenRecord = user.services.email.verificationTokens.find(
t => t.token == token
);
if (!tokenRecord)
return {
userId: user._id,
error: new Meteor.Error(403, "Verify email link expired")
};
var emailsRecord = _.find(user.emails, function (e) {
return e.address == tokenRecord.address;
});
const emailsRecord = user.emails.find(
e => e.address == tokenRecord.address
);
if (!emailsRecord)
return {
userId: user._id,
@@ -941,16 +939,16 @@ Meteor.methods({verifyEmail: function (token) {
* be marked as verified. Defaults to false.
* @importFromPackage accounts-base
*/
Accounts.addEmail = function (userId, newEmail, verified) {
Accounts.addEmail = (userId, newEmail, verified) => {
check(userId, NonEmptyString);
check(newEmail, NonEmptyString);
check(verified, Match.Optional(Boolean));
if (_.isUndefined(verified)) {
if (verified === void 0) {
verified = false;
}
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user)
throw new Meteor.Error(403, "User not found");
@@ -962,23 +960,26 @@ Accounts.addEmail = function (userId, newEmail, verified) {
// then we are OK and (2) if this would create a conflict with other users
// then there would already be a case-insensitive duplicate and we can't fix
// that in this code anyway.
var caseInsensitiveRegExp =
new RegExp('^' + Meteor._escapeRegExp(newEmail) + '$', 'i');
const caseInsensitiveRegExp =
new RegExp(`^${Meteor._escapeRegExp(newEmail)}$`, 'i');
var didUpdateOwnEmail = _.any(user.emails, function(email, index) {
if (caseInsensitiveRegExp.test(email.address)) {
Meteor.users.update({
_id: user._id,
'emails.address': email.address
}, {$set: {
'emails.$.address': newEmail,
'emails.$.verified': verified
}});
return true;
}
return false;
});
const didUpdateOwnEmail = user.emails.reduce(
(prev, email) => {
if (caseInsensitiveRegExp.test(email.address)) {
Meteor.users.update({
_id: user._id,
'emails.address': email.address
}, {$set: {
'emails.$.address': newEmail,
'emails.$.verified': verified
}});
return true;
} else {
return prev;
}
},
false
);
// In the other updates below, we have to do another call to
// checkForCaseInsensitiveDuplicates to make sure that no conflicting values
@@ -1025,11 +1026,11 @@ Accounts.addEmail = function (userId, newEmail, verified) {
* @param {String} email The email address to remove.
* @importFromPackage accounts-base
*/
Accounts.removeEmail = function (userId, email) {
Accounts.removeEmail = (userId, email) => {
check(userId, NonEmptyString);
check(email, NonEmptyString);
var user = Meteor.users.findOne(userId);
const user = getUserById(userId);
if (!user)
throw new Meteor.Error(403, "User not found");
@@ -1046,7 +1047,7 @@ Accounts.removeEmail = function (userId, email) {
// does the actual user insertion.
//
// returns the user id
var createUser = function (options) {
const createUser = options => {
// Unknown keys allowed, because a onCreateUserHook can take arbitrary
// options.
check(options, Match.ObjectIncluding({
@@ -1055,14 +1056,13 @@ var createUser = function (options) {
password: Match.Optional(passwordValidator)
}));
var username = options.username;
var email = options.email;
const { username, email, password } = options;
if (!username && !email)
throw new Meteor.Error(400, "Need to set a username or email");
var user = {services: {}};
if (options.password) {
var hashed = hashPassword(options.password);
const user = {services: {}};
if (password) {
const hashed = hashPassword(password);
user.services.password = { bcrypt: hashed };
}
@@ -1075,7 +1075,7 @@ var createUser = function (options) {
checkForCaseInsensitiveDuplicates('username', 'Username', username);
checkForCaseInsensitiveDuplicates('emails.address', 'Email', email);
var userId = Accounts.insertUserDoc(options, user);
const userId = Accounts.insertUserDoc(options, user);
// Perform another check after insert, in case a matching user has been
// inserted in the meantime
try {
@@ -1090,14 +1090,14 @@ var createUser = function (options) {
};
// method for create user. Requests come from the client.
Meteor.methods({createUser: function (options) {
var self = this;
Meteor.methods({createUser: function (...args) {
const options = args[0];
return Accounts._loginMethod(
self,
this,
"createUser",
arguments,
args,
"password",
function () {
() => {
// createUser() above does more checking.
check(options, Object);
if (Accounts._options.forbidClientAccountCreation)
@@ -1106,7 +1106,7 @@ Meteor.methods({createUser: function (options) {
};
// Create user. result contains id and token.
var userId = createUser(options);
const userId = createUser(options);
// safety belt. createUser is supposed to throw on error. send 500 error
// instead of sending a verification email with empty userid.
if (! userId)
@@ -1136,8 +1136,8 @@ Meteor.methods({createUser: function (options) {
// true", which we want to prevent the client from setting, but which a custom
// method calling Accounts.createUser could set?
//
Accounts.createUser = function (options, callback) {
options = _.clone(options);
Accounts.createUser = (options, callback) => {
options = { ...options };
// XXX allow an optional callback?
if (callback) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
Accounts.validateNewUser(function (user) {
Accounts.validateNewUser(user => {
if (user.profile && user.profile.invalidAndThrowException)
throw new Meteor.Error(403, "An exception thrown within Accounts.validateNewUser");
return !(user.profile && user.profile.invalid);
});
Accounts.onCreateUser(function (options, user) {
Accounts.onCreateUser((options, user) => {
if (options.testOnCreateUserHook) {
user.profile = user.profile || {};
user.profile.touchedByOnCreateUser = true;
@@ -16,7 +16,7 @@ Accounts.onCreateUser(function (options, user) {
// connection id -> action
var invalidateLogins = {};
const invalidateLogins = {};
Meteor.methods({
@@ -29,8 +29,8 @@ Meteor.methods({
});
Accounts.validateLoginAttempt(function (attempt) {
var action =
Accounts.validateLoginAttempt(attempt => {
const action =
attempt &&
attempt.connection &&
invalidateLogins[attempt.connection.id];
@@ -42,25 +42,26 @@ Accounts.validateLoginAttempt(function (attempt) {
else if (action === 'hide')
throw new Meteor.Error(403, 'hide actual error');
else
throw new Error('unknown action: ' + action);
throw new Error(`unknown action: ${action}`);
});
// connection id -> [{successful: boolean, attempt: object}]
var capturedLogins = {};
const capturedLogins = {};
let capturedLogouts = [];
Meteor.methods({
testCaptureLogins: function () {
capturedLogins[this.connection.id] = [];
},
testCaptureLogouts: function() {
testCaptureLogouts: () => {
capturedLogouts = [];
},
testFetchCapturedLogins: function () {
if (capturedLogins[this.connection.id]) {
var logins = capturedLogins[this.connection.id];
const logins = capturedLogins[this.connection.id];
delete capturedLogins[this.connection.id];
return logins;
}
@@ -68,41 +69,37 @@ Meteor.methods({
return [];
},
testFetchCapturedLogouts: function() {
return capturedLogouts;
}
testFetchCapturedLogouts: () => capturedLogouts,
});
Accounts.onLogin(function (attempt) {
Accounts.onLogin(attempt => {
if (!attempt.connection) // if login method called from the server
return;
const attemptWithoutConnection = { ...attempt };
delete attemptWithoutConnection.connection;
if (capturedLogins[attempt.connection.id])
capturedLogins[attempt.connection.id].push({
successful: true,
attempt: _.omit(attempt, 'connection')
attempt: attemptWithoutConnection,
});
});
Accounts.onLoginFailure(function (attempt) {
Accounts.onLoginFailure(attempt => {
if (!attempt.connection) // if login method called from the server
return;
const attemptWithoutConnection = { ...attempt };
delete attemptWithoutConnection.connection;
if (capturedLogins[attempt.connection.id]) {
capturedLogins[attempt.connection.id].push({
successful: false,
attempt: _.omit(attempt, 'connection')
attempt: attemptWithoutConnection,
});
}
});
var capturedLogouts = [];
Accounts.onLogout(function() {
capturedLogouts.push({
successful: true
});
});
Accounts.onLogout(() => capturedLogouts.push({ successful: true }));
// Because this is global state that affects every client, we can't turn
// it on and off during the tests. Doing so would mean two simultaneous
@@ -122,7 +119,7 @@ Accounts.config({
Meteor.methods({
testMeteorUser: function () { return Meteor.user(); },
testMeteorUser: () => Meteor.user(),
clearUsernameAndProfile: function () {
if (!this.userId)
throw new Error("Not logged in!");
@@ -133,19 +130,17 @@ Meteor.methods({
expireTokens: function () {
Accounts._expireTokens(new Date(), this.userId);
},
removeUser: function (username) {
Meteor.users.remove({ "username": username });
}
removeUser: username => Meteor.users.remove({ "username": username }),
});
// Create a user that had previously logged in with SRP.
Meteor.methods({
testCreateSRPUser: function () {
var username = Random.id();
testCreateSRPUser: () => {
const username = Random.id();
Meteor.users.remove({username: username});
var userId = Accounts.createUser({username: username});
const userId = Accounts.createUser({username: username});
Meteor.users.update(
userId,
{ '$set': { 'services.password.srp': {
@@ -157,16 +152,16 @@ Meteor.methods({
return username;
},
testSRPUpgrade: function (username) {
var user = Meteor.users.findOne({username: username});
testSRPUpgrade: username => {
const user = Meteor.users.findOne({username: username});
if (user.services && user.services.password && user.services.password.srp)
throw new Error("srp wasn't removed");
if (!(user.services && user.services.password && user.services.password.bcrypt))
throw new Error("bcrypt wasn't added");
},
testNoSRPUpgrade: function (username) {
var user = Meteor.users.findOne({username: username});
testNoSRPUpgrade: username => {
const user = Meteor.users.findOne({username: username});
if (user.services && user.services.password && user.services.password.bcrypt)
throw new Error("bcrypt was added");
if (user.services && user.services.password && ! user.services.password.srp)

View File

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

View File

@@ -1,10 +1,10 @@
Package.describe({
summary: "Login service for Twitter accounts",
version: "1.4.0"
version: "1.4.2",
});
Package.onUse(function(api) {
api.use('underscore', ['server']);
Package.onUse(api => {
api.use('ecmascript');
api.use('accounts-base', ['client', 'server']);
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,25 +1,25 @@
Accounts.oauth.registerService('twitter');
if (Meteor.isClient) {
const loginWithTwitter = function(options, callback) {
const loginWithTwitter = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Twitter.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('twitter', loginWithTwitter);
Meteor.loginWithTwitter = function () {
return Accounts.applyLoginFunction('twitter', arguments);
};
Meteor.loginWithTwitter = (...args) =>
Accounts.applyLoginFunction('twitter', args);
} else {
var autopublishedFields = _.map(
const autopublishedFields =
// don't send access token. https://dev.twitter.com/discussions/5025
Twitter.whitelistedFields.concat(['id', 'screenName']),
function (subfield) { return 'services.twitter.' + subfield; });
Twitter.whitelistedFields.concat(['id', 'screenName']).map(
subfield => `services.twitter.${subfield}`
);
Accounts.addAutopublishFields({
forLoggedInUser: autopublishedFields,

View File

@@ -24,37 +24,42 @@ Accounts.ui._options = {
* @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default).
* @importFromPackage accounts-base
*/
Accounts.ui.config = function(options) {
Accounts.ui.config = options => {
// validate options keys
var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt'];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Accounts.ui.config: Invalid key: " + key);
const VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt'];
Object.keys(options).forEach(key => {
if (!VALID_KEYS.includes(key))
throw new Error(`Accounts.ui.config: Invalid key: ${key}`);
});
// deal with `passwordSignupFields`
if (options.passwordSignupFields) {
if (_.contains([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
"EMAIL_ONLY"
], options.passwordSignupFields)) {
if (options.passwordSignupFields.reduce((prev, field) =>
prev &&
[
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
"EMAIL_ONLY"
].includes(field),
true
)) {
if (Accounts.ui._options.passwordSignupFields)
throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once");
else
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
} else {
throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields);
throw new Error(`Accounts.ui.config: Invalid option for \`passwordSignupFields\`: ${options.passwordSignupFields}`);
}
}
// deal with `requestPermissions`
if (options.requestPermissions) {
_.each(options.requestPermissions, function (scope, service) {
Object.keys(options.requestPermissions).forEach(service => {
const scope = options.forceApprovalPrompt[service];
if (Accounts.ui._options.requestPermissions[service]) {
throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service);
} else if (!(scope instanceof Array)) {
throw new Error(`Accounts.ui.config: Can't set \`requestPermissions\` more than once for ${service}`);
} else if (!Array.isArray(scope)) {
throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array");
} else {
Accounts.ui._options.requestPermissions[service] = scope;
@@ -64,12 +69,13 @@ Accounts.ui.config = function(options) {
// deal with `requestOfflineToken`
if (options.requestOfflineToken) {
_.each(options.requestOfflineToken, function (value, service) {
Object.keys(options.requestOfflineToken).forEach(service => {
const value = options.forceApprovalPrompt[service];
if (service !== 'google')
throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.");
if (Accounts.ui._options.requestOfflineToken[service]) {
throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service);
throw new Error(`Accounts.ui.config: Can't set \`requestOfflineToken\` more than once for ${service}`);
} else {
Accounts.ui._options.requestOfflineToken[service] = value;
}
@@ -78,12 +84,13 @@ Accounts.ui.config = function(options) {
// deal with `forceApprovalPrompt`
if (options.forceApprovalPrompt) {
_.each(options.forceApprovalPrompt, function (value, service) {
Object.keys(options.forceApprovalPrompt).forEach(service => {
const value = options.forceApprovalPrompt[service];
if (service !== 'google')
throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.");
if (Accounts.ui._options.forceApprovalPrompt[service]) {
throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service);
throw new Error(`Accounts.ui.config: Can't set \`forceApprovalPrompt\` more than once for ${service}`);
} else {
Accounts.ui._options.forceApprovalPrompt[service] = value;
}
@@ -91,7 +98,13 @@ Accounts.ui.config = function(options) {
}
};
passwordSignupFields = function () {
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY";
};
export const passwordSignupFields = () => {
const { passwordSignupFields } = Accounts.ui._options;
if (Array.isArray(passwordSignupFields)) {
return passwordSignupFields;
} else if (typeof passwordSignupFields === 'string') {
return [passwordSignupFields];
}
return ["EMAIL_ONLY"];
}

View File

@@ -7,20 +7,18 @@
// XXX it'd be cool to also test that the right thing happens if options
// *are* validated, but Accounts.ui._options is global state which makes this hard
// (impossible?)
Tinytest.add('accounts-ui - config validates keys', function (test) {
test.throws(function () {
Accounts.ui.config({foo: "bar"});
});
Tinytest.add('accounts-ui - config validates keys', test => {
test.throws(() => Accounts.ui.config({foo: "bar"}));
test.throws(function () {
Accounts.ui.config({passwordSignupFields: "not a valid option"});
});
test.throws(
() => Accounts.ui.config({passwordSignupFields: "not a valid option"})
);
test.throws(function () {
Accounts.ui.config({requestPermissions: {facebook: "not an array"}});
});
test.throws(
() => Accounts.ui.config({requestPermissions: {facebook: "not an array"}})
);
test.throws(function () {
Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}});
});
test.throws(
() => Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}})
);
});

View File

@@ -73,6 +73,10 @@
position: relative; // so that we can position the image absolutely within the button
}
button.login-button {
width: 100%;
}
.login-buttons-with-only-one-button {
display: inline-block;
.login-button { display: inline-block; }

View File

@@ -1,16 +1,15 @@
import { passwordSignupFields } from './accounts_ui.js';
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
const loginButtonsSession = Accounts._loginButtonsSession;
// shared between dropdown and single mode
Template.loginButtons.events({
'click #login-buttons-logout': function() {
Meteor.logout(function () {
loginButtonsSession.closeDropdown();
});
}
'click #login-buttons-logout': () =>
Meteor.logout(() => loginButtonsSession.closeDropdown()),
});
Template.registerHelper('loginButtons', function () {
Template.registerHelper('loginButtons', () => {
throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}");
});
@@ -18,8 +17,8 @@ Template.registerHelper('loginButtons', function () {
// helpers
//
displayName = function () {
var user = Meteor.user();
export const displayName = () => {
const user = Meteor.user();
if (!user)
return '';
@@ -43,11 +42,9 @@ displayName = function () {
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
getLoginServices = function () {
var self = this;
export const getLoginServices = () => {
// First look for OAuth services.
var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
@@ -58,24 +55,19 @@ getLoginServices = function () {
if (hasPasswordService())
services.push('password');
return _.map(services, function(name) {
return {name: name};
});
return services.map(name => ({ name }));
};
hasPasswordService = function () {
return !!Package['accounts-password'];
};
export const hasPasswordService = () => !!Package['accounts-password'];
dropdown = function () {
return hasPasswordService() || getLoginServices().length > 1;
};
export const dropdown = () =>
hasPasswordService() || getLoginServices().length > 1;
// XXX improve these. should this be in accounts-password instead?
//
// XXX these will become configurable, and will be validated on
// the server as well.
validateUsername = function (username) {
export const validateUsername = username => {
if (username.length >= 3) {
return true;
} else {
@@ -83,18 +75,20 @@ validateUsername = function (username) {
return false;
}
};
validateEmail = function (email) {
export const validateEmail = email => {
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
if (email.includes('@')) {
return true;
} else {
loginButtonsSession.errorMessage("Invalid email");
return false;
}
};
validatePassword = function (password) {
export const validatePassword = password => {
if (password.length >= 6) {
return true;
} else {
@@ -108,18 +102,16 @@ validatePassword = function (password) {
//
Template._loginButtonsLoggedOut.helpers({
dropdown: dropdown,
dropdown,
services: getLoginServices,
singleService: function () {
var services = getLoginServices();
singleService: () => {
const services = getLoginServices();
if (services.length !== 1)
throw new Error(
"Shouldn't be rendering this template with more than one configured service");
return services[0];
},
configurationLoaded: function () {
return Accounts.loginServicesConfigured();
}
configurationLoaded: () => Accounts.loginServicesConfigured(),
});
@@ -129,9 +121,7 @@ Template._loginButtonsLoggedOut.helpers({
// decide whether we should show a dropdown rather than a row of
// buttons
Template._loginButtonsLoggedIn.helpers({
dropdown: dropdown
});
Template._loginButtonsLoggedIn.helpers({ dropdown });
@@ -139,9 +129,7 @@ Template._loginButtonsLoggedIn.helpers({
// loginButtonsLoggedInSingleLogoutButton template
//
Template._loginButtonsLoggedInSingleLogoutButton.helpers({
displayName: displayName
});
Template._loginButtonsLoggedInSingleLogoutButton.helpers({ displayName });
@@ -150,15 +138,11 @@ Template._loginButtonsLoggedInSingleLogoutButton.helpers({
//
Template._loginButtonsMessages.helpers({
errorMessage: function () {
return loginButtonsSession.get('errorMessage');
}
errorMessage: () => loginButtonsSession.get('errorMessage'),
});
Template._loginButtonsMessages.helpers({
infoMessage: function () {
return loginButtonsSession.get('infoMessage');
}
infoMessage: () => loginButtonsSession.get('infoMessage'),
});
@@ -166,6 +150,4 @@ Template._loginButtonsMessages.helpers({
// loginButtonsLoggingInPadding template
//
Template._loginButtonsLoggingInPadding.helpers({
dropdown: dropdown
});
Template._loginButtonsLoggingInPadding.helpers({ dropdown });

View File

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

View File

@@ -1,22 +1,23 @@
import { displayName, dropdown, validatePassword } from './login_buttons.js';
// for convenience
var loginButtonsSession = Accounts._loginButtonsSession;
const loginButtonsSession = Accounts._loginButtonsSession;
// since we don't want to pass around the callback that we get from our event
// handlers, we just make it a variable for the whole file
var doneCallback;
let doneCallback;
Accounts.onResetPasswordLink(function (token, done) {
Accounts.onResetPasswordLink((token, done) => {
loginButtonsSession.set("resetPasswordToken", token);
doneCallback = done;
});
Accounts.onEnrollmentLink(function (token, done) {
Accounts.onEnrollmentLink((token, done) => {
loginButtonsSession.set("enrollAccountToken", token);
doneCallback = done;
});
Accounts.onEmailVerificationLink(function (token, done) {
Accounts.verifyEmail(token, function (error) {
Accounts.onEmailVerificationLink((token, done) => {
Accounts.verifyEmail(token, error => {
if (! error) {
loginButtonsSession.set('justVerifiedEmail', true);
}
@@ -32,29 +33,27 @@ Accounts.onEmailVerificationLink(function (token, done) {
//
Template._resetPasswordDialog.events({
'click #login-buttons-reset-password-button': function () {
resetPassword();
},
'keypress #reset-password-new-password': function (event) {
'click #login-buttons-reset-password-button': () => resetPassword(),
'keypress #reset-password-new-password': event => {
if (event.keyCode === 13)
resetPassword();
},
'click #login-buttons-cancel-reset-password': function () {
'click #login-buttons-cancel-reset-password': () => {
loginButtonsSession.set('resetPasswordToken', null);
if (doneCallback)
doneCallback();
}
});
var resetPassword = function () {
const resetPassword = () => {
loginButtonsSession.resetMessages();
var newPassword = document.getElementById('reset-password-new-password').value;
const newPassword = document.getElementById('reset-password-new-password').value;
if (!validatePassword(newPassword))
return;
Accounts.resetPassword(
loginButtonsSession.get('resetPasswordToken'), newPassword,
function (error) {
error => {
if (error) {
loginButtonsSession.errorMessage(error.reason || "Unknown error");
} else {
@@ -67,9 +66,8 @@ var resetPassword = function () {
};
Template._resetPasswordDialog.helpers({
inResetPasswordFlow: function () {
return loginButtonsSession.get('resetPasswordToken');
}
displayName,
inResetPasswordFlow: () => loginButtonsSession.get('resetPasswordToken'),
});
//
@@ -77,16 +75,13 @@ Template._resetPasswordDialog.helpers({
//
Template._justResetPasswordDialog.events({
'click #just-verified-dismiss-button': function () {
loginButtonsSession.set('justResetPassword', false);
}
'click #just-verified-dismiss-button': () =>
loginButtonsSession.set('justResetPassword', false),
});
Template._justResetPasswordDialog.helpers({
visible: function () {
return loginButtonsSession.get('justResetPassword');
},
displayName: displayName
visible: () => loginButtonsSession.get('justResetPassword'),
displayName,
});
@@ -95,30 +90,15 @@ Template._justResetPasswordDialog.helpers({
// enrollAccountDialog template
//
Template._enrollAccountDialog.events({
'click #login-buttons-enroll-account-button': function () {
enrollAccount();
},
'keypress #enroll-account-password': function (event) {
if (event.keyCode === 13)
enrollAccount();
},
'click #login-buttons-cancel-enroll-account': function () {
loginButtonsSession.set('enrollAccountToken', null);
if (doneCallback)
doneCallback();
}
});
var enrollAccount = function () {
const enrollAccount = () => {
loginButtonsSession.resetMessages();
var password = document.getElementById('enroll-account-password').value;
const password = document.getElementById('enroll-account-password').value;
if (!validatePassword(password))
return;
Accounts.resetPassword(
loginButtonsSession.get('enrollAccountToken'), password,
function (error) {
error => {
if (error) {
loginButtonsSession.errorMessage(error.reason || "Unknown error");
} else {
@@ -129,28 +109,37 @@ var enrollAccount = function () {
});
};
Template._enrollAccountDialog.helpers({
inEnrollAccountFlow: function () {
return loginButtonsSession.get('enrollAccountToken');
Template._enrollAccountDialog.events({
'click #login-buttons-enroll-account-button': enrollAccount,
'keypress #enroll-account-password': event => {
if (event.keyCode === 13)
enrollAccount();
},
'click #login-buttons-cancel-enroll-account': () => {
loginButtonsSession.set('enrollAccountToken', null);
if (doneCallback)
doneCallback();
}
});
Template._enrollAccountDialog.helpers({
displayName,
inEnrollAccountFlow: () => loginButtonsSession.get('enrollAccountToken'),
});
//
// justVerifiedEmailDialog template
//
Template._justVerifiedEmailDialog.events({
'click #just-verified-dismiss-button': function () {
loginButtonsSession.set('justVerifiedEmail', false);
}
'click #just-verified-dismiss-button': () =>
loginButtonsSession.set('justVerifiedEmail', false),
});
Template._justVerifiedEmailDialog.helpers({
visible: function () {
return loginButtonsSession.get('justVerifiedEmail');
},
displayName: displayName
visible: () => loginButtonsSession.get('justVerifiedEmail'),
displayName,
});
@@ -159,14 +148,13 @@ Template._justVerifiedEmailDialog.helpers({
//
Template._loginButtonsMessagesDialog.events({
'click #messages-dialog-dismiss-button': function () {
loginButtonsSession.resetMessages();
}
'click #messages-dialog-dismiss-button': () =>
loginButtonsSession.resetMessages(),
});
Template._loginButtonsMessagesDialog.helpers({
visible: function () {
var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
visible: () => {
const hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
return !dropdown() && hasMessage;
}
});
@@ -177,34 +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;
});
// 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 =
$('#configure-login-service-dialog input[name="loginStyle"]:checked')
.val();
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',
@@ -215,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)
@@ -226,62 +220,55 @@ Template._configureLoginServiceDialog.events({
// check whether the 'save configuration' button should be enabled.
// this is a really strange way to implement this and a Forms
// Abstraction would make all of this reactive, and simpler.
var updateSaveDisabled = function () {
var anyFieldEmpty = _.any(configurationFields(), function(field) {
return document.getElementById(
'configure-login-service-dialog-' + field.property).value === '';
});
const updateSaveDisabled = () => {
const anyFieldEmpty = configurationFields().reduce((prev, field) =>
prev || document.getElementById(
`configure-login-service-dialog-${field.property}`
).value === '',
false
);
loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty);
};
// Returns the appropriate template for this login service. This
// template should be defined in the service's package
Template._configureLoginServiceDialog.templateForService = function(serviceName) {
Template._configureLoginServiceDialog.templateForService = serviceName => {
serviceName = serviceName || loginButtonsSession.get('configureLoginServiceDialogServiceName');
// XXX Service providers should be able to specify their configuration
// template name.
return Template['configureLoginServiceDialogFor' +
(serviceName === 'meteor-developer' ?
return Template[`configureLoginServiceDialogFor${
serviceName === 'meteor-developer' ?
'MeteorDeveloper' :
capitalize(serviceName))];
capitalize(serviceName)}`];
};
var configurationFields = function () {
var template = Template._configureLoginServiceDialog.templateForService();
const configurationFields = () => {
const template = Template._configureLoginServiceDialog.templateForService();
return template.fields();
};
Template._configureLoginServiceDialog.helpers({
configurationFields: function () {
return configurationFields();
},
visible: function () {
return loginButtonsSession.get('configureLoginServiceDialogVisible');
},
configurationSteps: function () {
// renders the appropriate template
return Template._configureLoginServiceDialog.templateForService();
},
saveDisabled: function () {
return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled');
}
configurationFields,
visible: () => loginButtonsSession.get('configureLoginServiceDialogVisible'),
// renders the appropriate template
configurationSteps: () =>
Template._configureLoginServiceDialog.templateForService(),
saveDisabled: () =>
loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'),
});
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
var capitalize = function(str){
const capitalize = str => {
str = str == null ? '' : String(str);
return str.charAt(0).toUpperCase() + str.slice(1);
};
Template._configureLoginOnDesktopDialog.helpers({
visible: function () {
return loginButtonsSession.get('configureOnDesktopVisible');
}
visible: () => loginButtonsSession.get('configureOnDesktopVisible'),
});
Template._configureLoginOnDesktopDialog.events({
'click #configure-on-desktop-dismiss-button': function () {
loginButtonsSession.set('configureOnDesktopVisible', false);
}
'click #configure-on-desktop-dismiss-button': () =>
loginButtonsSession.set('configureOnDesktopVisible', false),
});

View File

@@ -97,20 +97,23 @@
{{#if inForgotPasswordFlow}}
{{> _forgotPasswordForm}}
{{else}}
<div class="login-form login-password-form">
<form class="login-form login-password-form">
{{#each fields}}
{{> _loginButtonsFormField}}
{{/each}}
{{> _loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-password">
<button
class="login-button login-button-form-submit"
id="login-buttons-password"
>
{{#if inSignupFlow}}
Create account
{{else}}
Sign in
{{/if}}
</div>
</button>
{{#if inLoginFlow}}
{{#if showCreateAccountLink}}
@@ -129,15 +132,15 @@
{{#if inSignupFlow}}
{{> _loginButtonsBackToLoginLink}}
{{/if}}
</div>
</form>
{{/if}}
</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}}
@@ -147,7 +150,7 @@
</div>
{{> _loginButtonsBackToLoginLink}}
</div>
</form>
</template>
<template name="_loginButtonsBackToLoginLink">
@@ -156,45 +159,33 @@
</div>
</template>
<!--
This strategy for login forms means that browsers' "Remember password"
functionality does not work. Different browsers have different
requirements for remembering passwords:
- Firefox: Must be an actual form (with a submit button), but you can
cancel the submit with onsubmit='return false'.
- Safari: Must be an actual form, and the form must actually be
submitted somewhere (though it can target a hidden iframe and go to a
bogus URL)
- Chrome: Must be an actual form, and the the form elements must be
present in the initial HTML, not added to the page with javascript. This
basically rules out using normal meteor templates.
https://gist.github.com/968927
-->
<template name="_loginButtonsFormField">
{{#if visible}}
<div id="login-{{fieldName}}-label-and-input">
<div id="login-{{fieldName}}-label-and-input" style="{{fieldStyle}}">
<label id="login-{{fieldName}}-label" for="login-{{fieldName}}">
{{fieldLabel}}
</label>
<input id="login-{{fieldName}}" type="{{inputType}}" />
<input
id="login-{{fieldName}}"
type="{{inputType}}"
value="{{fieldValue}}"
autocomplete="{{autocomplete}}"
/>
</div>
{{/if}}
</template>
<template name="_loginButtonsChangePassword">
{{#each fields}}
{{> _loginButtonsFormField}}
{{/each}}
<form class="login-form">
{{> _loginButtonsMessages}}
{{#each fields}}
{{> _loginButtonsFormField}}
{{/each}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
{{> _loginButtonsMessages}}
<div class="login-button login-button-form-submit" id="login-buttons-do-change-password">
Change password
</div>
</form>
</template>

View File

@@ -1,377 +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 () {
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;
@@ -393,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 {
@@ -402,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;
@@ -415,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;
@@ -424,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
@@ -433,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 {
@@ -442,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
@@ -458,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 {
@@ -483,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;
@@ -496,3 +172,341 @@ var matchPasswordAgainIfPresent = function () {
}
return true;
};
// Utility containment function that works with both arrays and single values
const isInPasswordSignupFields = (fieldOrFields) => {
const signupFields = passwordSignupFields();
if (Array.isArray(fieldOrFields)) {
return signupFields.reduce(
(prev, field) => prev && fieldOrFields.includes(field),
true,
)
}
return signupFields.includes(fieldOrFields);
};
// events shared between loginButtonsLoggedOutDropdown and
// loginButtonsLoggedInDropdown
Template.loginButtons.events({
'click #login-name-link, click #login-sign-in-link': () =>
loginButtonsSession.set('dropdownVisible', true),
'click .login-close-text': loginButtonsSession.closeDropdown,
});
//
// loginButtonsLoggedInDropdown template and related
//
Template._loginButtonsLoggedInDropdown.events({
'click #login-buttons-open-change-password': () => {
loginButtonsSession.resetMessages();
loginButtonsSession.set('inChangePasswordFlow', true);
}
});
Template._loginButtonsLoggedInDropdown.helpers({
displayName,
inChangePasswordFlow: () => loginButtonsSession.get('inChangePasswordFlow'),
inMessageOnlyFlow: () => loginButtonsSession.get('inMessageOnlyFlow'),
dropdownVisible: () => loginButtonsSession.get('dropdownVisible'),
});
Template._loginButtonsLoggedInDropdownActions.helpers({
allowChangingPassword: () => {
// it would be more correct to check whether the user has a password set,
// but in order to do that we'd have to send more data down to the client,
// and it'd be preferable not to send down the entire service.password document.
//
// instead we use the heuristic: if the user has a username or email set.
const user = Meteor.user();
return user.username || (user.emails && user.emails[0] && user.emails[0].address);
}
});
//
// loginButtonsLoggedOutDropdown template and related
//
Template._loginButtonsLoggedOutDropdown.events({
'click #login-buttons-password': event => {
event.preventDefault();
loginOrSignup();
},
'keypress #forgot-password-email': event => {
if (event.keyCode === 13)
forgotPassword();
},
'click #login-buttons-forgot-password': forgotPassword,
'click #signup-link': () => {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
const username = trimmedElementValueById('login-username');
const email = trimmedElementValueById('login-email');
const usernameOrEmail = trimmedElementValueById('login-username-or-email');
// notably not trimmed. a password could (?) start or end with a space
const password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', true);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (username !== null)
document.getElementById('login-username').value = username;
else if (email !== null)
document.getElementById('login-email').value = email;
else if (usernameOrEmail !== null)
if (!usernameOrEmail.includes('@'))
document.getElementById('login-username').value = usernameOrEmail;
else
document.getElementById('login-email').value = usernameOrEmail;
if (password !== null)
document.getElementById('login-password').value = password;
// Force redrawing the `login-dropdown-list` element because of
// a bizarre Chrome bug in which part of the DIV is not redrawn
// in case you had tried to unsuccessfully log in before
// switching to the signup form.
//
// Found tip on how to force a redraw on
// http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654
const redraw = document.getElementById('login-dropdown-list');
redraw.style.display = 'none';
redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work
redraw.style.display = 'block';
},
'click #forgot-password-link': () => {
loginButtonsSession.resetMessages();
// store values of fields before swtiching to the signup form
const email = trimmedElementValueById('login-email');
const usernameOrEmail = trimmedElementValueById('login-username-or-email');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', true);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
// update new fields with appropriate defaults
if (email !== null)
document.getElementById('forgot-password-email').value = email;
else if (usernameOrEmail !== null)
if (usernameOrEmail.includes('@'))
document.getElementById('forgot-password-email').value = usernameOrEmail;
},
'click #back-to-login-link': () => {
loginButtonsSession.resetMessages();
const username = trimmedElementValueById('login-username');
const email = trimmedElementValueById('login-email')
|| trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names?
// notably not trimmed. a password could (?) start or end with a space
const password = elementValueById('login-password');
loginButtonsSession.set('inSignupFlow', false);
loginButtonsSession.set('inForgotPasswordFlow', false);
// force the ui to update so that we have the approprate fields to fill in
Tracker.flush();
if (document.getElementById('login-username') && username !== null)
document.getElementById('login-username').value = username;
if (document.getElementById('login-email') && email !== null)
document.getElementById('login-email').value = email;
const usernameOrEmailInput = document.getElementById('login-username-or-email');
if (usernameOrEmailInput) {
if (email !== null)
usernameOrEmailInput.value = email;
if (username !== null)
usernameOrEmailInput.value = username;
}
if (password !== null)
document.getElementById('login-password').value = password;
},
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': event => {
if (event.keyCode === 13)
loginOrSignup();
}
});
Template._loginButtonsLoggedOutDropdown.helpers({
// additional classes that can be helpful in styling the dropdown
additionalClasses: () => {
if (!hasPasswordService()) {
return false;
} else {
if (loginButtonsSession.get('inSignupFlow')) {
return 'login-form-create-account';
} else if (loginButtonsSession.get('inForgotPasswordFlow')) {
return 'login-form-forgot-password';
} else {
return 'login-form-sign-in';
}
}
},
dropdownVisible: () => loginButtonsSession.get('dropdownVisible'),
hasPasswordService,
});
// return all login services, with password last
Template._loginButtonsLoggedOutAllServices.helpers({
services: getLoginServices,
isPasswordService: function () {
return this.name === 'password';
},
hasOtherServices: () => getLoginServices().length > 1,
hasPasswordService,
});
Template._loginButtonsLoggedOutPasswordService.helpers({
fields: () => {
const loginFields = [
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
autocomplete: 'username email',
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"]
),
},
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
visible: () => isInPasswordSignupFields("USERNAME_ONLY"),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields("EMAIL_ONLY"),
},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
autocomplete: 'current-password',
visible: () => true,
}
];
const signupFields = [
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
visible: () => isInPasswordSignupFields([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
]),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"]
),
},
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
autocomplete: 'email',
visible: () => isInPasswordSignupFields("USERNAME_AND_OPTIONAL_EMAIL"),
},
{fieldName: 'password', fieldLabel: 'Password', inputType: 'password',
autocomplete: 'new-password',
visible: () => true,
},
{fieldName: 'password-again', fieldLabel: 'Password (again)',
inputType: 'password', autocomplete: 'new-password',
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
visible: () => isInPasswordSignupFields(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"]
),
},
];
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
},
inForgotPasswordFlow: () => loginButtonsSession.get('inForgotPasswordFlow'),
inLoginFlow: () =>
!loginButtonsSession.get('inSignupFlow') &&
!loginButtonsSession.get('inForgotPasswordFlow'),
inSignupFlow: () => loginButtonsSession.get('inSignupFlow'),
showCreateAccountLink: () => !Accounts._options.forbidClientAccountCreation,
showForgotPasswordLink: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"]
),
});
Template._loginButtonsFormField.helpers({
inputType: function () {
return this.inputType || "text"
}
});
//
// loginButtonsChangePassword template
//
Template._loginButtonsChangePassword.events({
'keypress #login-old-password, keypress #login-password, keypress #login-password-again': event => {
if (event.keyCode === 13)
changePassword();
},
'click #login-buttons-do-change-password': changePassword,
});
Template._loginButtonsChangePassword.helpers({
fields: () => {
const { username, emails } = Meteor.user()
let email;
if (emails) {
email = emails[0].address;
}
return [
// The username and email fields are included here to address an
// accessibility warning in Chrome, but the fields don't actually display.
// The warning states that there should be an optionally hidden
// username/email field on password forms.
// XXX I think we should not use a CSS class here because this is the
// `unstyled` package. So instead we apply an inline style.
{fieldName: 'username', fieldLabel: 'Username', autocomplete: 'username',
fieldStyle: 'display: none;', fieldValue: username,
visible: () => isInPasswordSignupFields([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
]),
},
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
autocomplete: 'email', fieldStyle: 'display: none;', fieldValue: email,
visible: () => isInPasswordSignupFields(
["USERNAME_AND_EMAIL", "EMAIL_ONLY"]
),
},
{fieldName: 'old-password', fieldLabel: 'Current Password', inputType: 'password',
autocomplete: 'current-password', visible: () => true,
},
{fieldName: 'password', fieldLabel: 'New Password', inputType: 'password',
autocomplete: 'new-password', visible: () => true,
},
{fieldName: 'password-again', fieldLabel: 'New Password (again)',
inputType: 'password', autocomplete: 'new-password',
// No need to make users double-enter their password if
// they'll necessarily have an email set, since they can use
// the "forgot password" flow.
visible: () => isInPasswordSignupFields(
["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"]
),
},
];
}
});

View File

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

View File

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

View File

@@ -1,11 +1,18 @@
Package.describe({
summary: "Unstyled version of login widgets",
version: "1.3.0"
version: "1.4.1"
});
Package.onUse(function (api) {
api.use(['tracker', 'service-configuration', 'accounts-base',
'underscore', 'templating@1.2.13', 'session', 'jquery'], 'client');
api.use([
'tracker',
'service-configuration',
'accounts-base',
'ecmascript',
'templating@1.2.13',
'session',
], 'client');
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
@@ -39,7 +46,7 @@ Package.onUse(function (api) {
api.addFiles('login_buttons.import.less');
});
Package.onTest(function (api) {
Package.onTest(api => {
api.use('accounts-ui-unstyled');
api.use('tinytest');
api.addFiles('accounts_ui_tests.js', 'client');

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
Package.describe({
summary: "Login service for Sina Weibo accounts",
version: "1.3.0"
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.
api.imply('accounts-base', ['client', 'server']);

View File

@@ -1,20 +1,19 @@
Accounts.oauth.registerService('weibo');
if (Meteor.isClient) {
const loginWithWeibo = function(options, callback) {
const loginWithWeibo = (options, callback) => {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};
Accounts.registerClientLoginFunction('weibo', loginWithWeibo);
Meteor.loginWithWeibo = function () {
return Accounts.applyLoginFunction('weibo', arguments);
};
Meteor.loginWithWeibo = (...args) =>
Accounts.applyLoginFunction('weibo', args);
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately

View File

@@ -192,7 +192,11 @@ CollectionPrototype._defineMutationMethods = function(options) {
throw new Meteor.Error(403, "Access denied");
}
} catch (e) {
if (e.name === 'MongoError' || e.name === 'MinimongoError') {
if (
e.name === 'MongoError' ||
e.name === 'BulkWriteError' ||
e.name === 'MinimongoError'
) {
throw new Meteor.Error(409, e.toString());
} else {
throw e;
@@ -418,7 +422,7 @@ CollectionPrototype._callMutatorMethod = function _callMutatorMethod(name, args,
// not force a callback.
callback = function (err) {
if (err)
Meteor._debug(name + " failed: " + (err.reason || err.stack));
Meteor._debug(name + " failed", err);
};
}

View File

@@ -1,67 +1,69 @@
import { Meteor } from 'meteor/meteor';
if (window.applicationCache) {
var appCacheStatuses = [
'uncached',
'idle',
'checking',
'downloading',
'updateready',
'obsolete'
];
const appCacheStatuses = [
'uncached',
'idle',
'checking',
'downloading',
'updateready',
'obsolete'
];
var updatingAppcache = false;
var reloadRetry = null;
var appcacheUpdated = false;
let updatingAppcache = false;
let reloadRetry = null;
let appcacheUpdated = false;
Reload._onMigrate('appcache', function (retry) {
if (appcacheUpdated)
return [true];
// An uncached application (one that does not have a manifest) cannot
// be updated.
if (window.applicationCache.status === window.applicationCache.UNCACHED)
return [true];
if (!updatingAppcache) {
try {
window.applicationCache.update();
} catch (e) {
Meteor._debug('applicationCache update error', e);
// There's no point in delaying the reload if we can't update the cache.
Reload._onMigrate('appcache', retry => {
if (appcacheUpdated)
return [true];
// An uncached application (one that does not have a manifest) cannot
// be updated.
if (window.applicationCache.status === window.applicationCache.UNCACHED)
return [true];
if (!updatingAppcache) {
try {
window.applicationCache.update();
} catch (e) {
Meteor._debug('applicationCache update error', e);
// There's no point in delaying the reload if we can't update the cache.
return [true];
}
updatingAppcache = true;
}
updatingAppcache = true;
}
// Delay migration until the app cache has been updated.
reloadRetry = retry;
return false;
});
// Delay migration until the app cache has been updated.
reloadRetry = retry;
return false;
});
// If we're migrating and the app cache is now up to date, signal that
// we're now ready to migrate.
var cacheIsNowUpToDate = function () {
if (!updatingAppcache)
return;
appcacheUpdated = true;
reloadRetry();
};
window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false);
window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false);
// We'll get the obsolete event on a 404 fetching the app.manifest:
// we had previously been running with an app cache, but the app
// cache has now been disabled or the appcache package removed.
// Reload to get the new non-cached code.
window.applicationCache.addEventListener('obsolete', (function () {
if (reloadRetry) {
cacheIsNowUpToDate();
} else {
// If we're migrating and the app cache is now up to date, signal that
// we're now ready to migrate.
const cacheIsNowUpToDate = () => {
if (!updatingAppcache)
return;
appcacheUpdated = true;
Reload._reload();
}
}), false);
reloadRetry();
};
window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false);
window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false);
// We'll get the obsolete event on a 404 fetching the app.manifest:
// we had previously been running with an app cache, but the app
// cache has now been disabled or the appcache package removed.
// Reload to get the new non-cached code.
window.applicationCache.addEventListener('obsolete', () => {
if (reloadRetry) {
cacheIsNowUpToDate();
} else {
appcacheUpdated = true;
Reload._reload();
}
}, false);
} // if window.applicationCache

View File

@@ -1,22 +1,25 @@
var crypto = Npm.require('crypto');
var fs = Npm.require('fs');
var path = Npm.require('path');
import { Meteor } from 'meteor/meteor'
import { isModern } from "meteor/modern-browsers";
import { WebApp } from "meteor/webapp";
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
var _disableSizeCheck = false;
let _disableSizeCheck = false;
let disabledBrowsers = {};
Meteor.AppCache = {
config: function (options) {
_.each(options, function (value, option) {
config: options => {
Object.keys(options).forEach(option => {
value = options[option];
if (option === 'browsers') {
disabledBrowsers = {};
_.each(value, function (browser) {
disabledBrowsers[browser] = false;
});
value.each(browser => disabledBrowsers[browser] = false);
}
else if (option === 'onlineOnly') {
_.each(value, function (urlPrefix) {
RoutePolicy.declare(urlPrefix, 'static-online');
});
value.forEach(urlPrefix =>
RoutePolicy.declare(urlPrefix, 'static-online')
);
}
// option to suppress warnings for tests.
else if (option === '_disableSizeCheck') {
@@ -34,23 +37,30 @@ Meteor.AppCache = {
}
};
var disabledBrowsers = {};
var browserDisabled = function (request) {
return disabledBrowsers[request.browser.name];
};
const browserDisabled = request => disabledBrowsers[request.browser.name];
WebApp.addHtmlAttributeHook(function (request) {
if (browserDisabled(request))
return null;
else
return { manifest: "/app.manifest" };
});
// Cache of previously computed app.manifest files.
const manifestCache = new Map;
WebApp.connectHandlers.use(function (req, res, next) {
const shouldSkip = resource =>
resource.type === 'dynamic js' ||
(resource.type === 'json' &&
(resource.url.endsWith('.map') ||
resource.url.endsWith('.stats.json?meteor_js_resource=true')));
WebApp.addHtmlAttributeHook(request =>
browserDisabled(request) ?
null :
{ manifest: "/app.manifest" }
);
WebApp.connectHandlers.use((req, res, next) => {
if (req.url !== '/app.manifest') {
return next();
}
const request = WebApp.categorizeRequest(req);
// Browsers will get confused if we unconditionally serve the
// manifest and then disable the app cache for that browser. If
// the app cache had previously been enabled for a browser, it
@@ -61,13 +71,61 @@ WebApp.connectHandlers.use(function (req, res, next) {
// use"). Returning a 404 gets the browser to really turn off the
// app cache.
if (browserDisabled(WebApp.categorizeRequest(req))) {
if (browserDisabled(request)) {
res.writeHead(404);
res.end();
return;
}
var manifest = "CACHE MANIFEST\n\n";
const cacheInfo = {
modern: isModern(request.browser),
};
cacheInfo.arch = cacheInfo.modern
? "web.browser"
: "web.browser.legacy";
// The true hash of the client manifest for this arch, regardless of
// AUTOUPDATE_VERSION or Autoupdate.autoupdateVersion.
cacheInfo.clientHash = WebApp.clientHash(cacheInfo.arch);
if (Package.autoupdate) {
const {
// New in Meteor 1.7.1 (autoupdate@1.5.0), this versions object maps
// client architectures (e.g. "web.browser") to client hashes that
// reflect AUTOUPDATE_VERSION and Autoupdate.autoupdateVersion.
versions,
// The legacy way of forcing a particular version, supported here
// just in case Autoupdate.versions is not defined.
autoupdateVersion,
} = Package.autoupdate.Autoupdate;
const version = versions
? versions[cacheInfo.arch].version
: autoupdateVersion;
if (typeof version === "string" &&
version !== cacheInfo.clientHash) {
cacheInfo.autoupdateVersion = version;
}
}
const cacheKey = JSON.stringify(cacheInfo);
if (! manifestCache.has(cacheKey)) {
manifestCache.set(cacheKey, computeManifest(cacheInfo));
}
const manifest = manifestCache.get(cacheKey);
res.setHeader('Content-Type', 'text/cache-manifest');
res.setHeader('Content-Length', manifest.length);
return res.end(manifest);
});
function computeManifest(cacheInfo) {
let manifest = "CACHE MANIFEST\n\n";
// After the browser has downloaded the app files from the server and
// has populated the browser's application cache, the browser will
@@ -76,60 +134,82 @@ WebApp.connectHandlers.use(function (req, res, next) {
//
// So to ensure that the client updates if client resources change,
// include a hash of client resources in the manifest.
manifest += "# " + WebApp.clientHash() + "\n";
manifest += `# ${cacheInfo.clientHash}\n`;
// When using the autoupdate package, also include
// AUTOUPDATE_VERSION. Otherwise the client will get into an
// infinite loop of reloads when the browser doesn't fetch the new
// app HTML which contains the new version, and autoupdate will
// reload again trying to get the new code.
if (Package.autoupdate) {
var version = Package.autoupdate.Autoupdate.autoupdateVersion;
if (version !== WebApp.clientHash())
manifest += "# " + version + "\n";
if (typeof cacheInfo.autoupdateVersion === "string") {
manifest += `# ${cacheInfo.autoupdateVersion}\n`;
}
manifest += "\n";
manifest += "CACHE:" + "\n";
manifest += "/" + "\n";
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url)) {
manifest += resource.url;
// If the resource is not already cacheable (has a query
// parameter, presumably with a hash or version of some sort),
// put a version with a hash in the cache.
//
// Avoid putting a non-cacheable asset into the cache, otherwise
// the user can't modify the asset until the cache headers
// expire.
if (!resource.cacheable)
manifest += "?" + resource.hash;
manifest += "CACHE:\n";
manifest += "/\n";
manifest += "\n";
eachResource(cacheInfo, resource => {
const { url } = resource;
if (resource.where !== 'client' ||
RoutePolicy.classify(url) ||
shouldSkip(resource)) {
return;
}
manifest += url;
// If the resource is not already cacheable (has a query parameter,
// presumably with a hash or version of some sort), put a version with
// a hash in the cache.
//
// Avoid putting a non-cacheable asset into the cache, otherwise the
// user can't modify the asset until the cache headers expire.
if (! resource.cacheable) {
manifest += `?${resource.hash}`;
}
manifest += "\n";
});
manifest += "\n";
manifest += "FALLBACK:\n";
manifest += "/ /" + "\n";
// Add a fallback entry for each uncacheable asset we added above.
//
// This means requests for the bare url (/image.png instead of
// /image.png?hash) will work offline. Online, however, the browser
// will send a request to the server. Users can remove this extra
// request to the server and have the asset served from cache by
// specifying the full URL with hash in their code (manually, with
// some sort of URL rewriting helper)
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
!resource.cacheable) {
manifest += resource.url + " " + resource.url +
"?" + resource.hash + "\n";
manifest += "/ /\n";
eachResource(cacheInfo, (resource, arch, prefix) => {
const { url } = resource;
if (resource.where !== 'client' ||
RoutePolicy.classify(url) ||
shouldSkip(resource)) {
return;
}
if (! resource.cacheable) {
// Add a fallback entry for each uncacheable asset we added above.
//
// This means requests for the bare url ("/image.png" instead of
// "/image.png?hash") will work offline. Online, however, the
// browser will send a request to the server. Users can remove this
// extra request to the server and have the asset served from cache
// by specifying the full URL with hash in their code (manually,
// with some sort of URL rewriting helper)
manifest += `${url} ${url}?${resource.hash}\n`;
}
if (resource.type === 'asset' &&
prefix.length > 0 &&
url.startsWith(prefix)) {
// If the URL has a prefix like /__browser.legacy or /__cordova, add
// a fallback from the un-prefixed URL to the fully prefixed URL, so
// that legacy/cordova browsers can load assets offline without
// using an explicit prefix. When the client is online, these assets
// will simply come from the modern web.browser bundle, which does
// not prefix its asset URLs. Using a fallback rather than just
// duplicating the resources in the manifest is important because of
// appcache size limits.
manifest += `${url.slice(prefix.length)} ${url}?${resource.hash}\n`;
}
});
@@ -138,53 +218,69 @@ WebApp.connectHandlers.use(function (req, res, next) {
manifest += "NETWORK:\n";
// TODO adding the manifest file to NETWORK should be unnecessary?
// Want more testing to be sure.
manifest += "/app.manifest" + "\n";
_.each(
[].concat(
RoutePolicy.urlPrefixesFor('network'),
RoutePolicy.urlPrefixesFor('static-online')
),
function (urlPrefix) {
manifest += urlPrefix + "\n";
}
);
manifest += "*" + "\n";
manifest += "/app.manifest\n";
[
...RoutePolicy.urlPrefixesFor('network'),
...RoutePolicy.urlPrefixesFor('static-online')
].forEach(urlPrefix => manifest += `${urlPrefix}\n`);
manifest += "*\n";
// content length needs to be based on bytes
var body = Buffer.from(manifest);
return Buffer.from(manifest, "utf8");
}
res.setHeader('Content-Type', 'text/cache-manifest');
res.setHeader('Content-Length', body.length);
return res.end(body);
});
function eachResource({
modern,
arch,
}, callback) {
const manifest = WebApp.clientPrograms[arch].manifest;
var sizeCheck = function () {
var totalSize = 0;
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url)) {
totalSize += resource.size;
let prefix = "";
if (! modern) {
manifest.some(({ url }) => {
if (url && url.startsWith("/__")) {
prefix = url.split("/", 2).join("/");
return true;
}
});
}
manifest.forEach(resource => {
callback(resource, arch, prefix);
});
}
function sizeCheck() {
[ // Check size of each known architecture independently.
"web.browser",
"web.browser.legacy",
].forEach(arch => {
let totalSize = 0;
WebApp.clientPrograms[arch].manifest.forEach(resource => {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
! shouldSkip(resource)) {
totalSize += resource.size;
}
});
if (totalSize > 5 * 1024 * 1024) {
Meteor._debug([
"** You are using the appcache package but the total size of the",
`** cached resources is ${(totalSize / 1024 / 1024).toFixed(1)}MB.`,
"**",
"** This is over the recommended maximum of 5MB and may break your",
"** app in some browsers! See http://docs.meteor.com/#appcache",
"** for more information and fixes."
].join("\n"));
}
});
if (totalSize > 5 * 1024 * 1024) {
Meteor._debug(
"** You are using the appcache package but the total size of the\n" +
"** cached resources is " +
(totalSize / 1024 / 1024).toFixed(1) + "MB.\n" +
"**\n" +
"** This is over the recommended maximum of 5 MB and may break your\n" +
"** app in some browsers! See http://docs.meteor.com/#appcache\n" +
"** for more information and fixes.\n"
);
}
};
}
// Run the size check after user code has had a chance to run. That way,
// the size check can take into account files that the user does not
// want cached. Otherwise, the size check warning will still print even
// if the user excludes their large files with
// `Meteor.AppCache.config({onlineOnly: files})`.
Meteor.startup(function () {
if (! _disableSizeCheck)
sizeCheck();
});
Meteor.startup(() => _disableSizeCheck || sizeCheck());

View File

@@ -1,45 +1,44 @@
var manifestUrl = '/app.manifest';
const manifestUrl = '/app.manifest';
var appcacheTest = function (name, cb) {
Tinytest.addAsync('appcache - ' + name, function (test, next) {
HTTP.get(manifestUrl, function (err, res) {
if (err) {
test.fail(err);
} else {
cb(test, res);
}
next();
});
function appcacheTest(name, cb) {
Tinytest.addAsync(`appcache - ${name}`, test => {
return fetch(manifestUrl).then(
res => cb(test, res),
err => test.fail(err)
);
});
};
}
// Verify that the code status of the HTTP response is "OK"
appcacheTest('presence', function (test, manifest) {
test.equal(manifest.statusCode, 200, 'manifest not served');
});
appcacheTest('presence', (test, manifest) =>
test.equal(manifest.status, 200, 'manifest not served'));
// Verify the content-type HTTP header
appcacheTest('content type', function (test, manifest) {
test.equal(manifest.headers['content-type'], 'text/cache-manifest');
});
appcacheTest('content type', (test, manifest) =>
test.equal(manifest.headers.get('content-type'), 'text/cache-manifest'));
// Verify that each section header is only set once.
appcacheTest('sections uniqueness', function (test, manifest) {
var content = manifest.content;
var mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:'];
var optionalSectionHeaders = ['SETTINGS'];
_.each(_.union(mandatorySectionHeaders, optionalSectionHeaders),
function (sectionHeader) {
var globalSearch = new RegExp(sectionHeader, "g");
var matches = content.match(globalSearch) || [];
test.isTrue(matches.length <= 1, sectionHeader + ' is set twice');
if (_.contains(mandatorySectionHeaders, sectionHeader)) {
test.isTrue(matches.length == 1, sectionHeader + ' is not set');
}
});
appcacheTest('sections uniqueness', async (test, manifest) => {
const content = await manifest.text();
const mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:'];
const optionalSectionHeaders = ['SETTINGS'];
const allSectionHeaders = [
...mandatorySectionHeaders,
...optionalSectionHeaders.filter(
header => !mandatorySectionHeaders.includes(header)
),
];
allSectionHeaders.forEach(sectionHeader => {
const globalSearch = new RegExp(sectionHeader, "g");
const matches = content.match(globalSearch) || [];
test.isTrue(matches.length <= 1, `${sectionHeader} is set twice`);
if (mandatorySectionHeaders.includes(sectionHeader)) {
test.isTrue(matches.length == 1, `${sectionHeader} is not set`);
}
});
});
@@ -47,32 +46,31 @@ appcacheTest('sections uniqueness', function (test, manifest) {
// regular expressions. Regular expressions matches malformed URIs but that's
// not what we're trying to catch here (the user is free to add its own content
// in the manifest -- even malformed).
appcacheTest('sections validity', function (test, manifest) {
var lines = manifest.content.split('\n');
var i = 0;
var currentRegex = null, line = null;
appcacheTest('sections validity', async (test, manifest) => {
const lines = (await manifest.text()).split('\n');
let i = 0;
let currentRegex = null;
let line = null;
var nextLine = function () {
return lines[i++];
};
const nextLine = () => lines[i++];
var eof = function () {
return i >= lines.length;
};
const eof = () => i >= lines.length;
var nextLineMatches = function (expected, n) {
const nextLineMatches = (expected, n) => {
n = n || 1;
_.times(n, function () {
var testFunc = _.isRegExp(expected) ? 'matches' : 'equal';
for(let j = 0; j < n; j++) {
const testFunc = toString.call(expected) === '[object RegExp]' ?
'matches' :
'equal';
test[testFunc](nextLine(), expected);
});
}
};
// Verify header validity
nextLineMatches('CACHE MANIFEST');
nextLineMatches('');
nextLineMatches(/^# [a-z0-9]+$/i, 2);
nextLineMatches(/^# [a-z0-9]+$/i);
nextLineMatches('');
// Verify body validity
while (! eof()) {
@@ -96,7 +94,7 @@ appcacheTest('sections validity', function (test, manifest) {
// Outside sections, only blanks lines and comments are valid
else if (currentRegex === null)
test.fail('Invalid line ' + i + ': ' + line);
test.fail(`Invalid line ${i}: ${line}`);
// Inside a section, a star is a valid expression
else if (line === '*')
@@ -105,7 +103,7 @@ appcacheTest('sections validity', function (test, manifest) {
// If it is not a blank line, not a comment, and not a header it must
// match the current section format
else
test.matches(line, currentRegex, 'line ' + i);
test.matches(line, currentRegex, `line ${i}`);
}
});
@@ -114,30 +112,30 @@ appcacheTest('sections validity', function (test, manifest) {
// are present in the network section of the manifest. The `appcache` package
// also automatically add the manifest (`app.manifest`) add the star symbol to
// this list and therefore we also check the presence of these two elements.
appcacheTest('network section content', function (test, manifest) {
var shouldBePresentInNetworkSection = [
appcacheTest('network section content', async (test, manifest) => {
const shouldBePresentInNetworkSection = [
"/app.manifest",
"/online/",
"/bigimage.jpg",
"/largedata.json",
"*"
];
var lines = manifest.content.split('\n');
var startNetworkSection = lines.indexOf('NETWORK:');
const lines = (await manifest.text()).split('\n');
const startNetworkSection = lines.indexOf('NETWORK:');
// We search the end of the 'NETWORK:' section by looking at the beginning
// of any potential other section. By default we set this value to
// `lines.length - 1` which is the index of the last line.
var otherSections = ['CACHE:', 'FALLBACK:', 'SETTINGS'];
var endNetworkSection = _.reduce(otherSections, function (min, sectionName) {
var position = lines.indexOf(sectionName);
const otherSections = ['CACHE:', 'FALLBACK:', 'SETTINGS'];
const endNetworkSection = otherSections.reduce((min, sectionName) => {
const position = lines.indexOf(sectionName);
return position > startNetworkSection && position < min ? position : min;
}, lines.length - 1);
// We remove the first line because it's the 'NETWORK:' header line.
var networkLines = lines.slice(startNetworkSection + 1, endNetworkSection);
const networkLines = lines.slice(startNetworkSection + 1, endNetworkSection);
_.each(shouldBePresentInNetworkSection, function (item) {
test.include(networkLines, item);
});
shouldBePresentInNetworkSection.forEach(
item => test.include(networkLines, item)
);
});

View File

@@ -8,9 +8,7 @@
// real hook. We point to a non-existent file to clear the appcache in
// case there was previously a site running with appcache on
// localhost:3000.
WebApp.addHtmlAttributeHook(function (request) {
return { manifest: "/no-such-file" };
});
WebApp.addHtmlAttributeHook(request => ({ manifest: "/no-such-file" }));
// Let's add some resources in the 'NETWORK' section

View File

@@ -1,23 +1,21 @@
Package.describe({
summary: "Enable the application cache in the browser",
version: "1.1.0"
version: "1.2.2",
});
Package.onUse(function (api) {
api.use('webapp', 'server');
Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use(['webapp', 'routepolicy'], 'server');
api.use('reload', 'client');
api.use('routepolicy', 'server');
api.use('underscore', 'server');
api.use('autoupdate', 'server', {weak: true});
api.addFiles('appcache-client.js', 'client');
api.addFiles('appcache-server.js', 'server');
api.mainModule('appcache-client.js', 'client');
api.mainModule('appcache-server.js', 'server');
});
Package.onTest(function (api) {
Package.onTest(api => {
api.use('tinytest');
api.use('appcache');
api.use('http', 'client');
api.use('underscore', 'client');
api.use('fetch');
api.use('webapp', 'server');
api.addFiles('appcache_tests-server.js', 'server');
api.addFiles('appcache_tests-client.js', 'client');

View File

@@ -24,28 +24,46 @@
// The client version of the client code currently running in the
// browser.
var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown";
var autoupdateVersionRefreshable =
__meteor_runtime_config__.autoupdateVersionRefreshable || "unknown";
const clientArch = Meteor.isCordova ? "web.cordova" :
Meteor.isModern ? "web.browser" : "web.browser.legacy";
const autoupdateVersions =
__meteor_runtime_config__.autoupdate.versions[clientArch] || {
version: "unknown",
versionRefreshable: "unknown",
versionNonRefreshable: "unknown",
assets: [],
};
export const Autoupdate = {};
// The collection of acceptable client versions.
ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions");
Autoupdate = {};
const ClientVersions =
Autoupdate._ClientVersions = // Used by a self-test.
new Mongo.Collection("meteor_autoupdate_clientVersions");
Autoupdate.newClientAvailable = function () {
return !! ClientVersions.findOne({
_id: "version",
version: {$ne: autoupdateVersion} }) ||
!! ClientVersions.findOne({
_id: "version-refreshable",
version: {$ne: autoupdateVersionRefreshable} });
return !! (
ClientVersions.findOne({
_id: clientArch,
versionNonRefreshable: {
$ne: autoupdateVersions.versionNonRefreshable,
}
}) ||
ClientVersions.findOne({
_id: clientArch,
versionRefreshable: {
$ne: autoupdateVersions.versionRefreshable,
}
})
);
};
Autoupdate._ClientVersions = ClientVersions; // Used by a self-test
var knownToSupportCssOnLoad = false;
// Set to true if the link.onload callback ever fires for any <link> node.
let knownToSupportCssOnLoad = false;
var retry = new Retry({
const retry = new Retry({
// Unlike the stream reconnect use of Retry, which we want to be instant
// in normal operation, this is a wacky failure. We don't want to retry
// right away, we can start slowly.
@@ -57,12 +75,13 @@ var retry = new Retry({
minCount: 0, // don't do any immediate retries
baseTimeout: 30*1000 // start with 30s
});
var failures = 0;
Autoupdate._retrySubscription = function () {
let failures = 0;
Autoupdate._retrySubscription = () => {
Meteor.subscribe("meteor_autoupdate_clientVersions", {
onError: function (error) {
Meteor._debug("autoupdate subscription failed:", error);
onError(error) {
Meteor._debug("autoupdate subscription failed", error);
failures++;
retry.retryLater(failures, function () {
// Just retry making the subscription, don't reload the whole
@@ -75,82 +94,112 @@ Autoupdate._retrySubscription = function () {
Autoupdate._retrySubscription();
});
},
onReady: function () {
if (Package.reload) {
var checkNewVersionDocument = function (doc) {
var self = this;
if (doc._id === 'version-refreshable' &&
doc.version !== autoupdateVersionRefreshable) {
autoupdateVersionRefreshable = doc.version;
// Switch out old css links for the new css links. Inspired by:
// https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
var newCss = (doc.assets && doc.assets.allCss) || [];
var oldLinks = [];
_.each(document.getElementsByTagName('link'), function (link) {
onReady() {
// Call checkNewVersionDocument with a slight delay, so that the
// const handle declaration is guaranteed to be initialized, even if
// the added or changed callbacks are called synchronously.
const resolved = Promise.resolve();
function check(doc) {
resolved.then(() => checkNewVersionDocument(doc));
}
const handle = ClientVersions.find().observe({
added: check,
changed: check
});
function checkNewVersionDocument(doc) {
if (doc._id !== clientArch) {
return;
}
if (doc.versionNonRefreshable !==
autoupdateVersions.versionNonRefreshable) {
// Non-refreshable assets have changed, so we have to reload the
// whole page rather than just replacing <link> tags.
if (handle) handle.stop();
if (Package.reload) {
// The reload package should be provided by ddp-client, which
// is provided by the ddp package that autoupdate depends on.
Package.reload.Reload._reload();
}
return;
}
if (doc.versionRefreshable !== autoupdateVersions.versionRefreshable) {
autoupdateVersions.versionRefreshable = doc.versionRefreshable;
// Switch out old css links for the new css links. Inspired by:
// https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
var newCss = doc.assets || [];
var oldLinks = [];
Array.prototype.forEach.call(
document.getElementsByTagName('link'),
function (link) {
if (link.className === '__meteor-css__') {
oldLinks.push(link);
}
});
}
);
var waitUntilCssLoads = function (link, callback) {
var executeCallback = _.once(callback);
link.onload = function () {
knownToSupportCssOnLoad = true;
executeCallback();
};
if (! knownToSupportCssOnLoad) {
var id = Meteor.setInterval(function () {
if (link.sheet) {
executeCallback();
Meteor.clearInterval(id);
}
}, 50);
function waitUntilCssLoads(link, callback) {
var called;
link.onload = function () {
knownToSupportCssOnLoad = true;
if (! called) {
called = true;
callback();
}
};
var removeOldLinks = _.after(newCss.length, function () {
_.each(oldLinks, function (oldLink) {
oldLink.parentNode.removeChild(oldLink);
});
});
if (! knownToSupportCssOnLoad) {
var id = Meteor.setInterval(function () {
if (link.sheet) {
if (! called) {
called = true;
callback();
}
Meteor.clearInterval(id);
}
}, 50);
}
}
var attachStylesheetLink = function (newLink) {
document.getElementsByTagName("head").item(0).appendChild(newLink);
let newLinksLeftToLoad = newCss.length;
function removeOldLinks() {
if (oldLinks.length > 0 &&
--newLinksLeftToLoad < 1) {
oldLinks.splice(0).forEach(link => {
link.parentNode.removeChild(link);
});
}
}
if (newCss.length > 0) {
newCss.forEach(css => {
const newLink = document.createElement("link");
newLink.setAttribute("rel", "stylesheet");
newLink.setAttribute("type", "text/css");
newLink.setAttribute("class", "__meteor-css__");
newLink.setAttribute("href", css.url);
waitUntilCssLoads(newLink, function () {
Meteor.setTimeout(removeOldLinks, 200);
});
};
if (newCss.length !== 0) {
_.each(newCss, function (css) {
var newLink = document.createElement("link");
newLink.setAttribute("rel", "stylesheet");
newLink.setAttribute("type", "text/css");
newLink.setAttribute("class", "__meteor-css__");
newLink.setAttribute("href", css.url);
attachStylesheetLink(newLink);
});
} else {
removeOldLinks();
}
const head = document.getElementsByTagName("head").item(0);
head.appendChild(newLink);
});
} else {
removeOldLinks();
}
else if (doc._id === 'version' && doc.version !== autoupdateVersion) {
handle && handle.stop();
if (Package.reload) {
Package.reload.Reload._reload();
}
}
};
var handle = ClientVersions.find().observe({
added: checkNewVersionDocument,
changed: checkNewVersionDocument
});
}
}
}
});
};
Autoupdate._retrySubscription();

View File

@@ -1,16 +1,21 @@
var autoupdateVersionCordova = __meteor_runtime_config__.autoupdateVersionCordova || "unknown";
var autoupdateVersionsCordova =
__meteor_runtime_config__.autoupdate.versions["web.cordova"] || {
version: "unknown"
};
// The collection of acceptable client versions.
ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions");
const ClientVersions =
new Mongo.Collection("meteor_autoupdate_clientVersions");
Autoupdate = {};
export const Autoupdate = {};
Autoupdate.newClientAvailable = function() {
return !! ClientVersions.findOne({
_id: 'version-cordova',
version: {$ne: autoupdateVersionCordova}
Autoupdate.newClientAvailable =
() => !! ClientVersions.findOne({
_id: "web.cordova",
version: {
$ne: autoupdateVersionsCordova.version
}
});
};
var retry = new Retry({
// Unlike the stream reconnect use of Retry, which we want to be instant
@@ -24,12 +29,14 @@ var retry = new Retry({
minCount: 0, // don't do any immediate retries
baseTimeout: 30*1000 // start with 30s
});
var failures = 0;
Autoupdate._retrySubscription = function() {
var appId = __meteor_runtime_config__.appId;
let failures = 0;
Autoupdate._retrySubscription = () => {
const { appId } = __meteor_runtime_config__;
Meteor.subscribe("meteor_autoupdate_clientVersions", appId, {
onError: function(error) {
onError(error) {
console.log("autoupdate subscription failed:", error);
failures++;
retry.retryLater(failures, function() {
@@ -43,16 +50,18 @@ Autoupdate._retrySubscription = function() {
Autoupdate._retrySubscription();
});
},
onReady: function() {
onReady() {
if (Package.reload) {
var checkNewVersionDocument = function(doc) {
var self = this;
if (doc.version !== autoupdateVersionCordova) {
function checkNewVersionDocument(doc) {
if (doc.version !== autoupdateVersionsCordova.version) {
newVersionAvailable();
}
};
}
var handle = ClientVersions.find({_id: 'version-cordova'}).observe({
ClientVersions.find({
_id: "web.cordova"
}).observe({
added: checkNewVersionDocument,
changed: checkNewVersionDocument
});
@@ -61,8 +70,8 @@ Autoupdate._retrySubscription = function() {
});
};
Meteor.startup(function() {
WebAppLocalServer.onNewVersionReady(function() {
Meteor.startup(() => {
WebAppLocalServer.onNewVersionReady(() => {
if (Package.reload) {
Package.reload.Reload._reload();
}
@@ -71,6 +80,6 @@ Meteor.startup(function() {
Autoupdate._retrySubscription();
});
var newVersionAvailable = function() {
function newVersionAvailable() {
WebAppLocalServer.checkForUpdates();
}

View File

@@ -1,43 +1,44 @@
// Publish the current client versions to the client. When a client
// sees the subscription change and that there is a new version of the
// client available on the server, it can reload.
// Publish the current client versions for each client architecture
// (web.browser, web.browser.legacy, web.cordova). When a client observes
// a change in the versions associated with its client architecture,
// it will refresh itself, either by swapping out CSS assets or by
// reloading the page.
//
// By default there are two current client versions. The refreshable client
// version is identified by a hash of the client resources seen by the browser
// that are refreshable, such as CSS, while the non refreshable client version
// is identified by a hash of the rest of the client assets
// (the HTML, code, and static files in the `public` directory).
// There are three versions for any given client architecture: `version`,
// `versionRefreshable`, and `versionNonRefreshable`. The refreshable
// version is a hash of just the client resources that are refreshable,
// such as CSS, while the non-refreshable version is a hash of the rest of
// the client assets, excluding the refreshable ones: HTML, JS, and static
// files in the `public` directory. The `version` version is a combined
// hash of everything.
//
// If the environment variable `AUTOUPDATE_VERSION` is set it will be
// used as the client id instead. You can use this to control when
// the client reloads. For example, if you want to only force a
// reload on major changes, you can use a custom AUTOUPDATE_VERSION
// which you only change when something worth pushing to clients
// immediately happens.
// If the environment variable `AUTOUPDATE_VERSION` is set, it will be
// used in place of all client versions. You can use this variable to
// control when the client reloads. For example, if you want to force a
// reload only after major changes, use a custom AUTOUPDATE_VERSION and
// change it only when something worth pushing to clients happens.
//
// The server publishes a `meteor_autoupdate_clientVersions`
// collection. There are two documents in this collection, a document
// with _id 'version' which represents the non refreshable client assets,
// and a document with _id 'version-refreshable' which represents the
// refreshable client assets. Each document has a 'version' field
// which is equivalent to the hash of the relevant assets. The refreshable
// document also contains a list of the refreshable assets, so that the client
// can swap in the new assets without forcing a page refresh. Clients can
// observe changes on these documents to detect when there is a new
// version available.
//
// In this implementation only two documents are present in the collection
// the current refreshable client version and the current nonRefreshable client
// version. Developers can easily experiment with different versioning and
// updating models by forking this package.
// The server publishes a `meteor_autoupdate_clientVersions` collection.
// The ID of each document is the client architecture, and the fields of
// the document are the versions described above.
var Future = Npm.require("fibers/future");
Autoupdate = {};
export const Autoupdate = __meteor_runtime_config__.autoupdate = {
// Map from client architectures (web.browser, web.browser.legacy,
// web.cordova) to version fields { version, versionRefreshable,
// versionNonRefreshable, refreshable } that will be stored in
// ClientVersions documents (whose IDs are client architectures). This
// data gets serialized into the boilerplate because it's stored in
// __meteor_runtime_config__.autoupdate.versions.
versions: {}
};
// The collection of acceptable client versions.
ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions",
{ connection: null });
const ClientVersions =
new Mongo.Collection("meteor_autoupdate_clientVersions", {
connection: null
});
// The client hash includes __meteor_runtime_config__, so wait until
// all packages have loaded and have had a chance to populate the
@@ -53,91 +54,56 @@ Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID;
var syncQueue = new Meteor._SynchronousQueue();
// updateVersions can only be called after the server has fully loaded.
var updateVersions = function (shouldReloadClientProgram) {
// Step 1: load the current client program on the server and update the
// hash values in __meteor_runtime_config__.
function updateVersions(shouldReloadClientProgram) {
// Step 1: load the current client program on the server
if (shouldReloadClientProgram) {
WebAppInternals.reloadClientPrograms();
}
// If we just re-read the client program, or if we don't have an autoupdate
// version, calculate it.
if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) {
Autoupdate.autoupdateVersion =
process.env.AUTOUPDATE_VERSION ||
WebApp.calculateClientHashNonRefreshable();
}
// If we just recalculated it OR if it was set by (eg) test-in-browser,
// ensure it ends up in __meteor_runtime_config__.
__meteor_runtime_config__.autoupdateVersion =
Autoupdate.autoupdateVersion;
const {
// If the AUTOUPDATE_VERSION environment variable is defined, it takes
// precedence, but Autoupdate.autoupdateVersion is still supported as
// a fallback. In most cases neither of these values will be defined.
AUTOUPDATE_VERSION = Autoupdate.autoupdateVersion
} = process.env;
Autoupdate.autoupdateVersionRefreshable =
__meteor_runtime_config__.autoupdateVersionRefreshable =
process.env.AUTOUPDATE_VERSION ||
WebApp.calculateClientHashRefreshable();
// Step 2: update __meteor_runtime_config__.autoupdate.versions.
const clientArchs = Object.keys(WebApp.clientPrograms);
clientArchs.forEach(arch => {
Autoupdate.versions[arch] = {
version: AUTOUPDATE_VERSION ||
WebApp.calculateClientHash(arch),
versionRefreshable: AUTOUPDATE_VERSION ||
WebApp.calculateClientHashRefreshable(arch),
versionNonRefreshable: AUTOUPDATE_VERSION ||
WebApp.calculateClientHashNonRefreshable(arch),
};
});
Autoupdate.autoupdateVersionCordova =
__meteor_runtime_config__.autoupdateVersionCordova =
process.env.AUTOUPDATE_VERSION ||
WebApp.calculateClientHashCordova();
// Step 2: form the new client boilerplate which contains the updated
// Step 3: form the new client boilerplate which contains the updated
// assets and __meteor_runtime_config__.
if (shouldReloadClientProgram) {
WebAppInternals.generateBoilerplate();
}
// XXX COMPAT WITH 0.8.3
if (! ClientVersions.findOne({current: true})) {
// To ensure apps with version of Meteor prior to 0.9.0 (in
// which the structure of documents in `ClientVersions` was
// different) also reload.
ClientVersions.insert({current: true});
}
if (! ClientVersions.findOne({_id: "version"})) {
ClientVersions.insert({
_id: "version",
version: Autoupdate.autoupdateVersion
});
} else {
ClientVersions.update("version", { $set: {
version: Autoupdate.autoupdateVersion
}});
}
if (! ClientVersions.findOne({_id: "version-cordova"})) {
ClientVersions.insert({
_id: "version-cordova",
version: Autoupdate.autoupdateVersionCordova,
refreshable: false
});
} else {
ClientVersions.update("version-cordova", { $set: {
version: Autoupdate.autoupdateVersionCordova
}});
}
// Use `onListening` here because we need to use
// `WebAppInternals.refreshableAssets`, which is only set after
// Step 4: update the ClientVersions collection.
// We use `onListening` here because we need to use
// `WebApp.getRefreshableAssets`, which is only set after
// `WebApp.generateBoilerplate` is called by `main` in webapp.
WebApp.onListening(function () {
if (! ClientVersions.findOne({_id: "version-refreshable"})) {
ClientVersions.insert({
_id: "version-refreshable",
version: Autoupdate.autoupdateVersionRefreshable,
assets: WebAppInternals.refreshableAssets
});
} else {
ClientVersions.update("version-refreshable", { $set: {
version: Autoupdate.autoupdateVersionRefreshable,
assets: WebAppInternals.refreshableAssets
}});
}
WebApp.onListening(() => {
clientArchs.forEach(arch => {
const payload = {
...Autoupdate.versions[arch],
assets: WebApp.getRefreshableAssets(arch),
};
if (! ClientVersions.findOne({ _id: arch })) {
ClientVersions.insert({ _id: arch, ...payload });
} else {
ClientVersions.update(arch, { $set: payload });
}
});
});
};
}
Meteor.publish(
"meteor_autoupdate_clientVersions",
@@ -159,6 +125,17 @@ Meteor.publish(
Meteor.startup(function () {
updateVersions(false);
// Force any connected clients that are still looking for these older
// document IDs to reload.
["version",
"version-refreshable",
"version-cordova",
].forEach(_id => {
ClientVersions.upsert(_id, {
$set: { version: "outdated" }
});
});
});
var fut = new Future();
@@ -177,22 +154,17 @@ WebApp.onListening(function () {
fut.return();
});
var enqueueVersionsRefresh = function () {
function enqueueVersionsRefresh() {
syncQueue.queueTask(function () {
updateVersions(true);
});
};
}
// Listen for the special {refresh: 'client'} message, which signals that a
// client asset has changed.
process.on('message', Meteor.bindEnvironment(function (m) {
if (m && m.refresh === 'client') {
enqueueVersionsRefresh();
}
}, "handling client refresh message"));
// Listen for messages pertaining to the client-refresh topic.
import { onMessage } from "meteor/inter-process-messaging";
onMessage("client-refresh", enqueueVersionsRefresh);
// Another way to tell the process to refresh: send SIGHUP signal
process.on('SIGHUP', Meteor.bindEnvironment(function () {
enqueueVersionsRefresh();
}, "handling SIGHUP signal for refresh"));

View File

@@ -1,12 +1,13 @@
Package.describe({
summary: "Update the client when new client code is available",
version: '1.3.12'
version: '1.5.0'
});
Package.onUse(function (api) {
api.use([
'webapp',
'check'
'check',
'inter-process-messaging',
], 'server');
api.use([
@@ -15,16 +16,14 @@ Package.onUse(function (api) {
], 'client');
api.use([
'ecmascript',
'ddp',
'mongo',
'underscore'
], ['client', 'server']);
api.use(['http', 'random'], 'web.cordova');
api.addFiles('autoupdate_server.js', 'server');
api.addFiles('autoupdate_client.js', 'web.browser');
api.addFiles('autoupdate_cordova.js', 'web.cordova');
api.mainModule('autoupdate_server.js', 'server');
api.mainModule('autoupdate_client.js', 'client');
api.mainModule('autoupdate_cordova.js', 'web.cordova');
api.export('Autoupdate');
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
var semver = Npm.require("semver");
var JSON5 = Npm.require("json5");
/**
* A compiler that can be instantiated with features and used inside
* Plugin.registerCompiler
@@ -9,6 +9,7 @@ BabelCompiler = function BabelCompiler(extraFeatures) {
this.extraFeatures = extraFeatures;
this._babelrcCache = null;
this._babelrcWarnings = Object.create(null);
this.cacheDirectory = null;
};
var BCp = BabelCompiler.prototype;
@@ -20,15 +21,27 @@ var hasOwn = Object.prototype.hasOwnProperty;
var isMeteorPre144 = semver.lt(process.version, "4.8.1");
BCp.processFilesForTarget = function (inputFiles) {
var compiler = this;
// Reset this cache for each batch processed.
this._babelrcCache = null;
inputFiles.forEach(function (inputFile) {
var toBeAdded = this.processOneFileForTarget(inputFile);
if (toBeAdded) {
inputFile.addJavaScript(toBeAdded);
if (inputFile.supportsLazyCompilation) {
inputFile.addJavaScript({
path: inputFile.getPathInPackage(),
hash: inputFile.getSourceHash(),
bare: !! inputFile.getFileOptions().bare
}, function () {
return compiler.processOneFileForTarget(inputFile);
});
} else {
var toBeAdded = compiler.processOneFileForTarget(inputFile);
if (toBeAdded) {
inputFile.addJavaScript(toBeAdded);
}
}
}, this);
});
};
// Returns an object suitable for passing to inputFile.addJavaScript, or
@@ -55,8 +68,11 @@ BCp.processOneFileForTarget = function (inputFile, source) {
sourceMap: null,
bare: !! fileOptions.bare
};
var cacheDeps = {
sourceHash: toBeAdded.hash
var cacheOptions = {
cacheDirectory: this.cacheDirectory,
cacheDeps: {
sourceHash: toBeAdded.hash,
},
};
// If you need to exclude a specific file within a package from Babel
@@ -71,11 +87,14 @@ BCp.processOneFileForTarget = function (inputFile, source) {
! excludedFileExtensionPattern.test(inputFilePath)) {
var extraFeatures = Object.assign({}, this.extraFeatures);
var arch = inputFile.getArch();
if (inputFile.getArch().startsWith("os.")) {
if (arch.startsWith("os.")) {
// Start with a much simpler set of Babel presets and plugins if
// we're compiling for Node 8.
extraFeatures.nodeMajorVersion = parseInt(process.versions.node);
extraFeatures.nodeMajorVersion = parseInt(process.versions.node, 10);
} else if (arch === "web.browser") {
extraFeatures.modernBrowsers = true;
}
if (! extraFeatures.hasOwnProperty("jscript")) {
@@ -86,33 +105,39 @@ BCp.processOneFileForTarget = function (inputFile, source) {
}
var babelOptions = Babel.getDefaultOptions(extraFeatures);
babelOptions.caller = { name: "meteor", arch };
this.inferExtraBabelOptions(inputFile, babelOptions, cacheDeps);
this.inferExtraBabelOptions(
inputFile,
babelOptions,
cacheOptions.cacheDeps,
);
babelOptions.sourceMap = true;
babelOptions.sourceMaps = true;
babelOptions.filename =
babelOptions.sourceFileName = packageName
? "packages/" + packageName + "/" + inputFilePath
: inputFilePath;
babelOptions.sourceMapTarget = babelOptions.filename + ".map";
try {
var result = profile('Babel.compile', function () {
return Babel.compile(source, babelOptions, cacheDeps);
return Babel.compile(source, babelOptions, cacheOptions);
});
} catch (e) {
if (e.loc) {
// Error is from @babel/parser.
inputFile.error({
message: e.message,
line: e.loc.line,
column: e.loc.column,
});
return null;
} else {
// Error is from a Babel transform, with line/column information
// embedded in e.message.
inputFile.error(e);
}
throw e;
return null;
}
if (isMeteorPre144) {
@@ -130,6 +155,11 @@ BCp.processOneFileForTarget = function (inputFile, source) {
toBeAdded.data = result.code;
toBeAdded.hash = result.hash;
// The babelOptions.sourceMapTarget option was deprecated in Babel
// 7.0.0-beta.41: https://github.com/babel/babel/pull/7500
result.map.file = babelOptions.filename + ".map";
toBeAdded.sourceMap = result.map;
}
@@ -137,7 +167,7 @@ BCp.processOneFileForTarget = function (inputFile, source) {
};
BCp.setDiskCacheDirectory = function (cacheDir) {
Babel.setCacheDir(cacheDir);
this.cacheDirectory = cacheDir;
};
function profile(name, func) {
@@ -167,80 +197,102 @@ BCp._inferFromBabelRc = function (inputFile, babelOptions, cacheDeps) {
if (babelrcPath) {
if (! hasOwn.call(this._babelrcCache, babelrcPath)) {
try {
this._babelrcCache[babelrcPath] =
JSON.parse(inputFile.readAndWatchFile(babelrcPath));
this._babelrcCache[babelrcPath] = {
controlFilePath: babelrcPath,
controlFileData: JSON5.parse(
inputFile.readAndWatchFile(babelrcPath)),
deps: Object.create(null),
};
} catch (e) {
if (e instanceof SyntaxError) {
e.message = ".babelrc is not a valid JSON file: " + e.message;
e.message = ".babelrc is not a valid JSON5 file: " + e.message;
}
throw e;
}
}
return this._inferHelper(
inputFile,
babelOptions,
babelrcPath,
this._babelrcCache[babelrcPath],
cacheDeps
);
const cacheEntry = this._babelrcCache[babelrcPath];
if (this._inferHelper(inputFile, cacheEntry)) {
merge(babelOptions, cacheEntry, "presets");
merge(babelOptions, cacheEntry, "plugins");
Object.assign(cacheDeps, cacheEntry.deps);
return true;
}
}
};
BCp._inferFromPackageJson = function (inputFile, babelOptions, cacheDeps) {
var pkgJsonPath = inputFile.findControlFile("package.json");
if (pkgJsonPath) {
if (! hasOwn.call(this._babelrcCache, pkgJsonPath)) {
this._babelrcCache[pkgJsonPath] = JSON.parse(
inputFile.readAndWatchFile(pkgJsonPath)
).babel || null;
}
const cacheEntry = hasOwn.call(this._babelrcCache, pkgJsonPath)
? this._babelrcCache[pkgJsonPath]
: this._babelrcCache[pkgJsonPath] = {
controlFilePath: pkgJsonPath,
controlFileData: JSON.parse(
inputFile.readAndWatchFile(pkgJsonPath)
).babel || null,
deps: Object.create(null),
};
return this._inferHelper(
inputFile,
babelOptions,
pkgJsonPath,
this._babelrcCache[pkgJsonPath],
cacheDeps
);
if (this._inferHelper(inputFile, cacheEntry)) {
merge(babelOptions, cacheEntry, "presets");
merge(babelOptions, cacheEntry, "plugins");
Object.assign(cacheDeps, cacheEntry.deps);
return true;
}
}
};
BCp._inferHelper = function (
inputFile,
babelOptions,
controlFilePath,
babelrc,
cacheDeps
) {
if (! babelrc) {
BCp._inferHelper = function (inputFile, cacheEntry) {
if (! cacheEntry.controlFileData) {
return false;
}
if (hasOwn.call(cacheEntry, "finalInferHelperResult")) {
// We've already run _inferHelper and populated
// cacheEntry.{presets,plugins}, so we can return early here.
return cacheEntry.finalInferHelperResult;
}
var compiler = this;
function walkBabelRC(obj, path) {
if (obj && typeof obj === "object") {
const copy = Object.create(null);
path = path || [];
var index = path.push("presets") - 1;
walkHelper(obj.presets, path);
path[index] = "plugins";
walkHelper(obj.plugins, path);
const index = path.length;
if (obj.presets) {
path[index] = "presets";
copy.presets = walkHelper(obj.presets, path);
}
if (obj.plugins) {
path[index] = "plugins";
copy.plugins = walkHelper(obj.plugins, path);
}
path.pop();
return copy;
}
return obj;
}
function walkHelper(list, path) {
if (list) {
// Empty the list and then refill it with resolved values.
list.splice(0).forEach(function (pluginOrPreset) {
var res = resolveHelper(pluginOrPreset, path);
if (res) {
list.push(res);
}
});
}
const copy = [];
list.forEach(function (pluginOrPreset) {
const res = resolveHelper(pluginOrPreset, path);
if (res) {
copy.push(res);
}
});
return copy;
}
function resolveHelper(value, path) {
@@ -252,24 +304,24 @@ BCp._inferHelper = function (
if (Array.isArray(value)) {
// The value is a [plugin, options] pair.
var res = value[0] = resolveHelper(value[0], path);
const res = resolveHelper(value[0], path);
if (res) {
return value;
const copy = value.slice(0);
copy[0] = res;
return copy;
}
} else if (typeof value === "string") {
// The value is a string that we need to require.
var result = requireWithPath(value, path);
const result = requireWithPath(value, path);
if (result && result.module) {
cacheDeps[result.name] = result.version;
walkBabelRC(result.module, path);
return result.module;
cacheEntry.deps[result.name] = result.version;
return walkBabelRC(result.module, path);
}
} else if (typeof value === "object") {
// The value is a { presets?, plugins? } preset object.
walkBabelRC(value, path);
return value;
return walkBabelRC(value, path);
}
}
@@ -277,55 +329,67 @@ BCp._inferHelper = function (
}
function requireWithPath(id, path) {
var prefix;
var lastInPath = path[path.length - 1];
const prefixes = [];
const lastInPath = path[path.length - 1];
if (lastInPath === "presets") {
prefix = "babel-preset-";
prefixes.push("@babel/preset-", "babel-preset-");
} else if (lastInPath === "plugins") {
prefix = "babel-plugin-";
prefixes.push("@babel/plugin-", "babel-plugin-");
}
// Try without a prefix if the prefixes fail.
prefixes.push("");
try {
return requireWithPrefix(inputFile, id, prefix, controlFilePath);
return requireWithPrefixes(
inputFile, id, prefixes,
cacheEntry.controlFilePath
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
throw e;
}
if (! hasOwn.call(compiler._babelrcWarnings, id)) {
compiler._babelrcWarnings[id] = controlFilePath;
compiler._babelrcWarnings[id] = cacheEntry.controlFilePath;
console.error(
"Warning: unable to resolve " +
JSON.stringify(id) +
" in " + path.join(".") +
" of " + controlFilePath
" of " + cacheEntry.controlFilePath + ", due to:"
);
console.error(e.stack || e);
}
return null;
}
}
babelrc = JSON.parse(JSON.stringify(babelrc));
const { controlFileData } = cacheEntry;
const clean = walkBabelRC(controlFileData);
merge(cacheEntry, clean, "presets");
merge(cacheEntry, clean, "plugins");
walkBabelRC(babelrc);
if (controlFileData &&
controlFileData.env) {
const envKey =
process.env.BABEL_ENV ||
process.env.NODE_ENV ||
"development";
merge(babelOptions, babelrc, "presets");
merge(babelOptions, babelrc, "plugins");
const clean = walkBabelRC(controlFileData.env[envKey]);
const babelEnv = (process.env.BABEL_ENV ||
process.env.NODE_ENV ||
"development");
if (babelrc && babelrc.env && babelrc.env[babelEnv]) {
const env = babelrc.env[babelEnv];
walkBabelRC(env);
merge(babelOptions, env, "presets");
merge(babelOptions, env, "plugins");
if (clean) {
merge(cacheEntry, clean, "presets");
merge(cacheEntry, clean, "plugins");
}
}
return !! (babelrc.presets ||
babelrc.plugins);
return cacheEntry.finalInferHelperResult =
!! (cacheEntry.presets ||
cacheEntry.plugins);
};
function merge(babelOptions, babelrc, name) {
@@ -336,30 +400,63 @@ function merge(babelOptions, babelrc, name) {
}
}
function requireWithPrefix(inputFile, id, prefix, controlFilePath) {
const forbiddenPresetNames = new Set([
// Since Meteor always includes babel-preset-meteor automatically, it's
// likely a mistake for that preset to appear in a custom .babelrc
// file. Previously we recommended that developers simply remove the
// preset (e.g. #9631), but we can easily just ignore it by returning
// null here, which seems like a better solution since it allows the
// same .babelrc file to be used for other purposes, such as running
// tests with a testing tool that needs to compile application code the
// same way Meteor does.
"babel-preset-meteor",
// Similar reasoning applies to these commonly misused Babel presets:
"@babel/preset-env",
"@babel/preset-react",
]);
function requireWithPrefixes(inputFile, id, prefixes, controlFilePath) {
var isTopLevel = "./".indexOf(id.charAt(0)) < 0;
var presetOrPlugin;
var presetOrPluginMeta;
if (isTopLevel) {
if (! prefix) {
throw new Error("missing babelrc prefix");
}
var presetOrPluginId;
try {
// If the identifier is top-level, try to prefix it with
// "babel-plugin-" or "babel-preset-".
presetOrPlugin = inputFile.require(prefix + id);
presetOrPluginMeta = inputFile.require(
packageNameFromTopLevelModuleId(prefix + id) + '/package.json');
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
throw e;
var found = prefixes.some(function (prefix) {
try {
// Call inputFile.resolve here rather than inputFile.require so
// that the import doesn't fail due to missing transitive
// dependencies imported by the preset or plugin.
if (inputFile.resolve(prefix + id, controlFilePath)) {
presetOrPluginId = prefix + id;
}
presetOrPluginMeta = inputFile.require(
packageNameFromTopLevelModuleId(prefix + id) + "/package.json",
controlFilePath
);
return true;
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
throw e;
}
return false;
}
// Fall back to requiring the plugin as-is if the prefix failed.
presetOrPlugin = inputFile.require(id);
presetOrPluginMeta = inputFile.require(
packageNameFromTopLevelModuleId(id) + '/package.json');
});
if (found) {
if (forbiddenPresetNames.has(presetOrPluginMeta.name)) {
return null;
}
presetOrPlugin = inputFile.require(
presetOrPluginId,
controlFilePath
);
}
} else {
@@ -380,16 +477,26 @@ function requireWithPrefix(inputFile, id, prefix, controlFilePath) {
};
}
return {
name: presetOrPluginMeta.name,
version: presetOrPluginMeta.version,
module: presetOrPlugin.__esModule
? presetOrPlugin.default
: presetOrPlugin
};
if (presetOrPlugin &&
presetOrPluginMeta) {
return {
name: presetOrPluginMeta.name,
version: presetOrPluginMeta.version,
module: presetOrPlugin.__esModule
? presetOrPlugin.default
: presetOrPlugin
};
}
return null;
}
// 'react-hot-loader/babel' => 'react-hot-loader'
// react-hot-loader/babel => react-hot-loader
// @babel/preset-env/lib/index.js => @babel/preset-env
function packageNameFromTopLevelModuleId(id) {
return id.split("/", 1)[0];
const parts = id.split("/", 2);
if (parts[0].charAt(0) === "@") {
return parts.join("/");
}
return parts[0];
}

View File

@@ -9,13 +9,7 @@ function getMeteorBabel() {
function getDefaultOptions(extraFeatures) {
// See https://github.com/meteor/babel/blob/master/options.js for more
// information about what the default options are.
var options = getMeteorBabel().getDefaultOptions(extraFeatures);
// The sourceMap option should probably be removed from the default
// options returned by meteorBabel.getDefaultOptions.
delete options.sourceMap;
return options;
return getMeteorBabel().getDefaultOptions(extraFeatures);
}
Babel = {
@@ -28,11 +22,16 @@ Babel = {
return getMeteorBabel().parse(source);
},
compile: function (source, options) {
options = options || getDefaultOptions();
return getMeteorBabel().compile(source, options);
compile: function (source, babelOptions, cacheOptions) {
return getMeteorBabel().compile(
source,
babelOptions || getDefaultOptions(),
cacheOptions,
);
},
// This method is deprecated in favor of passing
// cacheDeps.cacheDirectory to Babel.compile (see above).
setCacheDir: function (cacheDir) {
getMeteorBabel().setCacheDir(cacheDir);
},
@@ -44,5 +43,9 @@ Babel = {
getMinifierOptions: function (extraFeatures) {
return getMeteorBabel().getMinifierOptions(extraFeatures);
},
getMinimumModernBrowserVersions: function () {
return Npm.require("meteor-babel/modern-versions.js").get();
}
};

View File

@@ -6,19 +6,22 @@ Package.describe({
// isn't possible because you can't publish a non-recommended
// release with package versions that don't have a pre-release
// identifier at the end (eg, -dev)
version: '6.24.7'
version: '7.2.1'
});
Npm.depends({
'meteor-babel': '0.24.7'
'meteor-babel': '7.1.3',
'json5': '2.1.0'
});
Package.onUse(function (api) {
api.use('ecmascript-runtime', 'server');
api.use('modern-browsers');
api.addFiles([
'babel.js',
'babel-compiler.js'
'babel-compiler.js',
'versions.js',
], 'server');
api.export('Babel', 'server');

View File

@@ -0,0 +1,11 @@
// Make sure code compiled with features.modernBrowsers is delivered only
// to browsers that satisfy the assumptions of meteor-babel's modern Babel
// configuration.
Package["modern-browsers"].setMinimumBrowserVersions(
Babel.getMinimumModernBrowserVersions(),
// Although module.id is the recommended source string to pass as the
// second argument to setMinimumBrowserVersions, we can't use module.id
// here because babel-compiler cannot depend on the modules package. We
// can still make this string look like any other module.id, though.
"/node_modules/meteor/babel-compiler/versions.js"
);

View File

@@ -1,47 +1,28 @@
exports.meteorBabelHelpers = require("meteor-babel-helpers");
// Returns true if a given absolute identifier will be provided at runtime
// by the babel-runtime package.
exports.checkHelper = function checkHelper(id) {
// There used to be more complicated logic here, when the babel-runtime
// package provided helper implementations of its own, but now this
// function exists just for backwards compatibility.
return false;
};
try {
var babelRuntimeVersion = require("babel-runtime/package.json").version;
var regeneratorRuntime = require("babel-runtime/regenerator");
var babelRuntimeVersion = require("@babel/runtime/package.json").version;
} catch (e) {
throw new Error([
"The babel-runtime npm package could not be found in your node_modules ",
"",
"The @babel/runtime npm package could not be found in your node_modules ",
"directory. Please run the following command to install it:",
"",
" meteor npm install --save babel-runtime",
" meteor npm install --save @babel/runtime",
""
].join("\n"));
}
if (parseInt(babelRuntimeVersion, 10) < 6) {
throw new Error([
"The version of babel-runtime installed in your node_modules directory ",
if (parseInt(babelRuntimeVersion, 10) < 7 ||
(babelRuntimeVersion.indexOf("7.0.0-beta.") === 0 &&
parseInt(babelRuntimeVersion.split(".").pop(), 10) < 56)) {
console.error([
"The version of @babel/runtime installed in your node_modules directory ",
"(" + babelRuntimeVersion + ") is out of date. Please upgrade it by running ",
"",
" meteor npm install --save babel-runtime",
" meteor npm install --save @babel/runtime@latest",
"",
"in your application directory.",
""
].join("\n"));
}
if (regeneratorRuntime &&
typeof Promise === "function" &&
typeof Promise.asyncApply === "function") {
// If Promise.asyncApply is defined, use it to wrap calls to
// runtime.async so that the entire async function will run in its own
// Fiber, not just the code that comes after the first await.
var realAsync = regeneratorRuntime.async;
regeneratorRuntime.async = function () {
return Promise.asyncApply(realAsync, regeneratorRuntime, arguments);
};
}

View File

@@ -1,7 +1,7 @@
Package.describe({
name: "babel-runtime",
summary: "Runtime support for output of Babel transpiler",
version: '1.1.1',
version: '1.3.0',
documentation: 'README.md'
});
@@ -10,12 +10,7 @@ Npm.depends({
});
Package.onUse(function (api) {
// If the es5-shim package is installed, make sure it loads before
// babel-runtime, since babel-runtime uses some ES5 APIs like
// Object.defineProperties that are buggy in older browsers.
api.use("es5-shim", { weak: true });
api.use("modules");
api.use("promise"); // Needed by Regenerator.
api.mainModule("babel-runtime.js");
api.export("meteorBabelHelpers");
});

View File

@@ -1,144 +1,154 @@
// Base 64 encoding
var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var BASE_64_VALS = {};
const BASE_64_VALS = Object.create(null);
for (var i = 0; i < BASE_64_CHARS.length; i++) {
BASE_64_VALS[BASE_64_CHARS.charAt(i)] = i;
const getChar = val => BASE_64_CHARS.charAt(val);
const getVal = ch => ch === '=' ? -1 : BASE_64_VALS[ch];
for (let i = 0; i < BASE_64_CHARS.length; i++) {
BASE_64_VALS[getChar(i)] = i;
};
Base64 = {};
Base64.encode = function (array) {
const encode = array => {
if (typeof array === "string") {
var str = array;
array = Base64.newBinary(str.length);
for (var i = 0; i < str.length; i++) {
var ch = str.charCodeAt(i);
const str = array;
array = newBinary(str.length);
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
if (ch > 0xFF) {
throw new Error(
"Not ascii. Base64.encode can only take ascii strings.");
}
array[i] = ch;
}
}
var answer = [];
var a = null;
var b = null;
var c = null;
var d = null;
for (var i = 0; i < array.length; i++) {
const answer = [];
let a = null;
let b = null;
let c = null;
let d = null;
array.forEach((elm, i) => {
switch (i % 3) {
case 0:
a = (array[i] >> 2) & 0x3F;
b = (array[i] & 0x03) << 4;
break;
case 1:
b = b | (array[i] >> 4) & 0xF;
c = (array[i] & 0xF) << 2;
break;
case 2:
c = c | (array[i] >> 6) & 0x03;
d = array[i] & 0x3F;
answer.push(getChar(a));
answer.push(getChar(b));
answer.push(getChar(c));
answer.push(getChar(d));
a = null;
b = null;
c = null;
d = null;
break;
case 0:
a = (elm >> 2) & 0x3F;
b = (elm & 0x03) << 4;
break;
case 1:
b = b | (elm >> 4) & 0xF;
c = (elm & 0xF) << 2;
break;
case 2:
c = c | (elm >> 6) & 0x03;
d = elm & 0x3F;
answer.push(getChar(a));
answer.push(getChar(b));
answer.push(getChar(c));
answer.push(getChar(d));
a = null;
b = null;
c = null;
d = null;
break;
}
}
});
if (a != null) {
answer.push(getChar(a));
answer.push(getChar(b));
if (c == null)
if (c == null) {
answer.push('=');
else
} else {
answer.push(getChar(c));
if (d == null)
}
if (d == null) {
answer.push('=');
}
}
return answer.join("");
};
var getChar = function (val) {
return BASE_64_CHARS.charAt(val);
};
var getVal = function (ch) {
if (ch === '=') {
return -1;
}
return BASE_64_VALS[ch];
};
// XXX This is a weird place for this to live, but it's used both by
// this package and 'ejson', and we can't put it in 'ejson' without
// introducing a circular dependency. It should probably be in its own
// package or as a helper in a package that both 'base64' and 'ejson'
// use.
Base64.newBinary = function (len) {
const newBinary = len => {
if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') {
var ret = [];
for (var i = 0; i < len; i++) {
const ret = [];
for (let i = 0; i < len; i++) {
ret.push(0);
}
ret.$Uint8ArrayPolyfill = true;
return ret;
}
return new Uint8Array(new ArrayBuffer(len));
};
Base64.decode = function (str) {
var len = Math.floor((str.length*3)/4);
const decode = str => {
let len = Math.floor((str.length * 3) / 4);
if (str.charAt(str.length - 1) == '=') {
len--;
if (str.charAt(str.length - 2) == '=')
if (str.charAt(str.length - 2) == '=') {
len--;
}
var arr = Base64.newBinary(len);
var one = null;
var two = null;
var three = null;
var j = 0;
for (var i = 0; i < str.length; i++) {
var c = str.charAt(i);
var v = getVal(c);
switch (i % 4) {
case 0:
if (v < 0)
throw new Error('invalid base64 string');
one = v << 2;
break;
case 1:
if (v < 0)
throw new Error('invalid base64 string');
one = one | (v >> 4);
arr[j++] = one;
two = (v & 0x0F) << 4;
break;
case 2:
if (v >= 0) {
two = two | (v >> 2);
arr[j++] = two;
three = (v & 0x03) << 6;
}
break;
case 3:
if (v >= 0) {
arr[j++] = three | v;
}
break;
}
}
const arr = newBinary(len);
let one = null;
let two = null;
let three = null;
let j = 0;
for (let i = 0; i < str.length; i++) {
const c = str.charAt(i);
const v = getVal(c);
switch (i % 4) {
case 0:
if (v < 0) {
throw new Error('invalid base64 string');
}
one = v << 2;
break;
case 1:
if (v < 0) {
throw new Error('invalid base64 string');
}
one = one | (v >> 4);
arr[j++] = one;
two = (v & 0x0F) << 4;
break;
case 2:
if (v >= 0) {
two = two | (v >> 2);
arr[j++] = two;
three = (v & 0x03) << 6;
}
break;
case 3:
if (v >= 0) {
arr[j++] = three | v;
}
break;
}
}
return arr;
};
export const Base64 = { encode, decode, newBinary };

View File

@@ -1,59 +1,58 @@
var asciiToArray = function (str) {
var arr = Base64.newBinary(str.length);
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
import { Base64 } from './base64.js';
const asciiToArray = str => {
const arr = Base64.newBinary(str.length);
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
if (c > 0xFF) {
throw new Error("Not ascii");
}
arr[i] = c;
}
return arr;
};
var arrayToAscii = function (arr) {
var res = [];
for (var i = 0; i < arr.length; i++) {
res.push(String.fromCharCode(arr[i]));
}
return res.join("");
};
const arrayToAscii = arr => arr
.reduce(
(prev, charCode) => prev.push(String.fromCharCode(charCode)) && prev, []
).join('');
Tinytest.add("base64 - testing the test", function (test) {
Tinytest.add("base64 - testing the test", test => {
test.equal(arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")),
"The quick brown fox jumps over the lazy dog");
});
Tinytest.add("base64 - empty", function (test) {
Tinytest.add("base64 - empty", test => {
test.equal(Base64.encode(EJSON.newBinary(0)), "");
test.equal(Base64.decode(""), EJSON.newBinary(0));
});
Tinytest.add("base64 - wikipedia examples", function (test) {
var tests = [
Tinytest.add("base64 - wikipedia examples", test => {
const tests = [
{txt: "pleasure.", res: "cGxlYXN1cmUu"},
{txt: "leasure.", res: "bGVhc3VyZS4="},
{txt: "easure.", res: "ZWFzdXJlLg=="},
{txt: "asure.", res: "YXN1cmUu"},
{txt: "sure.", res: "c3VyZS4="}
];
_.each(tests, function(t) {
tests.forEach(t => {
test.equal(Base64.encode(asciiToArray(t.txt)), t.res);
test.equal(arrayToAscii(Base64.decode(t.res)), t.txt);
});
});
Tinytest.add("base64 - non-text examples", function (test) {
var tests = [
Tinytest.add("base64 - non-text examples", test => {
const tests = [
{array: [0, 0, 0], b64: "AAAA"},
{array: [0, 0, 1], b64: "AAAB"}
];
_.each(tests, function(t) {
tests.forEach(t => {
test.equal(Base64.encode(t.array), t.b64);
var expectedAsBinary = EJSON.newBinary(t.array.length);
_.each(t.array, function (val, i) {
expectedAsBinary[i] = val;
});
const expectedAsBinary = EJSON.newBinary(t.array.length);
t.array.forEach((val, i) => expectedAsBinary[i] = val);
test.equal(Base64.decode(t.b64), expectedAsBinary);
});
});

View File

@@ -1,16 +1,15 @@
Package.describe({
summary: "Base64 encoding and decoding",
version: '1.0.10'
version: '1.0.11',
});
Package.onUse(function (api) {
Package.onUse(api => {
api.export('Base64');
api.addFiles('base64.js', ['client', 'server']);
api.use('ecmascript');
api.mainModule('base64.js');
});
Package.onTest(function (api) {
api.use('base64', ['client', 'server']);
api.use(['tinytest', 'underscore', 'ejson']);
Package.onTest(api => {
api.use(['ecmascript', 'tinytest', 'ejson']);
api.addFiles('base64_test.js', ['client', 'server']);
});

View File

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

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