mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-0.6.0'
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.DS_Store
|
||||
/.meteor
|
||||
*~
|
||||
/dev_bundle
|
||||
/dev_bundle*.tar.gz
|
||||
@@ -11,4 +12,5 @@
|
||||
*.sublime-workspace
|
||||
TAGS
|
||||
*.log
|
||||
*.out
|
||||
*.out
|
||||
npm-debug.log
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "meteor-awssum"]
|
||||
path = scripts/admin/publish-release/packages/awssum
|
||||
url = https://github.com/avital/meteor-awssum
|
||||
65
History.md
65
History.md
@@ -1,6 +1,71 @@
|
||||
|
||||
## vNEXT
|
||||
|
||||
## v0.6.0
|
||||
|
||||
* Meteor has a brand new distribution system! In this new system, code-named
|
||||
Engine, packages are downloaded individually and on demand. All of the
|
||||
packages in each official Meteor release are prefetched and cached so you can
|
||||
still use Meteor while offline. You can have multiple releases of Meteor
|
||||
installed simultaneously; apps are pinned to specific Meteor releases.
|
||||
All `meteor` commands accept a `--release` argument to specify which release
|
||||
to use; `meteor update` changes what release the app is pinned to.
|
||||
Inside an app, the name of the release is available at `Meteor.release`.
|
||||
When running Meteor directly from a git checkout, the release is ignored.
|
||||
|
||||
* Variables declared with `var` at the outermost level of a JavaScript
|
||||
source file are now private to that file. Remove the `var` to share
|
||||
a value between files.
|
||||
|
||||
* Meteor now supports any x86 (32- or 64-bit) Linux system, not just those which
|
||||
use Debian or RedHat package management.
|
||||
|
||||
* Apps may contain packages inside a top-level directory named `packages`.
|
||||
|
||||
* Packages may depend on [NPM modules](https://npmjs.org), using the new
|
||||
`Npm.depends` directive in their `package.js` file. (Note: if the NPM module
|
||||
has architecture-specific binary components, bundles built with `meteor
|
||||
bundle` or `meteor deploy` will contain the components as built for the
|
||||
developer's platform and may not run on other platforms.)
|
||||
|
||||
* Meteor's internal package tests (as well as tests you add to your app's
|
||||
packages with the unsupported `Tinytest` framework) are now run with the new
|
||||
command `meteor test-packages`.
|
||||
|
||||
* `{{#each}}` helper can now iterate over falsey values without throwing an
|
||||
exception. #815, #801
|
||||
|
||||
* `{{#with}}` helper now only includes its block if its argument is not falsey,
|
||||
and runs an `{{else}}` block if provided if the argument is falsey. #770, #866
|
||||
|
||||
* Twitter login now stores profile_image_url and profile_image_url_https
|
||||
attributes in the user.services.twitter namespace. #788
|
||||
|
||||
* Allow packages to register file extensions with dots in the filename.
|
||||
|
||||
* When calling `this.changed` in a publish function, it is no longer an error to
|
||||
clear a field which was never set. #850
|
||||
|
||||
* Deps API
|
||||
* Add `dep.depend()`, deprecate `Deps.depend(dep)` and
|
||||
`dep.addDependent()`.
|
||||
* If first run of `Deps.autorun` throws an exception, stop it and don't
|
||||
rerun. This prevents a Spark exception when template rendering fails
|
||||
("Can't call 'firstNode' of undefined").
|
||||
* If an exception is thrown during `Deps.flush` with no stack, the
|
||||
message is logged instead. #822
|
||||
|
||||
* When connecting to MongoDB, use the JavaScript BSON parser unless specifically
|
||||
requested in `MONGO_URL`; the native BSON parser sometimes segfaults. (Meteor
|
||||
only started using the native parser in 0.5.8.)
|
||||
|
||||
* Calls to the `update` collection function in untrusted code may only use a
|
||||
whitelisted list of modifier operators.
|
||||
|
||||
Patches contributed by GitHub users awwx, blackcoat, cmather, estark37,
|
||||
mquandalle, Primigenus, raix, reustle, and timhaines.
|
||||
|
||||
|
||||
## v0.5.9
|
||||
|
||||
* Fix regression in 0.5.8 that prevented users from editing their own
|
||||
|
||||
30
README.md
30
README.md
@@ -44,7 +44,7 @@ with the provided script. If you do not run this script, Meteor will
|
||||
automatically download pre-compiled binaries when you first run it.
|
||||
|
||||
# OPTIONAL
|
||||
./admin/generate-dev-bundle.sh
|
||||
./scripts/generate-dev-bundle.sh
|
||||
|
||||
Now you can run meteor directly from the checkout (if you did not
|
||||
build the dependency bundle above, this will take a few moments to
|
||||
@@ -52,18 +52,26 @@ download a pre-build version).
|
||||
|
||||
./meteor --help
|
||||
|
||||
Or install to ```/usr/local``` like the normal install process. This
|
||||
will cause ```meteor``` to be in your ```PATH```.
|
||||
From your checkout, you can read the docs locally. The `/docs` directory is a
|
||||
meteor application, so simply change into the `/docs` directory and launch
|
||||
the app:
|
||||
|
||||
./install.sh
|
||||
meteor --help
|
||||
|
||||
After installing, you can read the docs locally. The ```/docs``` directory is a meteor application, so simply change into the ```/docs``` directory and launch the app:
|
||||
|
||||
cd docs/
|
||||
meteor
|
||||
|
||||
You'll then be able to read the docs locally in your browser at ```http://localhost:3000/```
|
||||
You'll then be able to read the docs locally in your browser at
|
||||
`http://localhost:3000/`
|
||||
|
||||
Note that if you run Meteor from a git checkout, you cannot pin apps to specific
|
||||
Meteor releases or run using different Meteor releases using `--release`.
|
||||
|
||||
## Uninstalling Meteor
|
||||
|
||||
Aside from a short launcher shell script, Meteor installs itself inside your
|
||||
home directory. To uninstall Meteor, run:
|
||||
|
||||
rm -rf ~/.meteor/
|
||||
sudo rm /usr/local/bin/meteor
|
||||
|
||||
## Developer Resources
|
||||
|
||||
@@ -72,9 +80,9 @@ Building an application with Meteor?
|
||||
* Announcement list: sign up at http://www.meteor.com/
|
||||
* Ask a question: http://stackoverflow.com/questions/tagged/meteor
|
||||
* Meteor help and discussion mailing list: https://groups.google.com/group/meteor-talk
|
||||
* IRC: ```#meteor``` on ```irc.freenode.net```
|
||||
* IRC: `#meteor` on `irc.freenode.net`
|
||||
|
||||
Interested in contributing to Meteor?
|
||||
|
||||
* Core framework design mailing list: https://groups.google.com/group/meteor-core
|
||||
* Contribution guidelines: https://github.com/meteor/meteor/wiki
|
||||
* Contribution guidelines: https://github.com/meteor/meteor/tree/devel/Contributing.md
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# cd to top level dir
|
||||
cd `dirname $0`
|
||||
cd ..
|
||||
TOPDIR=$(pwd)
|
||||
|
||||
UNAME=$(uname)
|
||||
ARCH=$(uname -m)
|
||||
|
||||
TMPDIR=$(mktemp -d -t meteor-build-release-XXXXXXXX)
|
||||
trap 'rm -rf "$TMPDIR" >/dev/null 2>&1' 0
|
||||
|
||||
# install it.
|
||||
echo "Installing."
|
||||
|
||||
export PREFIX="$TMPDIR/install"
|
||||
mkdir -p "$PREFIX"
|
||||
./install.sh
|
||||
|
||||
# get the version number.
|
||||
VERSION="$($PREFIX/bin/meteor --version | perl -pe 's/.+ ([^ \(]+)( \(.+\))*/$1/')"
|
||||
|
||||
# tar it up
|
||||
OUTDIR="$TOPDIR/dist"
|
||||
rm -rf "$OUTDIR"
|
||||
mkdir -p "$OUTDIR"
|
||||
TARBALL="$OUTDIR/meteor-package-${UNAME}-${ARCH}-${VERSION}.tar.gz"
|
||||
echo "Tarring to: $TARBALL"
|
||||
|
||||
tar -C "$PREFIX" --exclude .meteor/local -czf "$TARBALL" meteor
|
||||
|
||||
|
||||
if [ "$UNAME" == "Linux" ] ; then
|
||||
echo "Building debian package"
|
||||
DEBDIR="$TMPDIR/debian"
|
||||
mkdir "$DEBDIR"
|
||||
cd "$DEBDIR"
|
||||
cp "$TARBALL" "meteor_${VERSION}.orig.tar.gz"
|
||||
mkdir "meteor-${VERSION}"
|
||||
cd "meteor-${VERSION}"
|
||||
cp -r "$TOPDIR/admin/debian" .
|
||||
export TARBALL
|
||||
dpkg-buildpackage
|
||||
cp ../*.deb "$OUTDIR"
|
||||
|
||||
|
||||
echo "Building RPM"
|
||||
RPMDIR="$TMPDIR/rpm"
|
||||
mkdir $RPMDIR
|
||||
rpmbuild -bb --define="TARBALL $TARBALL" \
|
||||
--define="_topdir $RPMDIR" "$TOPDIR/admin/meteor.spec"
|
||||
cp $RPMDIR/RPMS/*/*.rpm "$OUTDIR"
|
||||
fi
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOTE: by default this tests the working copy, not the installed meteor.
|
||||
# To test the installed meteor, pass in --global
|
||||
|
||||
cd `dirname $0`
|
||||
METEOR=`pwd`/../meteor
|
||||
|
||||
if [ -z "$NODE" ]; then
|
||||
NODE=`pwd`/node.sh
|
||||
fi
|
||||
|
||||
#If this ever takes more options, use getopt
|
||||
if [ "$1" == "--global" ]; then
|
||||
METEOR=meteor
|
||||
fi
|
||||
|
||||
DIR=`mktemp -d -t meteor-cli-test-XXXXXXXX`
|
||||
trap 'echo FAILED ; rm -rf "$DIR" >/dev/null 2>&1' EXIT
|
||||
|
||||
cd "$DIR"
|
||||
set -e
|
||||
|
||||
## Begin actual tests
|
||||
|
||||
echo "... --help"
|
||||
|
||||
$METEOR --help | grep "List available" > /dev/null
|
||||
$METEOR run --help | grep "Port to listen" > /dev/null
|
||||
$METEOR create --help | grep "Make a subdirectory" > /dev/null
|
||||
$METEOR update --help | grep "Checks to see" > /dev/null
|
||||
$METEOR add --help | grep "Adds packages" > /dev/null
|
||||
$METEOR remove --help | grep "Removes a package" > /dev/null
|
||||
$METEOR list --help | grep "Without arguments" > /dev/null
|
||||
$METEOR bundle --help | grep "Package this project" > /dev/null
|
||||
$METEOR mongo --help | grep "Opens a Mongo" > /dev/null
|
||||
$METEOR deploy --help | grep "Deploys the project" > /dev/null
|
||||
$METEOR logs --help | grep "Retrieves the" > /dev/null
|
||||
$METEOR reset --help | grep "Reset the current" > /dev/null
|
||||
|
||||
echo "... not in dir"
|
||||
|
||||
$METEOR | grep "You're not in" > /dev/null
|
||||
$METEOR run | grep "You're not in" > /dev/null
|
||||
$METEOR add foo | grep "You're not in" > /dev/null
|
||||
$METEOR remove foo | grep "You're not in" > /dev/null
|
||||
$METEOR list --using | grep "You're not in" > /dev/null
|
||||
$METEOR bundle foo.tar.gz | grep "You're not in" > /dev/null
|
||||
$METEOR mongo | grep "You're not in" > /dev/null
|
||||
$METEOR deploy automated-test | grep "You're not in" > /dev/null
|
||||
$METEOR reset | grep "You're not in" > /dev/null
|
||||
|
||||
echo "... create"
|
||||
|
||||
DIR="skel with spaces"
|
||||
$METEOR create "$DIR"
|
||||
test -d "$DIR"
|
||||
test -f "$DIR/$DIR.js"
|
||||
|
||||
## Tests in a meteor project
|
||||
cd "$DIR"
|
||||
|
||||
echo "... add/remove/list"
|
||||
|
||||
$METEOR list | grep "backbone" > /dev/null
|
||||
! $METEOR list --using 2>&1 | grep "backbone" > /dev/null
|
||||
$METEOR add backbone 2>&1 | grep "backbone:" > /dev/null
|
||||
$METEOR list --using | grep "backbone" > /dev/null
|
||||
grep backbone .meteor/packages > /dev/null
|
||||
$METEOR remove backbone 2>&1 | grep "backbone: removed" > /dev/null
|
||||
! $METEOR list --using 2>&1 | grep "backbone" > /dev/null
|
||||
|
||||
echo "... bundle"
|
||||
|
||||
$METEOR bundle foo.tar.gz
|
||||
test -f foo.tar.gz
|
||||
|
||||
|
||||
echo "... run"
|
||||
|
||||
MONGOMARK='--bind_ip 127.0.0.1 --smallfiles --port 9102'
|
||||
# kill any old test meteor
|
||||
# there is probably a better way to do this, but it is at least portable across macos and linux
|
||||
# (the || true is needed on linux, whose xargs will invoke kill even with no args)
|
||||
ps ax | grep -e 'meteor.js -p 9100' | grep -v grep | awk '{print $1}' | xargs kill || true
|
||||
|
||||
! $METEOR mongo > /dev/null 2>&1
|
||||
$METEOR reset > /dev/null 2>&1
|
||||
|
||||
test ! -d .meteor/local
|
||||
! ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
|
||||
|
||||
PORT=9100
|
||||
$METEOR -p $PORT > /dev/null 2>&1 &
|
||||
METEOR_PID=$!
|
||||
|
||||
sleep 2 # XXX XXX lame
|
||||
|
||||
test -d .meteor/local/db
|
||||
ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
|
||||
curl -s "http://localhost:$PORT" > /dev/null
|
||||
|
||||
echo "show collections" | $METEOR mongo
|
||||
|
||||
# kill meteor, see mongo is still running
|
||||
kill $METEOR_PID
|
||||
|
||||
sleep 10 # XXX XXX lame. have to wait for inner app to die via keepalive!
|
||||
|
||||
! ps ax | grep "$METEOR_PID" | grep -v grep > /dev/null
|
||||
ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
|
||||
|
||||
|
||||
echo "... rerun"
|
||||
|
||||
$METEOR -p $PORT > /dev/null 2>&1 &
|
||||
METEOR_PID=$!
|
||||
|
||||
sleep 2 # XXX XXX lame
|
||||
|
||||
ps ax | grep -e "$MONGOMARK" | grep -v grep > /dev/null
|
||||
curl -s "http://localhost:$PORT" > /dev/null
|
||||
|
||||
kill $METEOR_PID
|
||||
sleep 10 # XXX XXX lame. have to wait for inner app to die via keepalive!
|
||||
|
||||
ps ax | grep -e "$MONGOMARK" | grep -v grep | awk '{print $1}' | xargs kill || true
|
||||
sleep 2 # need to make sure these kills take effect
|
||||
|
||||
echo "... mongo message"
|
||||
|
||||
# Run a server on the same port as mongod, so that mongod fails to start up. Rig
|
||||
# it so that a single connection will cause it to exit.
|
||||
$NODE -e 'require("net").createServer(function(){process.exit(0)}).listen('$PORT'+2, "127.0.0.1")' &
|
||||
|
||||
sleep 1
|
||||
|
||||
$METEOR -p $PORT > error.txt || true
|
||||
|
||||
grep 'port was closed' error.txt > /dev/null
|
||||
|
||||
# Kill the server by connecting to it.
|
||||
$NODE -e 'require("net").connect({host:"127.0.0.1",port:'$PORT'+2},function(){process.exit(0);})'
|
||||
|
||||
echo "... settings"
|
||||
|
||||
cat > settings.json <<EOF
|
||||
{ "foo" : "bar",
|
||||
"baz" : "quux"
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > settings.js <<EOF
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(function () {
|
||||
if (!Meteor.settings) process.exit(1);
|
||||
if (Meteor.settings.foo !== "bar") process.exit(1);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
EOF
|
||||
|
||||
$METEOR -p $PORT --settings='settings.json' --once > /dev/null
|
||||
|
||||
# XXX more tests here!
|
||||
|
||||
|
||||
|
||||
## Cleanup
|
||||
trap - EXIT
|
||||
rm -rf "$DIR"
|
||||
echo PASSED
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Requires s3cmd to be installed and an appropriate ~/.s3cfg.
|
||||
# Usage:
|
||||
# admin/copy-release-from-jenkins.sh [--prod] BUILDNUMBER
|
||||
# where BUILDNUMBER is the small integer Jenkins build number.
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
cd `dirname $0`
|
||||
|
||||
TARGET="s3://com.meteor.static/test/"
|
||||
TEST=no
|
||||
if [ $# -ge 1 -a $1 = '--prod' ]; then
|
||||
shift
|
||||
TARGET="s3://com.meteor.static/"
|
||||
else
|
||||
TEST=yes
|
||||
fi
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "usage: $0 [--prod] jenkins-build-number" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIRNAME=$(s3cmd ls s3://com.meteor.jenkins/ | perl -nle 'print $1 if m!/(release-.+--'$1'--.+)/!')
|
||||
|
||||
if [ -z "$DIRNAME" ]; then
|
||||
echo "build not found" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Found build $DIRNAME
|
||||
|
||||
# Check to make sure the proper number of each kind of file is there.
|
||||
s3cmd ls s3://com.meteor.jenkins/$DIRNAME/ | \
|
||||
perl -nle '++$RPM if /\.rpm/; ++$DEB if /\.deb/; ++$TAR if /\.tar\.gz/; ++$DIR if /DIR/; END { exit !($RPM == 2 && $DEB == 2 && $TAR == 3 && $DIR == 1) }'
|
||||
|
||||
echo Copying to $TARGET
|
||||
s3cmd -P cp -r s3://com.meteor.jenkins/$DIRNAME/ $TARGET
|
||||
|
||||
if [ $TEST = 'yes' ]; then
|
||||
echo Uploading modified install-s3.sh and manifest.json
|
||||
|
||||
OUTDIR=$(mktemp -dt meteor-crfj)
|
||||
perl -pe 's!https://d3sqy0vbqsdhku.cloudfront.net!https://s3.amazonaws.com/com.meteor.static/test!g' install-s3.sh >$OUTDIR/install-s3.sh
|
||||
perl -pe 's!https://d3sqy0vbqsdhku.cloudfront.net!https://s3.amazonaws.com/com.meteor.static/test!g' manifest.json >$OUTDIR/manifest.json
|
||||
|
||||
cd $OUTDIR
|
||||
s3cmd -P put install-s3.sh s3://com.meteor.static/test/update/
|
||||
s3cmd -P put manifest.json s3://com.meteor.static/test/update/
|
||||
fi
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# XXX
|
||||
echo "This script is currently broken and does not represent the new release procedure. Don't use it!"
|
||||
exit 1
|
||||
|
||||
# cd to top level dir
|
||||
cd `dirname $0`
|
||||
cd ..
|
||||
|
||||
# Check for MacOS
|
||||
if [ `uname` != "Darwin" ] ; then
|
||||
echo "Meteor release script must run on MacOS."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# make sure we're clean
|
||||
# http://stackoverflow.com/questions/2657935/checking-for-a-dirty-index-or-untracked-files-with-git
|
||||
function warn_and_exit { echo $1 ; return 1; }
|
||||
git diff-files --quiet || \
|
||||
warn_and_exit "Local changes. Aborting."
|
||||
git diff-index --quiet --cached HEAD || \
|
||||
warn_and_exit "Local changes staged. Aborting."
|
||||
test -z "$(git ls-files --others --exclude-standard)" || \
|
||||
warn_and_exit "Uncommitted files. Aborting."
|
||||
|
||||
for i in dev_bundle_*.tar.gz ; do
|
||||
test -f $i && warn_and_exit "Local dev_bundle tarball. Aborting."
|
||||
done
|
||||
|
||||
# Make sure we have an up to date dev bundle by re-downloading.
|
||||
if [ -d dev_bundle ] ; then
|
||||
echo "Removing old dev_bundle."
|
||||
rm -rf dev_bundle
|
||||
fi
|
||||
# Force dev_bundle re-creation
|
||||
./meteor --version || \
|
||||
warn_and_exit "dev_bundle installation failed."
|
||||
|
||||
|
||||
# increment the version number
|
||||
export NODE_PATH="$(pwd)/dev_bundle/lib/node_modules"
|
||||
./dev_bundle/bin/node admin/increment-version.js
|
||||
|
||||
|
||||
./admin/build-release.sh
|
||||
|
||||
# get the tarball. XXX copied from build-release.sh
|
||||
UNAME=$(uname)
|
||||
ARCH=$(uname -m)
|
||||
VERSION="$(/usr/local/bin/meteor --version | perl -pe 's/.+ ([^ \(]+)( \(.+\))*/$1/')"
|
||||
TARBALL=~/meteor-package-${UNAME}-${ARCH}-${VERSION}.tar.gz
|
||||
test -f "$TARBALL"
|
||||
|
||||
# commit to git
|
||||
echo
|
||||
echo "//////////////////////"
|
||||
echo "//////////////////////"
|
||||
git diff
|
||||
|
||||
echo
|
||||
echo "//////////////////////"
|
||||
echo "// Commit to git? Press enter to continue. Hit C-c to abort."
|
||||
read anykey
|
||||
|
||||
git commit -a -m "Bump to version $VERSION"
|
||||
git push origin master
|
||||
|
||||
git tag "v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
|
||||
|
||||
echo
|
||||
echo "//////////////////////"
|
||||
|
||||
# prompt are you sure
|
||||
echo "// Result tarball:"
|
||||
ls -l "$TARBALL"
|
||||
md5 "$TARBALL"
|
||||
|
||||
cat <<EOF
|
||||
/////////////////////
|
||||
|
||||
|
||||
/////////////////////
|
||||
// Push release $VERSION ? Press enter to continue. Hit C-c to abort.
|
||||
/////////////////////
|
||||
EOF
|
||||
read
|
||||
|
||||
s3cmd -P put "$TARBALL" s3://com.meteor.static
|
||||
s3cmd -P put ./admin/install-s3.sh s3://com.meteor.static/update/
|
||||
s3cmd -P put ./admin/manifest.json s3://com.meteor.static/update/
|
||||
|
||||
echo
|
||||
echo "//////////////////////"
|
||||
echo "// Pushed and live!"
|
||||
@@ -1 +0,0 @@
|
||||
usr/lib/meteor/bin/meteor usr/bin/meteor
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd $METEOR_HOME/examples;
|
||||
read -p "Prefix? " PREFIX;
|
||||
for EXAMPLE in leaderboard todos wordplay parties
|
||||
do
|
||||
cd $EXAMPLE;
|
||||
echo "meteor deploy $@ $PREFIX-$EXAMPLE;"
|
||||
meteor deploy $@ $PREFIX-$EXAMPLE;
|
||||
cd ..;
|
||||
done
|
||||
@@ -1,69 +0,0 @@
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var semver = require('semver');
|
||||
|
||||
|
||||
var optimist = require('optimist');
|
||||
|
||||
var updater = require(path.join(__dirname, '..', 'app', 'lib', 'updater.js'));
|
||||
var _ = require('underscore');
|
||||
|
||||
// What files to update. Relative to project root.
|
||||
var UPDATE_FILES = [path.join('app', 'lib', 'updater.js'),
|
||||
path.join('app', 'meteor', 'post-upgrade.js'),
|
||||
path.join('admin', 'install-s3.sh'),
|
||||
path.join('admin', 'debian', 'changelog'),
|
||||
path.join('admin', 'meteor.spec'),
|
||||
path.join('docs', 'client', 'docs.js'),
|
||||
path.join('docs', 'client', 'docs.html'),
|
||||
[path.join('admin', 'manifest.json'), 'g']];
|
||||
|
||||
// Files to update for dev_bundle
|
||||
var BUNDLE_FILES = [path.join('admin', 'generate-dev-bundle.sh'), 'meteor'];
|
||||
|
||||
|
||||
var opt = require('optimist')
|
||||
.alias('dev_bundle', 'd')
|
||||
.boolean('dev_bundle')
|
||||
.describe('dev_bundle', 'Update the dev_bundle version, not the main version.')
|
||||
.alias('new_version', 'n')
|
||||
.describe('new_version', 'A new version number. Default is to increment patch number.')
|
||||
.usage('Usage: $0 [options]')
|
||||
;
|
||||
var argv = opt.argv;
|
||||
if (argv.help) {
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var CURRENT_VERSION = updater.CURRENT_VERSION;
|
||||
var files = UPDATE_FILES;
|
||||
|
||||
if (argv.dev_bundle) {
|
||||
var version_path = path.join(__dirname, '..', 'meteor');
|
||||
var version_data = fs.readFileSync(version_path, 'utf8');
|
||||
var version_match = /BUNDLE_VERSION=([\d\.]+)/.exec(version_data);
|
||||
CURRENT_VERSION = version_match[1];
|
||||
files = BUNDLE_FILES;
|
||||
}
|
||||
|
||||
var NEW_VERSION = argv.new_version || semver.inc(CURRENT_VERSION, 'patch');
|
||||
|
||||
console.log("Updating from " + CURRENT_VERSION + " to " + NEW_VERSION);
|
||||
|
||||
_.each(files, function (file) {
|
||||
var flags = '';
|
||||
if (file instanceof Array) {
|
||||
flags = file[1];
|
||||
file = file[0];
|
||||
}
|
||||
var fp = path.join(__dirname, '..', file);
|
||||
var text = fs.readFileSync(fp, 'utf8');
|
||||
var new_text = text.replace(new RegExp(CURRENT_VERSION, flags), NEW_VERSION);
|
||||
fs.writeFileSync(fp, new_text, 'utf8');
|
||||
|
||||
console.log("updated file: " + fp);
|
||||
});
|
||||
|
||||
console.log("Complete");
|
||||
@@ -1,193 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
## NOTE sh NOT bash. This script should be POSIX sh only, since we don't
|
||||
## know what shell the user has. Debian uses 'dash' for 'sh', for
|
||||
## example.
|
||||
|
||||
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
|
||||
VERSION="0.5.9"
|
||||
PKGVERSION="${VERSION}-1"
|
||||
|
||||
UNAME=`uname`
|
||||
if [ "$UNAME" != "Linux" -a "$UNAME" != "Darwin" ] ; then
|
||||
echo "Sorry, this OS is not supported yet."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
trap "echo Installation failed." EXIT
|
||||
|
||||
if [ "$UNAME" = "Darwin" ] ; then
|
||||
### OSX ###
|
||||
|
||||
if [ "i386" != `uname -p` -o "1" != `sysctl -n hw.cpu64bit_capable 2>/dev/null || echo 0` ] ; then
|
||||
|
||||
# Can't just test uname -m = x86_64, because Snow Leopard can
|
||||
# return other values.
|
||||
echo "Only 64-bit Intel processors are supported at this time."
|
||||
exit 1
|
||||
fi
|
||||
ARCH="x86_64"
|
||||
|
||||
|
||||
URL="$URLBASE/meteor-package-$UNAME-$ARCH-$VERSION.tar.gz"
|
||||
TARGET="/usr/local/meteor"
|
||||
PARENT="/usr/local"
|
||||
|
||||
|
||||
if [ -e "$TARGET" ] ; then
|
||||
echo "Updating Meteor in $TARGET"
|
||||
else
|
||||
echo "Installing Meteor to $TARGET"
|
||||
fi
|
||||
|
||||
# if /usr/local doesn't exist or isn't writable, fix it with sudo.
|
||||
if [ ! -d "$PARENT" ] ; then
|
||||
echo
|
||||
echo "$PARENT does not exist. Creating it with 'sudo mkdir'."
|
||||
echo "This may prompt for your password."
|
||||
echo
|
||||
sudo /bin/mkdir "$PARENT"
|
||||
sudo /usr/bin/chgrp admin "$PARENT"
|
||||
sudo /bin/chmod 775 "$PARENT"
|
||||
elif [ ! -w "$PARENT" -o ! -w "$PARENT/bin" ] ; then
|
||||
echo
|
||||
echo "The install script needs to change the permissions on $PARENT so that"
|
||||
echo "administrators can write to it. This may prompt for your password."
|
||||
echo
|
||||
sudo /usr/bin/chgrp admin "$PARENT"
|
||||
sudo /bin/chmod g+rwx "$PARENT"
|
||||
if [ -d "$PARENT/bin" ] ; then
|
||||
sudo /usr/bin/chgrp admin "$PARENT/bin"
|
||||
sudo /bin/chmod g+rwx "$PARENT/bin"
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove old version
|
||||
if [ -e "$TARGET" ] ; then
|
||||
rm -rf "$TARGET"
|
||||
fi
|
||||
|
||||
# make sure target exists and is directory
|
||||
mkdir -p "$TARGET" || true
|
||||
if [ ! -d "$TARGET" -o ! -w "$TARGET" ] ; then
|
||||
echo "can't write to $TARGET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# download and untar
|
||||
echo "... downloading"
|
||||
curl --progress-bar $URL | tar -C "$PARENT" -xzf -
|
||||
|
||||
# add to $PATH
|
||||
mkdir -p "$PARENT/bin"
|
||||
rm -f "$PARENT/bin/meteor"
|
||||
ln -s "$TARGET/bin/meteor" "$PARENT/bin/meteor"
|
||||
|
||||
|
||||
elif [ "$UNAME" = "Linux" ] ; then
|
||||
### Linux ###
|
||||
ARCH=`uname -m`
|
||||
if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then
|
||||
echo "Unable to install Meteor on unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
download_url() {
|
||||
if [ -x "/usr/bin/curl" ] ; then
|
||||
/usr/bin/curl -# -O $1
|
||||
elif [ -x "/usr/bin/wget" ] ; then
|
||||
/usr/bin/wget -q $1
|
||||
else
|
||||
echo "Unable to install Meteor: can't find wget or curl in /usr/bin."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
do_with_root() {
|
||||
# already have root. just do it.
|
||||
if [ `whoami` = 'root' ] ; then
|
||||
$*
|
||||
elif [ -x /bin/sudo -o -x /usr/bin/sudo ] ; then
|
||||
echo
|
||||
echo "Since this system includes sudo, Meteor will request root privileges to"
|
||||
echo "install. You may be prompted for a password. If you prefer to not use"
|
||||
echo "sudo, please re-run this script as root."
|
||||
echo "sudo $*"
|
||||
sudo $*
|
||||
else
|
||||
echo "Meteor requires root privileges to install. Please re-run this script as"
|
||||
echo "root."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
TMPDIR=`mktemp -d -t meteor-install-XXXXXXX`
|
||||
cd "$TMPDIR"
|
||||
|
||||
if [ -f "/etc/debian_version" ] ; then
|
||||
## Debian
|
||||
echo "Detected a Debian system. Downloading install package."
|
||||
if [ "$ARCH" = "i686" ] ; then
|
||||
DEBARCH="i386"
|
||||
elif [ "$ARCH" = "x86_64" ] ; then
|
||||
DEBARCH="amd64"
|
||||
fi
|
||||
|
||||
FILE="meteor_${PKGVERSION}_${DEBARCH}.deb"
|
||||
URL="$URLBASE/$FILE"
|
||||
download_url $URL
|
||||
if [ ! -f "$FILE" ] ; then
|
||||
echo "Error: package download failed (no .deb file in $TMPDIR)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing $TMPDIR/$FILE"
|
||||
do_with_root dpkg -i "$FILE"
|
||||
|
||||
elif [ -f /etc/redhat_version -o -x /bin/rpm ] ; then
|
||||
## Redhat
|
||||
echo "Detected a RedHat system. Downloading install package."
|
||||
if [ "$ARCH" = "i686" ] ; then
|
||||
RPMARCH="i386"
|
||||
else
|
||||
RPMARCH="$ARCH"
|
||||
fi
|
||||
|
||||
FILE="meteor-${PKGVERSION}.${RPMARCH}.rpm"
|
||||
URL="$URLBASE/$FILE"
|
||||
download_url $URL
|
||||
if [ ! -f "$FILE" ] ; then
|
||||
echo "Error: package download failed (no .rpm file in $TMPDIR)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing $TMPDIR/$FILE"
|
||||
do_with_root rpm -U --force "$FILE"
|
||||
|
||||
else
|
||||
echo "Unable to install. Meteor supports RedHat and Debian."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd .. # get out of TMPDIR before we remove it.
|
||||
rm -rf "$TMPDIR"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Meteor installed! To get started fast:
|
||||
|
||||
$ meteor create ~/my_cool_app
|
||||
$ cd ~/my_cool_app
|
||||
$ meteor
|
||||
|
||||
Or see the docs at:
|
||||
|
||||
docs.meteor.com
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
trap - EXIT
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"version": "0.5.9",
|
||||
"deb_version": "0.5.9-1",
|
||||
"rpm_version": "0.5.9-1",
|
||||
"urlbase": "https://d3sqy0vbqsdhku.cloudfront.net"
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
var path = require('path');
|
||||
var _ = require('underscore');
|
||||
var files = require(path.join(__dirname, 'files.js'));
|
||||
var fs = require('fs');
|
||||
|
||||
// Under the hood, packages in the library (/package/foo), and user
|
||||
// applications, are both Packages -- they are just represented
|
||||
// differently on disk.
|
||||
//
|
||||
// To create a package object from a package in the library:
|
||||
// var pkg = new Package;
|
||||
// pkg.init_from_library(name);
|
||||
//
|
||||
// To create a package object from an app directory:
|
||||
// var pkg = new Package;
|
||||
// pkg.init_from_app_dir(app_dir);
|
||||
//
|
||||
// Or from a collection (a directory whose subdirs are packages):
|
||||
// var pkg = new Package;
|
||||
// pkg.init_from_collection(collection_dir);
|
||||
|
||||
var next_package_id = 1;
|
||||
var Package = function () {
|
||||
var self = this;
|
||||
|
||||
// Fields set by init_*:
|
||||
// name: package name, or null for an app pseudo-package or collection
|
||||
// source_root: base directory for resolving source files, null for collection
|
||||
// serve_root: base directory for serving files, null for collection
|
||||
|
||||
// A unique ID (guaranteed to not be reused in this process -- if
|
||||
// the package is reloaded, it will get a different id the second
|
||||
// time)
|
||||
self.id = next_package_id++;
|
||||
|
||||
// package metadata, from describe()
|
||||
self.metadata = {};
|
||||
|
||||
self.on_use_handler = null;
|
||||
self.on_test_handler = null;
|
||||
|
||||
// registered source file handlers
|
||||
self.extensions = {};
|
||||
|
||||
// functions that can be called when the package is scanned
|
||||
self.declarationFuncs = {
|
||||
// keys
|
||||
// - summary: for 'meteor list'
|
||||
// - internal: if true, hide in list
|
||||
// - environments: optional
|
||||
// (1) if present, if depended on in an environment not on this
|
||||
// list, then throw an error
|
||||
// (2) if present, these are also the environments that will be
|
||||
// used when an application uses the package (since it can't
|
||||
// specify environments.) if not present, apps will use
|
||||
// [''], which is suitable for a package that doesn't care
|
||||
// where it's loaded (like livedata.)
|
||||
describe: function (metadata) {
|
||||
_.extend(self.metadata, metadata);
|
||||
},
|
||||
|
||||
on_use: function (f) {
|
||||
if (self.on_use_handler)
|
||||
throw new Error("A package may have only one on_use handler");
|
||||
self.on_use_handler = f;
|
||||
},
|
||||
|
||||
on_test: function (f) {
|
||||
if (self.on_test_handler)
|
||||
throw new Error("A package may have only one on_test handler");
|
||||
self.on_test_handler = f;
|
||||
},
|
||||
|
||||
register_extension: function (extension, callback) {
|
||||
if (_.has(self.extensions, extension))
|
||||
throw new Error("This package has already registered a handler for " +
|
||||
extension);
|
||||
self.extensions[extension] = callback;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
_.extend(Package.prototype, {
|
||||
init_from_library: function (name) {
|
||||
var self = this;
|
||||
self.name = name;
|
||||
self.source_root = files.get_package_dir(name);
|
||||
self.serve_root = path.join(path.sep, 'packages', name);
|
||||
|
||||
if (!self.source_root)
|
||||
throw new Error("The package named " + self.name + " does not exist.");
|
||||
|
||||
var fullpath = path.join(self.source_root, 'package.js');
|
||||
var code = fs.readFileSync(fullpath).toString();
|
||||
// \n is necessary in case final line is a //-comment
|
||||
var wrapped = "(function(Package,require){" + code + "\n})";
|
||||
// XXX it'd be nice to runInNewContext so that the package
|
||||
// setup code can't mess with our globals, but objects that
|
||||
// come out of runInNewContext have bizarro antimatter
|
||||
// prototype chains and break 'instanceof Array'. for now,
|
||||
// steer clear
|
||||
var func = require('vm').runInThisContext(wrapped, fullpath, true);
|
||||
// XXX would be nice to eliminate require. packages like
|
||||
// 'templating' use this to load other code to run at
|
||||
// bundle-time. and to pull in, eg, 'fs' and 'path' to access
|
||||
// the file system
|
||||
func(self.declarationFuncs, require);
|
||||
},
|
||||
|
||||
init_from_app_dir: function (app_dir, ignore_files) {
|
||||
var self = this;
|
||||
self.name = null;
|
||||
self.source_root = app_dir;
|
||||
self.serve_root = path.sep;
|
||||
|
||||
var sources_except = function (api, except, tests) {
|
||||
return _(self._scan_for_sources(api, ignore_files || []))
|
||||
.reject(function (source_path) {
|
||||
return (path.sep + source_path + path.sep).indexOf(path.sep + except + path.sep) !== -1;
|
||||
})
|
||||
.filter(function (source_path) {
|
||||
var is_test = ((path.sep + source_path + path.sep).indexOf(path.sep + 'tests' + path.sep) !== -1);
|
||||
return is_test === (!!tests);
|
||||
});
|
||||
};
|
||||
|
||||
self.declarationFuncs.on_use(function (api) {
|
||||
// -- Packages --
|
||||
|
||||
// standard client packages (for now), for the classic meteor
|
||||
// stack -- has to come before user packages, because we don't
|
||||
// (presently) require packages to declare dependencies on
|
||||
// 'standard meteor stuff' like minimongo.
|
||||
api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'spark',
|
||||
'templating', 'startup', 'past']);
|
||||
api.use(require(path.join(__dirname, 'project.js')).get_packages(app_dir));
|
||||
|
||||
// -- Source files --
|
||||
api.add_files(sources_except(api, "server"), "client");
|
||||
api.add_files(sources_except(api, "client"), "server");
|
||||
});
|
||||
|
||||
self.declarationFuncs.on_test(function (api) {
|
||||
api.use(self);
|
||||
api.add_files(sources_except(api, "server", true), "client");
|
||||
api.add_files(sources_except(api, "client", true), "server");
|
||||
});
|
||||
},
|
||||
|
||||
// Find all files under this.source_root that have an extension we
|
||||
// recognize, and return them as a list of paths relative to
|
||||
// source_root. Ignore files that match a regexp in the ignore_files
|
||||
// array, if given. As a special case (ugh), push all html files to
|
||||
// the head of the list.
|
||||
_scan_for_sources: function (api, ignore_files) {
|
||||
var self = this;
|
||||
|
||||
// find everything in tree, sorted depth-first alphabetically.
|
||||
var file_list = files.file_list_sync(self.source_root,
|
||||
api.registered_extensions());
|
||||
file_list = _.reject(file_list, function (file) {
|
||||
return _.any(ignore_files || [], function (pattern) {
|
||||
return file.match(pattern);
|
||||
});
|
||||
});
|
||||
file_list.sort(files.sort);
|
||||
|
||||
// XXX HUGE HACK --
|
||||
// push html (template) files ahead of everything else. this is
|
||||
// important because the user wants to be able to say
|
||||
// Template.foo.events = { ... }
|
||||
//
|
||||
// maybe all of the templates should go in one file? packages
|
||||
// should probably have a way to request this treatment (load
|
||||
// order depedency tags?) .. who knows.
|
||||
var htmls = [];
|
||||
_.each(file_list, function (filename) {
|
||||
if (path.extname(filename) === '.html') {
|
||||
htmls.push(filename);
|
||||
file_list = _.reject(file_list, function (f) { return f === filename;});
|
||||
}
|
||||
});
|
||||
file_list = htmls.concat(file_list);
|
||||
|
||||
// now make everything relative to source_root
|
||||
var prefix = self.source_root;
|
||||
if (prefix[prefix.length - 1] !== path.sep)
|
||||
prefix += path.sep;
|
||||
|
||||
return file_list.map(function (abs) {
|
||||
if (path.relative(prefix, abs).match(/\.\./))
|
||||
// XXX audit to make sure it works in all possible symlink
|
||||
// scenarios
|
||||
throw new Error("internal error: source file outside of parent?");
|
||||
return abs.substr(prefix.length);
|
||||
});
|
||||
},
|
||||
|
||||
init_from_collection: function (collection_dir) {
|
||||
var self = this;
|
||||
self.name = null;
|
||||
self.source_root = null;
|
||||
self.serve_root = null;
|
||||
|
||||
self.declarationFuncs.on_test(function (api) {
|
||||
_.each(fs.readdirSync(collection_dir), function (name) {
|
||||
// only take things that are actually packages
|
||||
if (files.is_package_dir(path.join(collection_dir, name)))
|
||||
api.include_tests(name);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// in the future, this could be an on-disk cache that tracks mtimes.
|
||||
var package_cache = {};
|
||||
|
||||
var packages = module.exports = {
|
||||
// get a package by name. also maps package objects to themselves.
|
||||
get: function (name) {
|
||||
if (name instanceof Package)
|
||||
return name;
|
||||
if (!(name in package_cache)) {
|
||||
var pkg = new Package;
|
||||
pkg.init_from_library(name);
|
||||
package_cache[name] = pkg;
|
||||
}
|
||||
|
||||
return package_cache[name];
|
||||
},
|
||||
|
||||
// get a package that represents an app. (ignore_files is optional
|
||||
// and if given, it should be an array of regexps for filenames to
|
||||
// ignore when scanning for source files.)
|
||||
get_for_app: function (app_dir, ignore_files) {
|
||||
var pkg = new Package;
|
||||
pkg.init_from_app_dir(app_dir, ignore_files || []);
|
||||
return pkg;
|
||||
},
|
||||
|
||||
get_for_collection: function (collection_dir) {
|
||||
var pkg = new Package;
|
||||
pkg.init_from_collection(collection_dir);
|
||||
return pkg;
|
||||
},
|
||||
|
||||
// get a package that represents a particular directory on disk,
|
||||
// which might be an app, a package, or even a collection of
|
||||
// packages.
|
||||
get_for_dir: function (project_dir) {
|
||||
if (files.is_app_dir(project_dir))
|
||||
return packages.get_for_app(project_dir);
|
||||
else if (files.is_package_dir(project_dir))
|
||||
// this will need to change when packages are stored in more
|
||||
// than one place
|
||||
return packages.get(path.basename(project_dir));
|
||||
else if (files.is_package_collection_dir(project_dir))
|
||||
return packages.get_for_collection(project_dir);
|
||||
else
|
||||
throw new Error("Unknown project directory type");
|
||||
},
|
||||
|
||||
// force reload of all packages
|
||||
flush: function () {
|
||||
package_cache = {};
|
||||
},
|
||||
|
||||
// get all packages in the directory, in a map from package name to
|
||||
// a package object.
|
||||
list: function () {
|
||||
var ret = {};
|
||||
|
||||
_.each(files.get_package_dirs(), function(dir) {
|
||||
_.each(fs.readdirSync(dir), function (name) {
|
||||
// skip .meteor directory
|
||||
if (fs.existsSync(path.join(dir, name, 'package.js')))
|
||||
ret[name] = packages.get(name);
|
||||
});
|
||||
})
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
// returns a pretty list suitable for showing to the user. input is
|
||||
// a list of package objects, each of which must have a name (not be
|
||||
// an application package.)
|
||||
format_list: function (pkgs) {
|
||||
var longest = '';
|
||||
_.each(pkgs, function (pkg) {
|
||||
if (pkg.name.length > longest.length)
|
||||
longest = pkg.name;
|
||||
});
|
||||
var pad = longest.replace(/./g, ' ');
|
||||
// it'd be nice to read the actual terminal width, but I tried
|
||||
// several methods and none of them work (COLUMNS isn't set in
|
||||
// node's environment; `tput cols` returns a constant 80.) maybe
|
||||
// node is doing something weird with ptys.
|
||||
var width = 80;
|
||||
|
||||
var out = '';
|
||||
_.each(pkgs, function (pkg) {
|
||||
if (pkg.metadata.internal)
|
||||
return;
|
||||
var name = pkg.name + pad.substr(pkg.name.length);
|
||||
var summary = pkg.metadata.summary || 'No description';
|
||||
out += (name + " " +
|
||||
summary.substr(0, width - 2 - pad.length) + "\n");
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var _ = require('underscore');
|
||||
|
||||
var project = module.exports = {
|
||||
|
||||
_get_lines: function (app_dir) {
|
||||
var raw = fs.readFileSync(path.join(app_dir, '.meteor', 'packages'), 'utf8');
|
||||
var lines = raw.split(/\r*\n\r*/);
|
||||
|
||||
// strip blank lines at the end
|
||||
while (lines.length) {
|
||||
var line = lines[lines.length - 1];
|
||||
if (line.match(/\S/))
|
||||
break;
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
return lines;
|
||||
},
|
||||
|
||||
_trim_line: function (line) {
|
||||
var match = line.match(/^([^#]*)#/);
|
||||
if (match)
|
||||
line = match[1];
|
||||
line = line.replace(/^\s+|\s+$/g, ''); // leading/trailing whitespace
|
||||
return line;
|
||||
},
|
||||
|
||||
_write_packages: function (app_dir, lines) {
|
||||
fs.writeFileSync(path.join(app_dir, '.meteor', 'packages'),
|
||||
lines.join('\n') + '\n', 'utf8');
|
||||
},
|
||||
|
||||
// Packages used by this project.
|
||||
get_packages: function (app_dir) {
|
||||
var ret = [];
|
||||
|
||||
_.each(project._get_lines(app_dir), function (line) {
|
||||
line = project._trim_line(line);
|
||||
if (line !== '')
|
||||
ret.push(line);
|
||||
});
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
add_package: function (app_dir, name) {
|
||||
var lines = project._get_lines(app_dir);
|
||||
|
||||
// detail: if the file starts with a comment, try to keep a single
|
||||
// blank line after the comment (unless the user removes it)
|
||||
var current = project.get_packages(app_dir);
|
||||
if (!current.length && lines.length)
|
||||
lines.push('');
|
||||
lines.push(name);
|
||||
project._write_packages(app_dir, lines);
|
||||
},
|
||||
|
||||
remove_package: function (app_dir, name) {
|
||||
// XXX assume no special regexp characters
|
||||
var lines = _.reject(project._get_lines(app_dir), function (line) {
|
||||
return project._trim_line(line) === name;
|
||||
});
|
||||
project._write_packages(app_dir, lines);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
<!--
|
||||
body {
|
||||
background: #ffdddd;
|
||||
}
|
||||
.box {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
margin-left: -250px;
|
||||
margin-top: -150px;
|
||||
|
||||
border-width: 3px;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
|
||||
background: white;
|
||||
text-align: center;
|
||||
}
|
||||
.header {
|
||||
font-size: 2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px
|
||||
|
||||
}
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
-->
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="header">Browser not supported</div>
|
||||
<p>
|
||||
Sorry, this application does not support this web browser. Meteor
|
||||
supports modern versions of Safari, Firefox, Chrome, Opera, and
|
||||
Internet Explorer versions 6 and above.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,85 +0,0 @@
|
||||
// During automated QA of the updater, modify this file to set testingUpdater to
|
||||
// true. This will make it act as if it is at version 0.1.0 and use test URLs
|
||||
// for update checks.
|
||||
var testingUpdater = false;
|
||||
exports.CURRENT_VERSION = testingUpdater ? "0.1.0" : "0.5.9";
|
||||
|
||||
var fs = require("fs");
|
||||
var http = require("http");
|
||||
var https = require("https");
|
||||
var path = require("path");
|
||||
var semver = require("semver");
|
||||
|
||||
var files = require(path.join(__dirname, 'files.js'));
|
||||
|
||||
var manifest_options = testingUpdater ? {
|
||||
host: 's3.amazonaws.com',
|
||||
path: '/com.meteor.static/test/update/manifest.json'
|
||||
} : {
|
||||
host: 'update.meteor.com',
|
||||
path: '/manifest.json'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the current manifest file and returns it via a callback (or
|
||||
* null on error)
|
||||
*/
|
||||
exports.get_manifest = function (callback) {
|
||||
var req = https.request(manifest_options, function(res) {
|
||||
if (res.statusCode !== 200) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
res.setEncoding('utf8');
|
||||
var manifest = '';
|
||||
res.on('data', function (chunk) {
|
||||
manifest = manifest + chunk;
|
||||
});
|
||||
res.on('end', function () {
|
||||
var parsed;
|
||||
try {
|
||||
parsed = JSON.parse(manifest);
|
||||
} catch (err) {
|
||||
parsed = null;
|
||||
};
|
||||
callback(parsed);
|
||||
});
|
||||
});
|
||||
req.addListener('error', function (err) {
|
||||
// Need to register an error handler or node will crash:
|
||||
// http://rentzsch.tumblr.com/post/664884799/node-js-handling-refused-http-client-connections
|
||||
|
||||
callback(null);
|
||||
});
|
||||
req.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a version string (or a manifest object) and returns true if
|
||||
* this copy is out of date.
|
||||
*/
|
||||
exports.needs_upgrade = function (version) {
|
||||
if (version && typeof version !== "string") {
|
||||
version = version.version;
|
||||
}
|
||||
if (!version) return false;
|
||||
|
||||
return semver.lt(exports.CURRENT_VERSION, version);
|
||||
};
|
||||
|
||||
|
||||
exports.git_sha = function () {
|
||||
var d = files.get_dev_bundle();
|
||||
var f = path.join(d, ".git_version.txt");
|
||||
|
||||
if (fs.existsSync(f)) {
|
||||
try {
|
||||
var contents = fs.readFileSync(f, 'utf8');
|
||||
contents = contents.replace(/\s+$/, "");
|
||||
return contents;
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,669 +0,0 @@
|
||||
var Fiber = require('fibers');
|
||||
Fiber(function () {
|
||||
|
||||
var path = require('path');
|
||||
var files = require(path.join(__dirname, '..', 'lib', 'files.js'));
|
||||
var _ = require('underscore');
|
||||
var deploy = require(path.join(__dirname, 'deploy'));
|
||||
var fs = require("fs");
|
||||
var runner = require(path.join(__dirname, 'run.js'));
|
||||
|
||||
// This code is duplicated in app/server/server.js.
|
||||
var MIN_NODE_VERSION = 'v0.8.18';
|
||||
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
|
||||
process.stderr.write(
|
||||
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var usage = function() {
|
||||
process.stdout.write(
|
||||
"Usage: meteor [--version] [--help] <command> [<args>]\n" +
|
||||
"\n" +
|
||||
"With no arguments, 'meteor' runs the project in the current\n" +
|
||||
"directory in local development mode. You can run it from the root\n" +
|
||||
"directory of the project or from any subdirectory.\n" +
|
||||
"\n" +
|
||||
"Use 'meteor create <name>' to create a new Meteor project.\n" +
|
||||
"\n" +
|
||||
"Commands:\n");
|
||||
_.each(Commands, function (cmd) {
|
||||
if (cmd.help) {
|
||||
var name = cmd.name + " ".substr(cmd.name.length);
|
||||
process.stdout.write(" " + name + cmd.help + "\n");
|
||||
}
|
||||
});
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write(
|
||||
"See 'meteor help <command>' for details on a command.\n");
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
var require_project = function (cmd, accept_package) {
|
||||
var app_dir = files.find_upwards(files.is_app_dir);
|
||||
if (app_dir)
|
||||
return app_dir;
|
||||
|
||||
var package_dir = files.find_upwards(function (p) {
|
||||
return files.is_package_dir(p) || files.is_package_collection_dir(p);
|
||||
});
|
||||
if (package_dir) {
|
||||
if (accept_package)
|
||||
return package_dir;
|
||||
|
||||
process.stdout.write(cmd + ": Only works on applications, not packages\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// This is where you end up if you type 'meteor' with no
|
||||
// args. Be gentle to the noobs..
|
||||
process.stdout.write(
|
||||
cmd + ": You're not in a Meteor project directory.\n" +
|
||||
"\n" +
|
||||
"To create a new Meteor project:\n" +
|
||||
" meteor create <project name>\n" +
|
||||
"For example:\n" +
|
||||
" meteor create myapp\n" +
|
||||
"\n" +
|
||||
"For more help, see 'meteor --help'.\n");
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
var find_mongo_port = function (cmd, callback) {
|
||||
var app_dir = require_project(cmd);
|
||||
var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js'));
|
||||
mongo_runner.find_mongo_port(app_dir, callback);
|
||||
};
|
||||
|
||||
Commands = [];
|
||||
|
||||
var findCommand = function (name) {
|
||||
for (var i = 0; i < Commands.length; i++)
|
||||
if (Commands[i].name === name)
|
||||
return Commands[i];
|
||||
process.stdout.write("'" + name + "' is not a Meteor command. See " +
|
||||
"'meteor --help'.\n");
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
// XXX when the pass unexpected argument or unrecognized flags, print
|
||||
// an error and fail out
|
||||
|
||||
Commands.push({
|
||||
name: "run",
|
||||
help: "[default] Run this project in local development mode",
|
||||
func: function (argv) {
|
||||
// reparse args
|
||||
// This help logic should probably move to run.js eventually
|
||||
var opt = require('optimist')
|
||||
.alias('port', 'p').default('port', 3000)
|
||||
.describe('port', 'Port to listen on. NOTE: Also uses port N+1 and N+2.')
|
||||
.boolean('production')
|
||||
.describe('production', 'Run in production mode. Minify and bundle CSS and JS files.')
|
||||
.describe('settings', 'Set optional data for Meteor.settings on the server')
|
||||
// With --once, meteor does not re-run the project if it crashes and
|
||||
// does not monitor for file changes. Intentionally undocumented:
|
||||
// intended for automated testing (eg, cli-test.sh), not end-user
|
||||
// use.
|
||||
.boolean('once')
|
||||
.usage(
|
||||
"Usage: meteor run [options]\n" +
|
||||
"\n" +
|
||||
"Searches upward from the current directory for the root directory of a\n" +
|
||||
"Meteor project, then runs that project in local development\n" +
|
||||
"mode. You can use the application by pointing your web browser at\n" +
|
||||
"localhost:3000. No internet connection is required.\n" +
|
||||
"\n" +
|
||||
"Whenever you change any of the application's source files, the changes\n" +
|
||||
"are automatically detected and applied to the running application.\n" +
|
||||
"\n" +
|
||||
"The application's database persists between runs. It's stored under\n" +
|
||||
"the .meteor directory in the root of the project.\n");
|
||||
|
||||
var new_argv = opt.argv;
|
||||
var settings = "";
|
||||
|
||||
if (argv.help) {
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var app_dir = path.resolve(require_project("run", true)); // app or package
|
||||
|
||||
var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true };
|
||||
runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, new_argv.settings);
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "help",
|
||||
func: function (argv) {
|
||||
if (!argv._.length || argv.help)
|
||||
usage();
|
||||
var cmd = argv._.splice(0,1)[0];
|
||||
argv.help = true;
|
||||
findCommand(cmd).func(argv);
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "create",
|
||||
help: "Create a new project",
|
||||
func: function (argv) {
|
||||
// reparse args
|
||||
var opt = require('optimist')
|
||||
.describe('example', 'Example template to use.')
|
||||
.boolean('list')
|
||||
.describe('list', 'Show list of available examples.')
|
||||
.usage(
|
||||
"Usage: meteor create <name>\n" +
|
||||
" meteor create --example <example_name> [<name>]\n" +
|
||||
" meteor create --list\n" +
|
||||
"\n" +
|
||||
"Make a subdirectory named <name> and create a new Meteor project\n" +
|
||||
"there. You can also pass an absolute or relative path.\n" +
|
||||
"\n" +
|
||||
"You can pass --example to start off with a copy of one of the Meteor\n" +
|
||||
"sample applications. Use --list to see the available examples.");
|
||||
|
||||
var new_argv = opt.argv;
|
||||
var appname;
|
||||
|
||||
var example_dir = path.join(__dirname, '..', '..', 'examples');
|
||||
var examples = _.reject(fs.readdirSync(example_dir), function (e) {
|
||||
return (e === 'unfinished' || e === 'other' || e[0] === '.');
|
||||
});
|
||||
|
||||
if (argv._.length === 1) {
|
||||
appname = argv._[0];
|
||||
} else if (argv._.length === 0 && new_argv.example) {
|
||||
appname = new_argv.example;
|
||||
}
|
||||
|
||||
if (new_argv['list']) {
|
||||
process.stdout.write("Available examples:\n");
|
||||
_.each(examples, function (e) {
|
||||
process.stdout.write(" " + e + "\n");
|
||||
});
|
||||
process.stdout.write("\n" +
|
||||
"Create a project from an example with 'meteor create --example <name>'.\n")
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
if (argv.help || !appname) {
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (fs.existsSync(appname)) {
|
||||
process.stderr.write(appname + ": Already exists\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (files.find_app_dir(appname)) {
|
||||
process.stderr.write(
|
||||
"You can't create a Meteor project inside another Meteor project.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var transform = function (x) {
|
||||
return x.replace(/~name~/g, path.basename(appname));
|
||||
};
|
||||
|
||||
if (new_argv.example) {
|
||||
if (examples.indexOf(new_argv.example) === -1) {
|
||||
process.stderr.write(new_argv.example + ": no such example\n\n");
|
||||
process.stderr.write("List available applications with 'meteor create --list'.\n");
|
||||
process.exit(1);
|
||||
} else {
|
||||
files.cp_r(path.join(example_dir, new_argv.example), appname, {
|
||||
ignore: [/^local$/]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
files.cp_r(path.join(__dirname, 'skel'), appname, {
|
||||
transform_filename: function (f) {
|
||||
return transform(f);
|
||||
},
|
||||
transform_contents: function (contents, f) {
|
||||
if ((/(\.html|\.js|\.css)/).test(f))
|
||||
return new Buffer(transform(contents.toString()));
|
||||
else
|
||||
return contents;
|
||||
},
|
||||
ignore: [/^local$/]
|
||||
});
|
||||
}
|
||||
|
||||
process.stderr.write(appname + ": created");
|
||||
if (new_argv.example &&
|
||||
new_argv.example !== appname)
|
||||
process.stderr.write(" (from '" + new_argv.example + "' template)");
|
||||
process.stderr.write(".\n\n");
|
||||
|
||||
process.stderr.write(
|
||||
"To run your new app:\n" +
|
||||
" cd " + appname + "\n" +
|
||||
" meteor\n");
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "update",
|
||||
help: "Upgrade to the latest version of Meteor",
|
||||
func: function (argv) {
|
||||
if (argv.help) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor update\n" +
|
||||
"\n" +
|
||||
"Checks to see if a new version of Meteor is available, and if so,\n" +
|
||||
"downloads and installs it. You must be connected to the internet.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
require(path.join(__dirname, 'update.js'));
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "add",
|
||||
help: "Add a package to this project",
|
||||
func: function (argv) {
|
||||
if (argv.help || !argv._.length) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor add <package> [package] [package..]\n" +
|
||||
"\n" +
|
||||
"Adds packages to your Meteor project. You can add multiple\n" +
|
||||
"packages with one command. For a list of the available packages, see\n" +
|
||||
"'meteor list'.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var app_dir = require_project('add');
|
||||
var packages = require(path.join(__dirname, '..', 'lib', 'packages.js'));
|
||||
var project = require(path.join(__dirname, '..', 'lib', 'project.js'));
|
||||
var all = packages.list();
|
||||
var using = {};
|
||||
_.each(project.get_packages(app_dir), function (name) {
|
||||
using[name] = true;
|
||||
});
|
||||
|
||||
_.each(argv._, function (name) {
|
||||
if (!(name in all)) {
|
||||
process.stderr.write(name + ": no such package\n");
|
||||
} else if (name in using) {
|
||||
process.stderr.write(name + ": already using\n");
|
||||
} else {
|
||||
project.add_package(app_dir, name);
|
||||
var note = all[name].metadata.summary || '';
|
||||
process.stderr.write(name + ": " + note + "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "remove",
|
||||
help: "Remove a package from this project",
|
||||
func: function (argv) {
|
||||
if (argv.help || !argv._.length) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor remove <package> [package] [package..]\n" +
|
||||
"\n" +
|
||||
"Removes a package previously added to your Meteor project. For a\n" +
|
||||
"list of the packages that your application is currently using, see\n" +
|
||||
"'meteor list --using'.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var app_dir = require_project('remove');
|
||||
var packages = require(path.join(__dirname, '..', 'lib', 'packages.js'));
|
||||
var project = require(path.join(__dirname, '..', 'lib', 'project.js'));
|
||||
var using = {};
|
||||
_.each(project.get_packages(app_dir), function (name) {
|
||||
using[name] = true;
|
||||
});
|
||||
|
||||
_.each(argv._, function (name) {
|
||||
if (!(name in using)) {
|
||||
process.stderr.write(name + ": not in project\n");
|
||||
} else {
|
||||
project.remove_package(app_dir, name);
|
||||
process.stderr.write(name + ": removed\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "list",
|
||||
help: "List available packages",
|
||||
func: function (argv) {
|
||||
if (argv.help) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor list [--using]\n" +
|
||||
"\n" +
|
||||
"Without arguments, lists all available Meteor packages. To add one\n" +
|
||||
"of these packages to your project, see 'meteor add'.\n" +
|
||||
"\n" +
|
||||
"With --using, list the packages that you have added to your project.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (argv.using) {
|
||||
var app_dir = require_project('list --using');
|
||||
var using = require(path.join(__dirname, '..', 'lib', 'project.js')).get_packages(app_dir);
|
||||
|
||||
if (using.length) {
|
||||
_.each(using, function (name) {
|
||||
process.stdout.write(name + "\n");
|
||||
});
|
||||
} else {
|
||||
process.stderr.write(
|
||||
"This project doesn't use any packages yet. To add some packages:\n" +
|
||||
" meteor add <package> <package> ...\n" +
|
||||
"\n" +
|
||||
"To see available packages:\n" +
|
||||
" meteor list\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var list = require(path.join(__dirname, '..', 'lib', 'packages.js')).list();
|
||||
var names = _.keys(list);
|
||||
names.sort();
|
||||
var pkgs = [];
|
||||
_.each(names, function (name) {
|
||||
pkgs.push(list[name]);
|
||||
});
|
||||
process.stdout.write("\n" +
|
||||
require(path.join(__dirname, '..', 'lib', 'packages.js')).format_list(pkgs) +
|
||||
"\n");
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "bundle",
|
||||
help: "Pack this project up into a tarball",
|
||||
func: function (argv) {
|
||||
if (argv.help || argv._.length != 1) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor bundle <output_file.tar.gz>\n" +
|
||||
"\n" +
|
||||
"Package this project up for deployment. The output is a tarball that\n" +
|
||||
"includes everything necessary to run the application. See README in\n" +
|
||||
"the tarball for details.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// XXX if they pass a file that doesn't end in .tar.gz or .tgz,
|
||||
// add the former for them
|
||||
|
||||
// XXX output, to stderr, the name of the file written to (for
|
||||
// human comfort, especially since we might change the name)
|
||||
|
||||
// XXX name the root directory in the bundle based on the basename
|
||||
// of the file, not a constant 'bundle' (a bit obnoxious for
|
||||
// machines, but worth it for humans)
|
||||
|
||||
var app_dir = path.resolve(require_project("bundle"));
|
||||
var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar');
|
||||
var bundle_path = path.join(build_dir, 'bundle');
|
||||
var output_path = path.resolve(argv._[0]); // get absolute path
|
||||
|
||||
var bundler = require(path.join(__dirname, '..', 'lib', 'bundler.js'));
|
||||
var errors = bundler.bundle(app_dir, bundle_path);
|
||||
if (errors) {
|
||||
process.stdout.write("Errors prevented bundling:\n");
|
||||
_.each(errors, function (e) {
|
||||
process.stdout.write(e + "\n");
|
||||
});
|
||||
files.rm_recursive(build_dir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var cp = require('child_process');
|
||||
cp.execFile('/usr/bin/env',
|
||||
['tar', 'czf', output_path, 'bundle'],
|
||||
{cwd: build_dir},
|
||||
function (error, stdout, stderr) {
|
||||
if (error !== null) {
|
||||
console.log(JSON.stringify(error));
|
||||
process.stderr.write("couldn't run tar\n");
|
||||
} else {
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
files.rm_recursive(build_dir);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "mongo",
|
||||
help: "Connect to the Mongo database for the specified site",
|
||||
func: function (argv) {
|
||||
var opt = require('optimist')
|
||||
.boolean('url')
|
||||
.boolean('U')
|
||||
.alias('url', 'U')
|
||||
.describe('url', 'return a Mongo database URL')
|
||||
.usage(
|
||||
"Usage: meteor mongo [--url] [site]\n" +
|
||||
"\n" +
|
||||
"Opens a Mongo shell to view or manipulate collections.\n" +
|
||||
"\n" +
|
||||
"If site is specified, this is the hosted Mongo database for the deployed\n" +
|
||||
"Meteor site.\n" +
|
||||
"\n" +
|
||||
"If no site is specified, this is the current project's local development\n" +
|
||||
"database. In this case, the current working directory must be a\n" +
|
||||
"Meteor project directory, and the Meteor application must already be\n" +
|
||||
"running.\n" +
|
||||
"\n" +
|
||||
"Instead of opening a shell, specifying --url (-U) will return a URL\n" +
|
||||
"suitable for an external program to connect to the database. For remote\n" +
|
||||
"databases on deployed applications, the URL is valid for one minute.\n"
|
||||
);
|
||||
|
||||
if (argv.help) {
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var new_argv = opt.argv;
|
||||
|
||||
if (new_argv._.length === 1) {
|
||||
// localhost mode
|
||||
find_mongo_port("mongo", function (mongod_port) {
|
||||
if (!mongod_port) {
|
||||
process.stdout.write(
|
||||
"mongo: Meteor isn't running.\n" +
|
||||
"\n" +
|
||||
"This command only works while Meteor is running your application\n" +
|
||||
"locally. Start your application first.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var mongo_url = "mongodb://127.0.0.1:" + mongod_port + "/meteor";
|
||||
|
||||
if (new_argv.url)
|
||||
console.log(mongo_url);
|
||||
else
|
||||
deploy.run_mongo_shell(mongo_url);
|
||||
});
|
||||
|
||||
} else if (new_argv._.length === 2) {
|
||||
// remote mode
|
||||
deploy.mongo(new_argv._[1], new_argv.url);
|
||||
|
||||
} else {
|
||||
// usage
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "deploy",
|
||||
help: "Deploy this project to Meteor",
|
||||
func: function (argv) {
|
||||
var opt = require('optimist')
|
||||
.alias('password', 'P')
|
||||
.boolean('password')
|
||||
.boolean('P')
|
||||
.describe('password', 'set a password for this deployment')
|
||||
.alias('delete', 'D')
|
||||
.boolean('delete')
|
||||
.boolean('D')
|
||||
.describe('delete', "permanently delete this deployment")
|
||||
.boolean('debug')
|
||||
.describe('debug', 'deploy in debug mode (don\'t minify, etc)')
|
||||
.boolean('tests')
|
||||
.describe('settings', 'set optional data for Meteor.settings')
|
||||
// .describe('tests', 'deploy the tests instead of the actual application')
|
||||
.usage(
|
||||
// XXX document --tests in the future, once we publicly
|
||||
// support tests
|
||||
"Usage: meteor deploy <site> [--password] [--settings settings.json] [--debug] [--delete]\n" +
|
||||
"\n" +
|
||||
"Deploys the project in your current directory to Meteor's servers.\n" +
|
||||
"\n" +
|
||||
"You can deploy to any available name under 'meteor.com'\n" +
|
||||
"without any additional configuration, for example,\n" +
|
||||
"'myapp.meteor.com'. If you deploy to a custom domain, such as\n" +
|
||||
"'myapp.mydomain.com', then you'll also need to configure your domain's\n" +
|
||||
"DNS records. See the Meteor docs for details.\n" +
|
||||
"\n" +
|
||||
"The --settings flag can be used to pass deploy-specific information to\n" +
|
||||
"the application. It will be available at runtime in Meteor.settings, but only\n" +
|
||||
"on the server. If the object contains a key named 'public', then\n" +
|
||||
"Meteor.settings.public will also be available on the client. The argument\n" +
|
||||
"is the name of a file containing the JSON data to use. The settings will\n" +
|
||||
"persist across deployments until you again specify a settings file. To\n" +
|
||||
"unset Meteor.settings, pass an empty settings file.\n" +
|
||||
"\n" +
|
||||
"The --delete flag permanently removes a deployed application, including\n" +
|
||||
"all of its stored data.\n" +
|
||||
"\n" +
|
||||
"The --password flag sets an administrative password for the domain. Once\n" +
|
||||
"set, any subsequent 'deploy', 'logs', or 'mongo' command will prompt for\n" +
|
||||
"the password. You can change the password with a second 'deploy' command."
|
||||
);
|
||||
|
||||
var new_argv = opt.argv;
|
||||
|
||||
if (argv.help || new_argv._.length != 2) {
|
||||
process.stdout.write(opt.help());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (new_argv.delete) {
|
||||
deploy.delete_app(new_argv._[1]);
|
||||
} else {
|
||||
var settings = undefined;
|
||||
if (new_argv.settings)
|
||||
settings = runner.getSettings(new_argv.settings);
|
||||
// accept packages iff we're deploying tests
|
||||
var project_dir = path.resolve(require_project("bundle", new_argv.tests));
|
||||
deploy.deploy_app(new_argv._[1], project_dir, new_argv.debug,
|
||||
new_argv.tests, new_argv.password, settings);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "logs",
|
||||
help: "Show logs for specified site",
|
||||
func: function (argv) {
|
||||
if (argv.help || argv._.length < 1 || argv._.length > 2) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor logs <site>\n" +
|
||||
"\n" +
|
||||
"Retrieves the server logs for the requested site.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
deploy.logs(argv._[0]);
|
||||
}
|
||||
});
|
||||
|
||||
Commands.push({
|
||||
name: "reset",
|
||||
help: "Reset the project state. Erases the local database.",
|
||||
func: function (argv) {
|
||||
if (argv.help) {
|
||||
process.stdout.write(
|
||||
"Usage: meteor reset\n" +
|
||||
"\n" +
|
||||
"Reset the current project to a fresh state. Removes all local\n" +
|
||||
"data and kills any running meteor development servers.\n");
|
||||
process.exit(1);
|
||||
} else if (!_.isEmpty(argv._)) {
|
||||
process.stdout.write("meteor reset only affects the locally stored database.\n\n" +
|
||||
"To reset a deployed application use\nmeteor deploy --delete appname\n" +
|
||||
"followed by\nmeteor deploy appname\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var app_dir = path.resolve(require_project("reset"));
|
||||
|
||||
find_mongo_port("reset", function (mongod_port) {
|
||||
if (mongod_port) {
|
||||
process.stdout.write(
|
||||
"reset: Meteor is running.\n" +
|
||||
"\n" +
|
||||
"This command does not work while Meteor is running your application.\n" +
|
||||
"Exit the running meteor development server.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var local_dir = path.join(app_dir, '.meteor', 'local');
|
||||
files.rm_recursive(local_dir);
|
||||
|
||||
process.stdout.write("Project reset.\n");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var main = function() {
|
||||
var optimist = require('optimist')
|
||||
.alias("h", "help")
|
||||
.boolean("h")
|
||||
.boolean("help")
|
||||
.boolean("version")
|
||||
.boolean("debug");
|
||||
|
||||
var argv = optimist.argv;
|
||||
|
||||
if (argv.help) {
|
||||
argv._.splice(0, 0, "help");
|
||||
delete argv.help;
|
||||
}
|
||||
|
||||
if (argv.version) {
|
||||
var updater = require(path.join(__dirname, '..', 'lib', 'updater.js'));
|
||||
var sha = updater.git_sha();
|
||||
|
||||
process.stdout.write("Meteor version " + updater.CURRENT_VERSION);
|
||||
|
||||
if (files.in_checkout())
|
||||
process.stdout.write(" (git checkout)");
|
||||
else if (sha)
|
||||
process.stdout.write(" (" + sha.substr(0, 10) + ")");
|
||||
|
||||
process.stdout.write("\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
var cmd = 'run';
|
||||
if (argv._.length)
|
||||
cmd = argv._.splice(0,1)[0];
|
||||
|
||||
findCommand(cmd).func(argv);
|
||||
};
|
||||
|
||||
main();
|
||||
}).run();
|
||||
@@ -1,34 +0,0 @@
|
||||
try {
|
||||
// XXX can't get this from updater.js because in 0.3.7 and before the
|
||||
// updater didn't have the right NODE_PATH set. At some point we can
|
||||
// remove this and just use updater.CURRENT_VERSION.
|
||||
var VERSION = "0.5.9";
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var files = require(path.join(__dirname, "..", "lib", "files.js"));
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var topDir = files.get_dev_bundle();
|
||||
var changelogPath = path.join(topDir, 'History.md');
|
||||
|
||||
if (fs.existsSync(changelogPath)) {
|
||||
var changelogData = fs.readFileSync(changelogPath, 'utf8');
|
||||
var changelogSections = changelogData.split(/\n\#\#/);
|
||||
|
||||
_.each(changelogSections, function (section) {
|
||||
var m = /^\s*v([^\s]+)/.exec(section);
|
||||
if (m && m[1] === VERSION) {
|
||||
section = section.replace(/^\s+/, '').replace(/\s+$/, '');
|
||||
console.log();
|
||||
console.log(section);
|
||||
console.log();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// don't print a weird error message if something goes wrong.
|
||||
}
|
||||
|
||||
console.log("Upgrade complete.");
|
||||
@@ -1,252 +0,0 @@
|
||||
var fs = require("fs");
|
||||
var https = require("https");
|
||||
var os = require("os");
|
||||
var path = require("path");
|
||||
var spawn = require('child_process').spawn;
|
||||
var url = require("url");
|
||||
|
||||
var ProgressBar = require('progress');
|
||||
|
||||
var updater = require(path.join(__dirname, "..", "lib", "updater.js"));
|
||||
var files = require(path.join(__dirname, "..", "lib", "files.js"));
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
// refuse to update if we're in a git checkout.
|
||||
if (files.in_checkout()) {
|
||||
console.log("Your Meteor installation is a git checkout. Update it " +
|
||||
"manually with 'git pull'.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Immediately kick off manifest check.
|
||||
updater.get_manifest(function (manifest) {
|
||||
|
||||
//// Examine manifest and see if we need to upgrade.
|
||||
|
||||
if (!manifest || !manifest.version || !manifest.urlbase) {
|
||||
console.log("Failed to download manifest.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updater.needs_upgrade(manifest)) {
|
||||
if (manifest.version === updater.CURRENT_VERSION) {
|
||||
console.log("Already at current version: " + manifest.version);
|
||||
} else {
|
||||
console.log("Not upgrading. Your version: " + updater.CURRENT_VERSION
|
||||
+ ". New version: " + manifest.version + ".");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("New version available: " + manifest.version);
|
||||
|
||||
//// Setup post-upgrade function so we can call it later
|
||||
var post_remove_directories = [];
|
||||
var cleanup_temp_dirs = function () {
|
||||
_.each(post_remove_directories, files.rm_recursive);
|
||||
post_remove_directories = [];
|
||||
};
|
||||
|
||||
var run_post_upgrade = function () {
|
||||
cleanup_temp_dirs();
|
||||
|
||||
// Launch post-upgrade script
|
||||
var nodejs_path = path.join(files.get_dev_bundle(), 'bin', 'node');
|
||||
var postup_path = path.join(files.get_core_dir(), 'meteor', 'post-upgrade.js');
|
||||
|
||||
if (fs.existsSync(nodejs_path) && fs.existsSync(postup_path)) {
|
||||
// setup environment.
|
||||
var modules_path = path.join(files.get_dev_bundle(), 'lib', 'node_modules');
|
||||
var env = _.extend({}, process.env);
|
||||
env.NODE_PATH = modules_path;
|
||||
|
||||
// launch it.
|
||||
var postup_proc = spawn(nodejs_path, [postup_path], {env: env});
|
||||
postup_proc.stderr.setEncoding('utf8');
|
||||
postup_proc.stderr.on('data', function (data) {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
postup_proc.stdout.setEncoding('utf8');
|
||||
postup_proc.stdout.on('data', function (data) {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
} else {
|
||||
// no postup. Still print a message, but one that is subtly
|
||||
// different so developers can debug what is going on.
|
||||
console.log("upgrade complete.");
|
||||
}
|
||||
};
|
||||
|
||||
var run_with_root = function (cmd, args) {
|
||||
if (0 === process.getuid()) {
|
||||
// already root. just spawn the command.
|
||||
return spawn(cmd, args);
|
||||
} else if (fs.existsSync("/bin/sudo") ||
|
||||
fs.existsSync("/usr/bin/sudo")) {
|
||||
// spawn a sudo
|
||||
console.log("Since this system includes sudo, Meteor will request root privileges to");
|
||||
console.log("install. You may be prompted for a password. If you prefer to not use");
|
||||
console.log("sudo, please re-run this command as root.");
|
||||
console.log("sudo", cmd, args.join(" "));
|
||||
return spawn('sudo', [cmd].concat(args));
|
||||
}
|
||||
|
||||
// no root, no sudo. fail
|
||||
console.log("Meteor requires root privileges to install. Please re-run this command");
|
||||
console.log("as root.");
|
||||
process.exit(1);
|
||||
return null; // unreached, but makes js2 mode happy.
|
||||
};
|
||||
|
||||
|
||||
//// Figure out what platform we're upgrading on (dpkg, rpm, tar)
|
||||
|
||||
var package_stamp_path = path.join(files.get_dev_bundle(), '.package_stamp');
|
||||
var package_stamp;
|
||||
try {
|
||||
package_stamp = fs.readFileSync(package_stamp_path, 'utf8');
|
||||
package_stamp = package_stamp.replace(/^\s+|\s+$/g, '');
|
||||
} catch (err) {
|
||||
// no package stamp, assume tarball.
|
||||
package_stamp = 'tar';
|
||||
}
|
||||
|
||||
var download_url; // url to download
|
||||
var download_callback; // callback to call with path on disk of download.
|
||||
|
||||
var arch = os.arch();
|
||||
var deb_arch;
|
||||
var rpm_arch;
|
||||
if ("ia32" == arch) {
|
||||
deb_arch = "i386";
|
||||
rpm_arch = "i386";
|
||||
arch = "i686";
|
||||
} else if ("x64" == arch) {
|
||||
deb_arch = "amd64";
|
||||
rpm_arch = "x86_64";
|
||||
arch = "x86_64";
|
||||
} else {
|
||||
console.log("Unsupported architecture", arch);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('deb' === package_stamp) {
|
||||
download_url =
|
||||
manifest.urlbase + "/meteor_" + manifest.deb_version +
|
||||
"_" + deb_arch + ".deb";
|
||||
|
||||
download_callback = function (deb_path) {
|
||||
var proc = run_with_root('dpkg', ['-i', deb_path]);
|
||||
proc.on('exit', function (code, signal) {
|
||||
if (code !== 0 || signal) {
|
||||
console.log("failed to install deb");
|
||||
return;
|
||||
}
|
||||
// success!
|
||||
run_post_upgrade();
|
||||
});
|
||||
};
|
||||
|
||||
} else if ('rpm' === package_stamp) {
|
||||
download_url =
|
||||
manifest.urlbase + "/meteor-" + manifest.rpm_version +
|
||||
"." + rpm_arch + ".rpm";
|
||||
|
||||
download_callback = function (rpm_path) {
|
||||
var proc = run_with_root('rpm', ['-U', '--force', rpm_path]);
|
||||
proc.on('exit', function (code, signal) {
|
||||
if (code !== 0 || signal) {
|
||||
console.log("Error: failed to install Meteor RPM package.");
|
||||
return;
|
||||
}
|
||||
// success!
|
||||
run_post_upgrade();
|
||||
});
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
download_url =
|
||||
manifest.urlbase + "/meteor-package-" + os.type() +
|
||||
"-" + arch + "-" + manifest.version + ".tar.gz";
|
||||
|
||||
download_callback = function (tar_path) {
|
||||
var base_dir = path.join(__dirname, "..", "..");
|
||||
var tmp_dir = path.join(base_dir, "tmp");
|
||||
// XXX error check!
|
||||
try { fs.mkdirSync(tmp_dir, 0755); } catch (err) { }
|
||||
|
||||
// open pipe to tar
|
||||
var tar_proc = spawn("tar", ["-C", tmp_dir, "-xzf", tar_path]);
|
||||
|
||||
tar_proc.stderr.setEncoding('utf8');
|
||||
tar_proc.stderr.on('data', function (data) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
tar_proc.on('exit', function (code, signal) {
|
||||
if (code !== 0 || signal) {
|
||||
console.log("Error: package download failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// untar complete. swap directories
|
||||
var old_base_dir = base_dir + ".old";
|
||||
if (fs.existsSync(old_base_dir))
|
||||
files.rm_recursive(old_base_dir); // rm -rf !!
|
||||
|
||||
fs.renameSync(base_dir, old_base_dir);
|
||||
fs.renameSync(path.join(old_base_dir, "tmp", "meteor"), base_dir);
|
||||
|
||||
// success!
|
||||
run_post_upgrade();
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
//// Kick off download
|
||||
|
||||
var download_parsed = url.parse(download_url);
|
||||
// XXX why is node's API for 'url' different from 'http'?
|
||||
download_parsed.path = download_parsed.pathname;
|
||||
|
||||
var req = https.request(download_parsed, function(res) {
|
||||
if (res.statusCode !== 200) {
|
||||
console.log("Failed to download: " + download_url);
|
||||
return;
|
||||
}
|
||||
var len = parseInt(res.headers['content-length'], 10);
|
||||
|
||||
var bar = new ProgressBar(' downloading [:bar] :percent', {
|
||||
complete: '='
|
||||
, incomplete: ' '
|
||||
, width: 30
|
||||
, total: len
|
||||
});
|
||||
|
||||
// find / make directory paths
|
||||
var tmp_dir = files.mkdtemp();
|
||||
post_remove_directories.push(tmp_dir);
|
||||
|
||||
// open tempfile
|
||||
var download_path = path.join(tmp_dir, path.basename(download_url));
|
||||
var download_stream = fs.createWriteStream(download_path);
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
download_stream.write(chunk);
|
||||
bar.tick(chunk.length);
|
||||
});
|
||||
|
||||
res.on('end', function () {
|
||||
download_stream.end();
|
||||
console.log("... finished download");
|
||||
download_callback(download_path);
|
||||
// don't remove temp dir here, download_callback is probably still
|
||||
// using it.
|
||||
});
|
||||
});
|
||||
req.end();
|
||||
|
||||
});
|
||||
1
docs/.meteor/release
Normal file
1
docs/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
@@ -16,8 +16,14 @@ on the client, just on the server, or *Anywhere*.
|
||||
|
||||
On a server, the function will run as soon as the server process is
|
||||
finished starting. On a client, the function will run as soon as the DOM
|
||||
is ready and any `<body>` templates from your `.html` files have been
|
||||
put on the screen.
|
||||
is ready.
|
||||
|
||||
The `startup` callbacks are called in the same order as the calls to
|
||||
`Meteor.startup` were made.
|
||||
|
||||
On a client, `startup` callbacks from smart packages will be called
|
||||
first, followed by `<body>` templates from your `.html` files,
|
||||
followed by your application code.
|
||||
|
||||
// On server startup, if the database is empty, create some initial data.
|
||||
if (Meteor.isServer) {
|
||||
@@ -32,6 +38,8 @@ put on the screen.
|
||||
|
||||
{{> api_box settings}}
|
||||
|
||||
{{> api_box release}}
|
||||
|
||||
<h2 id="publishandsubscribe"><span>Publish and subscribe</span></h2>
|
||||
|
||||
These functions control how Meteor servers publish sets of records and
|
||||
@@ -52,7 +60,6 @@ cursors.
|
||||
{{#warning}}
|
||||
If you return multiple cursors in an array, they currently must all be from
|
||||
different collections. We hope to lift this restriction in a future release.
|
||||
cursors.
|
||||
{{/warning}}
|
||||
|
||||
// server: publish the rooms collection, minus secret info.
|
||||
@@ -831,7 +838,7 @@ current version of the document from the database, without the
|
||||
proposed update.) Return `true` to permit the change.
|
||||
|
||||
`fieldNames` is an array of the (top-level) fields in `doc` that the
|
||||
client wants to mody, for example
|
||||
client wants to modify, for example
|
||||
`['name',` `'score']`. `modifier` is the raw Mongo modifier that
|
||||
the client wants to execute, for example `{$set: {'name.first':
|
||||
"Alice"}, $inc: {score: 1}}`.
|
||||
@@ -1806,12 +1813,6 @@ new event handlers in addition to the existing ones.
|
||||
See [Event Maps](#eventmaps) for a detailed description of the event
|
||||
map format and how event handling works in Meteor.
|
||||
|
||||
{{#note}}
|
||||
This syntax replaces the previous syntax: `Template.myTemplate.events = {...}`,
|
||||
but for now, the old syntax still works.
|
||||
{{/note}}
|
||||
|
||||
|
||||
{{> api_box template_helpers}}
|
||||
|
||||
Each template has a local dictionary of helpers that are made available to it,
|
||||
@@ -2356,6 +2357,9 @@ computation that is rerun, allowing new ones to be established. See
|
||||
[`Meteor.subscribe`](#meteor_subscribe) for more information about
|
||||
subscriptions and reactivity.
|
||||
|
||||
If the initial run of an autorun throws an exception, the computation
|
||||
is automatically stopped and won't be rerun.
|
||||
|
||||
{{> api_box deps_flush }}
|
||||
|
||||
Normally, when you make changes (like writing to the database),
|
||||
@@ -2392,7 +2396,7 @@ computation.
|
||||
{{> api_box deps_nonreactive }}
|
||||
|
||||
Calls `func()` with `Deps.currentComputation` temporarily set to
|
||||
`null`. If `func` accesses reactive data sources, these data sources
|
||||
`null`. If `func` accesses reactive data sources, these data sources
|
||||
will never cause a rerun of the enclosing computation.
|
||||
|
||||
{{> api_box deps_active }}
|
||||
@@ -2405,7 +2409,7 @@ whether they are being accessed reactively or not.
|
||||
It's very rare to need to access `currentComputation` directly. The
|
||||
current computation is used implicitly by
|
||||
[`Deps.active`](#deps_active) (which tests whether there is one),
|
||||
[`Deps.depend`](#deps_depend) (which registers that it depends on a
|
||||
[`dependency.depend()`](#dependency_depend) (which registers that it depends on a
|
||||
dependency), and [`Deps.onInvalidate`](#deps_oninvalidate) (which
|
||||
registers a callback with it).
|
||||
|
||||
@@ -2423,15 +2427,6 @@ computations that need rerunning. This means that if an `afterFlush`
|
||||
function invalidates a computation, that computation will be rerun
|
||||
before any other `afterFlush` functions are called.
|
||||
|
||||
{{> api_box deps_depend }}
|
||||
|
||||
`Deps.depend` is used in reactive data source implementations as the
|
||||
primary way to record the fact that a Dependency is being accessed from
|
||||
some computation. If there is a current computation, it becomes a
|
||||
dependent of the Dependency, while the Dependency becomes a dependency of
|
||||
the computation. Calls
|
||||
[`dependency.addDependent()`](#dependency_adddependent).
|
||||
|
||||
<h2 id="deps_computation"><span>Deps.Computation</span></h2>
|
||||
|
||||
A Computation object represents code that is repeatedly rerun in
|
||||
@@ -2545,10 +2540,10 @@ accompanied by a Dependency object that tracks the computations that depend
|
||||
on it, as in this example:
|
||||
|
||||
var weather = "sunny";
|
||||
var weatherDeps = new Deps.Dependency;
|
||||
var weatherDep = new Deps.Dependency;
|
||||
|
||||
var getWeather = function () {
|
||||
Deps.depend(weatherDeps);
|
||||
weatherDep.depend()
|
||||
return weather;
|
||||
};
|
||||
|
||||
@@ -2556,12 +2551,12 @@ on it, as in this example:
|
||||
weather = w;
|
||||
// (could add logic here to only call changed()
|
||||
// if the new value is different from the old)
|
||||
weatherDeps.changed();
|
||||
weatherDep.changed();
|
||||
};
|
||||
|
||||
This example implements a weather data source with a simple getter and
|
||||
setter. The getter records that the current computation depends on
|
||||
the `weatherDeps` dependency using `Deps.depend`, while the setter
|
||||
the `weatherDep` dependency using `depend()`, while the setter
|
||||
signals the dependency to invalidate all dependent computations by
|
||||
calling `changed()`.
|
||||
|
||||
@@ -2577,7 +2572,7 @@ current computation is made to depend on an internal Dependency that
|
||||
does not change if the weather goes from, say, "rainy" to "cloudy".
|
||||
|
||||
Conceptually, the only two things a Dependency can do are gain a
|
||||
dependent (typically via `Deps.depend`) and change.
|
||||
dependent and change.
|
||||
|
||||
A Dependency's dependent computations are always valid (they have
|
||||
`invalidated === false`). If a dependent is invalidated at any time,
|
||||
@@ -2586,14 +2581,10 @@ removed.
|
||||
|
||||
{{> api_box dependency_changed }}
|
||||
|
||||
{{> api_box dependency_adddependent }}
|
||||
{{> api_box dependency_depend }}
|
||||
|
||||
In almost all cases, you want to declare a dependency of the current
|
||||
computation and should use `Deps.depend(dependency)` instead,
|
||||
which calls `dependency.addDependent(null)`.
|
||||
|
||||
Computations added as dependents of the Dependency will be removed
|
||||
immediately if they are ever invalidated or stopped.
|
||||
`dep.depend()` is used in reactive data source implementations to record
|
||||
the fact that `dep` is being accessed from the current computation.
|
||||
|
||||
{{> api_box dependency_hasdependents }}
|
||||
|
||||
|
||||
@@ -66,6 +66,19 @@ Template.api.settings = {
|
||||
"`Meteor.settings.public` will also be available on the client."]
|
||||
};
|
||||
|
||||
Template.api.release = {
|
||||
id: "meteor_release",
|
||||
name: "Meteor.release",
|
||||
locus: "Server and client",
|
||||
descr: ["`Meteor.release` is a string containing the name of the " +
|
||||
"[release](#meteorupdate) with which the project was built (for " +
|
||||
"example, `\"" +
|
||||
// Put the current release in the docs as the example)
|
||||
(Meteor.release ? Meteor.release : '0.6.0') +
|
||||
"\"`). It is `undefined` if the project was built using a git " +
|
||||
"checkout of Meteor."]
|
||||
};
|
||||
|
||||
Template.api.ejsonParse = {
|
||||
id: "ejson_parse",
|
||||
name: "EJSON.parse(str)",
|
||||
@@ -812,18 +825,6 @@ Template.api.deps_afterflush = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.deps_depend = {
|
||||
id: "deps_depend",
|
||||
name: "Deps.depend(dependency)",
|
||||
locus: "Client",
|
||||
descr: ["Declares that the current computation depends on `dependency`. The current computation, if there is one, becomes a dependent of `dependency`, meaning it will be invalidated and rerun the next time `dependency` changes.", "Returns `true` if this results in `dependency` gaining a new dependent (or `false` if this relationship already exists or there is no current computation)."],
|
||||
args: [
|
||||
{name: "dependency",
|
||||
type: "Deps.Dependency",
|
||||
descr: "The dependency for this computation to depend on."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.computation_stop = {
|
||||
id: "computation_stop",
|
||||
name: "<em>computation</em>.stop()",
|
||||
@@ -878,15 +879,15 @@ Template.api.dependency_changed = {
|
||||
descr: ["Invalidate all dependent computations immediately and remove them as dependents."]
|
||||
};
|
||||
|
||||
Template.api.dependency_adddependent = {
|
||||
id: "dependency_adddependent",
|
||||
name: "<em>dependency</em>.addDependent(computation)",
|
||||
Template.api.dependency_depend = {
|
||||
id: "dependency_depend",
|
||||
name: "<em>dependency</em>.depend([fromComputation])",
|
||||
locus: "Client",
|
||||
descr: ["Adds `computation` as a dependent of this Dependency, recording the fact that the computation depends on this Dependency.", "Returns true if the computation was not already a dependent of this Dependency."],
|
||||
descr: ["Declares that the current computation (or `fromComputation` if given) depends on `dependency`. The computation will be invalidated the next time `dependency` changes.", "If there is no current computation and `depend()` is called with no arguments, it does nothing and returns false.", "Returns true if the computation is a new dependent of `dependency` rather than an existing one."],
|
||||
args: [
|
||||
{name: "computation",
|
||||
{name: "fromComputation",
|
||||
type: "Deps.Computation",
|
||||
descr: "The computation to add, or `null` to use the current computation (in which case there must be a current computation)."}
|
||||
descr: "An optional computation declared to depend on `dependency` instead of the current computation."}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<h3 id="meteorhelp">meteor help</h3>
|
||||
|
||||
Get help on meteor command line usage. Running `meteor
|
||||
help` by itself will list the common meteor
|
||||
Get help on meteor command line usage. Running `meteor help` by
|
||||
itself will list the common meteor
|
||||
commands. Running <code>meteor help <i>command</i></code> will print
|
||||
detailed help about the command.
|
||||
|
||||
@@ -118,10 +118,13 @@ inspector, just like any other client-side JavaScript.
|
||||
|
||||
<h3 id="meteorupdate">meteor update</h3>
|
||||
|
||||
Upgrade to the latest Meteor version. Checks to see if a new
|
||||
version of Meteor is available, and if so, downloads and installs
|
||||
it. You must be connected to the internet.
|
||||
Sets the version of Meteor to use with the current project. If a
|
||||
release is specified with `--release`, set the project to use that
|
||||
version. Otherwise download and use the latest release of Meteor.
|
||||
|
||||
Every project is pinned to a specific release of Meteor. You can temporarily try
|
||||
using your package with another release by passing the `--release` option to any
|
||||
command; `meteor update` simply changes the pinned release.
|
||||
|
||||
<h3 id="meteoradd">meteor add <i>package</i></h3>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="top"></div>
|
||||
<h1 class="main-headline">Meteor 0.5.9</h1>
|
||||
{{> headline }}
|
||||
{{> introduction }}
|
||||
{{> concepts }}
|
||||
{{> api }}
|
||||
@@ -35,6 +35,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- This only is displayed on narrow displays (eg phone) -->
|
||||
<template name="headline">
|
||||
<h1 class="main-headline">Meteor {{release}}</h1>
|
||||
</template>
|
||||
|
||||
|
||||
<template name="dtdd_helper">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
METEOR_VERSION = "0.5.9";
|
||||
Template.headline.release = function () {
|
||||
return Meteor.release || "(checkout)";
|
||||
};
|
||||
|
||||
|
||||
Meteor.startup(function () {
|
||||
// XXX this is broken by the new multi-page layout. Also, it was
|
||||
@@ -7,6 +10,9 @@ Meteor.startup(function () {
|
||||
// later.
|
||||
// prettyPrint();
|
||||
|
||||
//mixpanel tracking
|
||||
mixpanel.track('docs');
|
||||
|
||||
// returns a jQuery object suitable for setting scrollTop to
|
||||
// scroll the page, either directly for via animate()
|
||||
var scroller = function() {
|
||||
@@ -70,6 +76,8 @@ Meteor.startup(function () {
|
||||
evt.preventDefault();
|
||||
var sel = $(this).attr('href');
|
||||
scrollToSection(sel);
|
||||
|
||||
mixpanel.track('docs_navigate_' + sel);
|
||||
});
|
||||
|
||||
// Make external links open in a new tab.
|
||||
@@ -77,7 +85,7 @@ Meteor.startup(function () {
|
||||
});
|
||||
|
||||
var toc = [
|
||||
{name: "Meteor " + METEOR_VERSION, id: "top"}, [
|
||||
{name: "Meteor " + Template.headline.release(), id: "top"}, [
|
||||
"Quick start",
|
||||
"Seven principles",
|
||||
"Resources"
|
||||
@@ -98,7 +106,8 @@ var toc = [
|
||||
"Meteor.isServer",
|
||||
"Meteor.startup",
|
||||
"Meteor.absoluteUrl",
|
||||
"Meteor.settings"
|
||||
"Meteor.settings",
|
||||
"Meteor.release"
|
||||
],
|
||||
|
||||
"Publish and subscribe", [
|
||||
@@ -248,7 +257,6 @@ var toc = [
|
||||
"Deps.currentComputation",
|
||||
"Deps.onInvalidate",
|
||||
"Deps.afterFlush",
|
||||
"Deps.depend",
|
||||
"Deps.Computation", [
|
||||
{instance: "computation", name: "stop", id: "computation_stop"},
|
||||
{instance: "computation", name: "invalidate", id: "computation_invalidate"},
|
||||
@@ -259,7 +267,7 @@ var toc = [
|
||||
],
|
||||
"Deps.Dependency", [
|
||||
{instance: "dependency", name: "changed", id: "dependency_changed"},
|
||||
{instance: "dependency", name: "addDependent", id: "dependency_adddependent"},
|
||||
{instance: "dependency", name: "depend", id: "dependency_depend"},
|
||||
{instance: "dependency", name: "hasDependents", id: "dependency_hasdependents"}
|
||||
]
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
if (document.location.host.match(/^((docs)|(preview))\.meteor\.com(:80)?$/)) {
|
||||
if (document.location.host.match(/^docs\.meteor\.com(:80)?$/)) {
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-30093278-2']);
|
||||
_gaq.push(['_setDomainName', 'preview.meteor.com']);
|
||||
@@ -11,6 +11,26 @@
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
|
||||
// Mixpanel. Must be loaded even in devel mode or else mixpanel.track (etc) fails.
|
||||
(function(c,a){var b,d,h,e;b=c.createElement("script");b.type="text/javascript";
|
||||
b.async=!0;b.src=("https:"===c.location.protocol?"https:":"http:")+
|
||||
'//api.mixpanel.com/site_media/js/api/mixpanel.2.js';d=c.getElementsByTagName("script")[0];
|
||||
d.parentNode.insertBefore(b,d);a._i=[];a.init=function(b,c,f){function d(a,b){
|
||||
var c=b.split(".");2==c.length&&(a=a[c[0]],b=c[1]);a[b]=function(){a.push([b].concat(
|
||||
Array.prototype.slice.call(arguments,0)))}}var g=a;"undefined"!==typeof f?g=a[f]=[]:
|
||||
f="mixpanel";g.people=g.people||[];h=['disable','track','track_pageview','track_links',
|
||||
'track_forms','register','register_once','unregister','identify','name_tag',
|
||||
'set_config','people.set','people.increment'];for(e=0;e<h.length;e++)d(g,h[e]);
|
||||
a._i.push([b,c,f])};a.__SV=1.1;window.mixpanel=a})(document,window.mixpanel||[]);
|
||||
mixpanel.init("ccbcd7e4e53fc175e04474e70961cf45");
|
||||
} else {
|
||||
mixpanel = {
|
||||
track: function () {
|
||||
// console.log("track", _.toArray(arguments));
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -32,7 +32,6 @@ invalidations to clients.
|
||||
Meteor is a work in progress, but we hope it shows the direction of
|
||||
our thinking. We'd love to hear your feedback.
|
||||
|
||||
— Avital, David, David, Geoff, Jade, Kara, Kristy, Matt, Naomi, and Nick
|
||||
|
||||
## Quick start!
|
||||
|
||||
@@ -59,7 +58,7 @@ Run it locally:
|
||||
<pre>
|
||||
$ cd myapp
|
||||
$ meteor
|
||||
Running on: http://localhost:3000/
|
||||
=> Meteor server running on: http://localhost:3000/
|
||||
</pre>
|
||||
|
||||
Unleash it on the world (on a free server we provide):
|
||||
@@ -134,7 +133,7 @@ developers hang out here and will answer your questions whenever they
|
||||
can.</dd>
|
||||
|
||||
<dt><span>GitHub</span></dt>
|
||||
<dd>The code is on <a href="http://github.com/meteor/meteor">GitHub</a>. The best way to send a patch is with a GitHub pull request, and the best way to file a bug is in the GitHub bug tracker.</dd>
|
||||
<dd>The code is on <a href="http://github.com/meteor/meteor">GitHub</a>. The best way to send a patch is with a GitHub pull request, and the best way to file a bug is in the GitHub bug tracker. If the issue contains sensitive information or raises a security concern, email <a href="mailto:security@meteor.com">security@meteor.com</a> instead, which will page the security team.</dd>
|
||||
</dl>
|
||||
|
||||
{{/markdown}}
|
||||
|
||||
@@ -11,5 +11,15 @@ CoffeeScript is supported on both the client and the server. Files
|
||||
ending with `.coffee` or `.litcoffee` are automatically compiled to
|
||||
JavaScript.
|
||||
|
||||
Global variables can be set in CoffeeScript by using `this` (or CoffeeScript's
|
||||
`@` shorthand), because at the top level `this` refers to the global namespace
|
||||
(`window` on the client and
|
||||
[`global`](http://nodejs.org/api/globals.html#globals_global) on the server).
|
||||
Thus
|
||||
|
||||
@myFunction = -> 123
|
||||
|
||||
at the top level sets the global variable `myFunction`.
|
||||
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
|
||||
1
examples/leaderboard/.meteor/release
Normal file
1
examples/leaderboard/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/other/benchmark/.meteor/release
Normal file
1
examples/other/benchmark/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/other/controllers-demo/.meteor/release
Normal file
1
examples/other/controllers-demo/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
7
examples/other/login-demo/.meteor/packages
Normal file
7
examples/other/login-demo/.meteor/packages
Normal file
@@ -0,0 +1,7 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
preserve-inputs
|
||||
accounts-google
|
||||
1
examples/other/login-demo/.meteor/release
Normal file
1
examples/other/login-demo/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
32
examples/other/login-demo/login-demo.css
Normal file
32
examples/other/login-demo/login-demo.css
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
* { padding: 0; margin: 0; }
|
||||
|
||||
#main {
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.msgDiv {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 5px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
#readme {
|
||||
margin: 20px;
|
||||
border: 1px solid #ccc;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 16px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
38
examples/other/login-demo/login-demo.html
Normal file
38
examples/other/login-demo/login-demo.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<head>
|
||||
<title>login-demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main">
|
||||
{{> main}}
|
||||
</div>
|
||||
<div id="readme">
|
||||
<p>This is a minimal app where you need to log in to see the database. There's also a loading screen while logging in and until the initial data is loaded.</p>
|
||||
<p>There are three top-level screens corresponding to the three possible states of the app:</p>
|
||||
<ul>
|
||||
<li>Logging in / Loading — when <code>{{loggingIn}}</code> is true</li>
|
||||
<li>Logged in — when there is a <code>{{currentUser}}</code></li>
|
||||
<li>Logged out — otherwise</li>
|
||||
</ul>
|
||||
<p>If you reload the page while logged in, you'll start in the "logging in" state and see the "Loading..." message until the data loads. Because logging in doesn't complete until all subscriptions have been rerun and finished loading, and the app only serves data when you're logged in, the "logging in" state encompasses loading the initial data for all subscriptions and is the only loading screen we need.</p>
|
||||
<p>To configure this app for Google auth, the easiest way is to add the <code>accounts-ui</code> package, add <code>{{loginButtons}}</code> to the end of the body, and use the configuration wizard.</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<template name="main">
|
||||
{{#if loggingIn}}
|
||||
<div class="loading">Loading...</div>
|
||||
{{else}}
|
||||
{{#if currentUser}}
|
||||
<div class="msgDiv">
|
||||
Signed in as: {{currentUser.services.google.email}}
|
||||
</div>
|
||||
<a href="#" id="logout">Sign out</a>
|
||||
{{else}}
|
||||
<a href="#" id="login">Sign In With Google</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="msgDiv">
|
||||
Client can see {{numGizmos}} gizmos.
|
||||
</div>
|
||||
</template>
|
||||
57
examples/other/login-demo/login-demo.js
Normal file
57
examples/other/login-demo/login-demo.js
Normal file
@@ -0,0 +1,57 @@
|
||||
Gizmos = new Meteor.Collection("gizmos");
|
||||
|
||||
if (Meteor.isClient) {
|
||||
|
||||
var allGizmos = Meteor.subscribe("allGizmos");
|
||||
|
||||
Template.main.numGizmos = function () {
|
||||
return Gizmos.find().count();
|
||||
};
|
||||
|
||||
Template.main.events({
|
||||
'click #login': function (evt) {
|
||||
Meteor.loginWithGoogle(function (err) {
|
||||
if (err)
|
||||
Meteor._debug(err);
|
||||
});
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click #logout': function (evt) {
|
||||
Meteor.logout(function (err) {
|
||||
if (err)
|
||||
Meteor._debug(err);
|
||||
});
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
|
||||
Meteor.startup(function () {
|
||||
// populate the Gizmos collection if it's empty on startup
|
||||
if (Gizmos.find().count() === 0) {
|
||||
for (var i = 0; i < 1000; i++)
|
||||
Gizmos.insert({ name: "Gizmo " + i });
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.publish("allGizmos", function () {
|
||||
// Only publish the Gizmos if user is logged in.
|
||||
var user = this.userId && Meteor.users.findOne(this.userId);
|
||||
if (user) {
|
||||
// potentially put other conditions on user here...
|
||||
return Gizmos.find({});
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
Meteor.publish(null, function () {
|
||||
// If logged in, autopublish the current user's Google email
|
||||
// to the client (which isn't published by default).
|
||||
return this.userId &&
|
||||
Meteor.users.find(this.userId,
|
||||
{fields: {'services.google.email': 1}});
|
||||
});
|
||||
|
||||
}
|
||||
1
examples/other/quiescence/.meteor/release
Normal file
1
examples/other/quiescence/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
@@ -30,7 +30,7 @@ if (Meteor.isServer) {
|
||||
}
|
||||
});
|
||||
|
||||
var Fiber = __meteor_bootstrap__.require('fibers');
|
||||
var Fiber = Npm.require('fibers');
|
||||
|
||||
var sleep = function (ms) {
|
||||
var fiber = Fiber.current;
|
||||
|
||||
1
examples/other/template-demo/.meteor/release
Normal file
1
examples/other/template-demo/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/parties/.meteor/release
Normal file
1
examples/parties/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
@@ -38,7 +38,7 @@ Parties.allow({
|
||||
}
|
||||
});
|
||||
|
||||
var attending = function (party) {
|
||||
attending = function (party) {
|
||||
return (_.groupBy(party.rsvps, 'rsvp').yes || []).length;
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ Meteor.methods({
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Users
|
||||
|
||||
var displayName = function (user) {
|
||||
displayName = function (user) {
|
||||
if (user.profile && user.profile.name)
|
||||
return user.profile.name;
|
||||
return user.emails[0].address;
|
||||
|
||||
1
examples/todos/.meteor/release
Normal file
1
examples/todos/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/accounts-ui-viewer/.meteor/release
Normal file
1
examples/unfinished/accounts-ui-viewer/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/azrael/.meteor/release
Normal file
1
examples/unfinished/azrael/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/benchmark/.meteor/release
Normal file
1
examples/unfinished/benchmark/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/coffeeless/.meteor/release
Normal file
1
examples/unfinished/coffeeless/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/controls/.meteor/release
Normal file
1
examples/unfinished/controls/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/jsparse-docs/.meteor/release
Normal file
1
examples/unfinished/jsparse-docs/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/leaderboard-remote/.meteor/release
Normal file
1
examples/unfinished/leaderboard-remote/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/parse-inspector/.meteor/release
Normal file
1
examples/unfinished/parse-inspector/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/todos-backbone/.meteor/release
Normal file
1
examples/unfinished/todos-backbone/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/unfinished/todos-underscore/.meteor/release
Normal file
1
examples/unfinished/todos-underscore/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
1
examples/wordplay/.meteor/release
Normal file
1
examples/wordplay/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
0.6.0
|
||||
@@ -38,7 +38,7 @@ var ADJACENCIES = [
|
||||
];
|
||||
|
||||
// generate a new random selection of letters.
|
||||
var new_board = function () {
|
||||
new_board = function () {
|
||||
var board = [];
|
||||
var i;
|
||||
|
||||
@@ -62,7 +62,7 @@ var new_board = function () {
|
||||
// board. each path is an array of board positions 0-15. a valid
|
||||
// path can use each position only once, and each position must be
|
||||
// adjacent to the previous position.
|
||||
var paths_for_word = function (board, word) {
|
||||
paths_for_word = function (board, word) {
|
||||
var valid_paths = [];
|
||||
|
||||
var check_path = function (word, path, positions_to_try) {
|
||||
|
||||
46
install.sh
46
install.sh
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd `dirname $0`
|
||||
|
||||
if [ "$PREFIX" != "" ] ; then
|
||||
PARENT="$PREFIX"
|
||||
else
|
||||
PARENT="/usr/local"
|
||||
fi
|
||||
TARGET_DIR="$PARENT/meteor"
|
||||
|
||||
# XXX try to fix it up automatically?
|
||||
if [ ! -d "$PARENT" -o ! -w "$PARENT" ] ; then
|
||||
echo "Can not write to $PARENT"
|
||||
exit 1
|
||||
elif [ -d "$PARENT/bin" -a ! -w "$PARENT/bin" ] ; then
|
||||
echo "Can not write to $PARENT/bin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing in $PARENT"
|
||||
|
||||
rm -rf "$TARGET_DIR"
|
||||
|
||||
# make sure dev bundle exists before trying to install
|
||||
./meteor --version || exit 1
|
||||
|
||||
cp -a dev_bundle "$TARGET_DIR"
|
||||
cp LICENSE.txt "$TARGET_DIR"
|
||||
cp History.md "$TARGET_DIR"
|
||||
|
||||
function CPR {
|
||||
tar -c --exclude .meteor/local "$1" | tar -x -C "$2"
|
||||
}
|
||||
cp meteor "$TARGET_DIR/bin"
|
||||
CPR app "$TARGET_DIR"
|
||||
CPR packages "$TARGET_DIR"
|
||||
CPR examples "$TARGET_DIR"
|
||||
rm -rf "$TARGET_DIR"/examples/unfinished
|
||||
|
||||
mkdir -p "$PARENT/bin"
|
||||
rm -f "$PARENT/bin/meteor"
|
||||
ln -s "$TARGET_DIR/bin/meteor" "$PARENT/bin/meteor"
|
||||
|
||||
# mark directory with current git sha
|
||||
git rev-parse HEAD > "$TARGET_DIR/.git_version.txt"
|
||||
23
meteor
23
meteor
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.2.23
|
||||
BUNDLE_VERSION=0.3.0
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
@@ -27,6 +27,7 @@ elif [ "$UNAME" = "Linux" ] ; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
PLATFORM="${UNAME}_${ARCH}"
|
||||
|
||||
# Find the script dir, following one level of symlink. Note that symlink
|
||||
# can be relative or absolute. Too bad 'readlink -f' is not portable.
|
||||
@@ -44,23 +45,23 @@ function install_dev_bundle {
|
||||
set -e
|
||||
trap "echo Failed to install dependency kit." EXIT
|
||||
|
||||
TARBALL="dev_bundle_${UNAME}_${ARCH}_${BUNDLE_VERSION}.tar.gz"
|
||||
TMPDIR="$SCRIPT_DIR/dev_bundle.xxx"
|
||||
TARBALL="dev_bundle_${PLATFORM}_${BUNDLE_VERSION}.tar.gz"
|
||||
BUNDLE_TMPDIR="$SCRIPT_DIR/dev_bundle.xxx"
|
||||
|
||||
rm -rf "$TMPDIR"
|
||||
mkdir "$TMPDIR"
|
||||
rm -rf "$BUNDLE_TMPDIR"
|
||||
mkdir "$BUNDLE_TMPDIR"
|
||||
|
||||
if [ -f "$SCRIPT_DIR/$TARBALL" ] ; then
|
||||
echo "Skipping download and installing kit from $SCRIPT_DIR/$TARBALL"
|
||||
tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$TMPDIR"
|
||||
tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$BUNDLE_TMPDIR"
|
||||
else
|
||||
curl -# https://d3sqy0vbqsdhku.cloudfront.net/$TARBALL | tar -xzf - -C "$TMPDIR"
|
||||
test -x "${TMPDIR}/bin/node" # bomb out if it didn't work, eg no net
|
||||
curl -# https://d3sqy0vbqsdhku.cloudfront.net/$TARBALL | tar -xzf - -C "$BUNDLE_TMPDIR"
|
||||
test -x "${BUNDLE_TMPDIR}/bin/node" # bomb out if it didn't work, eg no net
|
||||
fi
|
||||
|
||||
# Delete old dev bundle and rename the new one on top of it.
|
||||
rm -rf "$SCRIPT_DIR/dev_bundle"
|
||||
mv "$TMPDIR" "$SCRIPT_DIR/dev_bundle"
|
||||
mv "$BUNDLE_TMPDIR" "$SCRIPT_DIR/dev_bundle"
|
||||
|
||||
echo "Installed dependency kit v${BUNDLE_VERSION} in dev_bundle."
|
||||
echo
|
||||
@@ -83,11 +84,11 @@ if [ -d "$SCRIPT_DIR/.git" ] || [ -f "$SCRIPT_DIR/.git" ]; then
|
||||
fi
|
||||
|
||||
DEV_BUNDLE="$SCRIPT_DIR/dev_bundle"
|
||||
METEOR="$SCRIPT_DIR/app/meteor/meteor.js"
|
||||
METEOR="$SCRIPT_DIR/tools/meteor.js"
|
||||
else
|
||||
# In an install
|
||||
DEV_BUNDLE=$(dirname "$SCRIPT_DIR")
|
||||
METEOR="$DEV_BUNDLE/app/meteor/meteor.js"
|
||||
METEOR="$DEV_BUNDLE/tools/meteor.js"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -1,202 +1,199 @@
|
||||
(function () {
|
||||
// This is reactive.
|
||||
Meteor.userId = function () {
|
||||
return Meteor.default_connection.userId();
|
||||
};
|
||||
|
||||
// This is reactive.
|
||||
Meteor.userId = function () {
|
||||
return Meteor.default_connection.userId();
|
||||
};
|
||||
|
||||
var loggingIn = false;
|
||||
var loggingInDeps = new Deps.Dependency;
|
||||
// This is mostly just called within this file, but Meteor.loginWithPassword
|
||||
// also uses it to make loggingIn() be true during the beginPasswordExchange
|
||||
// method call too.
|
||||
Accounts._setLoggingIn = function (x) {
|
||||
if (loggingIn !== x) {
|
||||
loggingIn = x;
|
||||
loggingInDeps.changed();
|
||||
}
|
||||
};
|
||||
Meteor.loggingIn = function () {
|
||||
Deps.depend(loggingInDeps);
|
||||
return loggingIn;
|
||||
};
|
||||
|
||||
// This calls userId, which is reactive.
|
||||
Meteor.user = function () {
|
||||
var userId = Meteor.userId();
|
||||
if (!userId)
|
||||
return null;
|
||||
return Meteor.users.findOne(userId);
|
||||
};
|
||||
|
||||
// Call a login method on the server.
|
||||
//
|
||||
// A login method is a method which on success calls `this.setUserId(id)` on
|
||||
// the server and returns an object with fields 'id' (containing the user id)
|
||||
// and 'token' (containing a resume token).
|
||||
//
|
||||
// This function takes care of:
|
||||
// - Updating the Meteor.loggingIn() reactive data source
|
||||
// - Calling the method in 'wait' mode
|
||||
// - On success, saving the resume token to localStorage
|
||||
// - On success, calling Meteor.default_connection.setUserId()
|
||||
// - Setting up an onReconnect handler which logs in with
|
||||
// the resume token
|
||||
//
|
||||
// Options:
|
||||
// - methodName: The method to call (default 'login')
|
||||
// - methodArguments: The arguments for the method
|
||||
// - validateResult: If provided, will be called with the result of the
|
||||
// method. If it throws, the client will not be logged in (and
|
||||
// its error will be passed to the callback).
|
||||
// - userCallback: Will be called with no arguments once the user is fully
|
||||
// logged in, or with the error on error.
|
||||
Accounts.callLoginMethod = function (options) {
|
||||
options = _.extend({
|
||||
methodName: 'login',
|
||||
methodArguments: [],
|
||||
_suppressLoggingIn: false
|
||||
}, options);
|
||||
// Set defaults for callback arguments to no-op functions; make sure we
|
||||
// override falsey values too.
|
||||
_.each(['validateResult', 'userCallback'], function (f) {
|
||||
if (!options[f])
|
||||
options[f] = function () {};
|
||||
});
|
||||
|
||||
var reconnected = false;
|
||||
|
||||
// We want to set up onReconnect as soon as we get a result token back from
|
||||
// the server, without having to wait for subscriptions to rerun. This is
|
||||
// because if we disconnect and reconnect between getting the result and
|
||||
// getting the results of subscription rerun, we WILL NOT re-send this
|
||||
// method (because we never re-send methods whose results we've received)
|
||||
// but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce"
|
||||
// time. This will lead to _makeClientLoggedIn(result.id) even though we
|
||||
// haven't actually sent a login method!
|
||||
//
|
||||
// But by making sure that we send this "resume" login in that case (and
|
||||
// calling _makeClientLoggedOut if it fails), we'll end up with an accurate
|
||||
// client-side userId. (It's important that livedata_connection guarantees
|
||||
// that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback
|
||||
// will occur before the callback from the resume login call.)
|
||||
var onResultReceived = function (err, result) {
|
||||
if (err || !result || !result.token) {
|
||||
Meteor.default_connection.onReconnect = null;
|
||||
} else {
|
||||
Meteor.default_connection.onReconnect = function() {
|
||||
reconnected = true;
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
Accounts._makeClientLoggedOut();
|
||||
}
|
||||
options.userCallback(error);
|
||||
}});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// This callback is called once the local cache of the current-user
|
||||
// subscription (and all subscriptions, in fact) are guaranteed to be up to
|
||||
// date.
|
||||
var loggedInAndDataReadyCallback = function (error, result) {
|
||||
// If the login method returns its result but the connection is lost
|
||||
// before the data is in the local cache, it'll set an onReconnect (see
|
||||
// above). The onReconnect will try to log in using the token, and *it*
|
||||
// will call userCallback via its own version of this
|
||||
// loggedInAndDataReadyCallback. So we don't have to do anything here.
|
||||
if (reconnected)
|
||||
return;
|
||||
|
||||
// Note that we need to call this even if _suppressLoggingIn is true,
|
||||
// because it could be matching a _setLoggingIn(true) from a
|
||||
// half-completed pre-reconnect login method.
|
||||
Accounts._setLoggingIn(false);
|
||||
if (error || !result) {
|
||||
error = error || new Error(
|
||||
"No result from call to " + options.methodName);
|
||||
options.userCallback(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
options.validateResult(result);
|
||||
} catch (e) {
|
||||
options.userCallback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the client logged in. (The user data should already be loaded!)
|
||||
Accounts._makeClientLoggedIn(result.id, result.token);
|
||||
options.userCallback();
|
||||
};
|
||||
|
||||
if (!options._suppressLoggingIn)
|
||||
Accounts._setLoggingIn(true);
|
||||
Meteor.apply(
|
||||
options.methodName,
|
||||
options.methodArguments,
|
||||
{wait: true, onResultReceived: onResultReceived},
|
||||
loggedInAndDataReadyCallback);
|
||||
};
|
||||
|
||||
Accounts._makeClientLoggedOut = function() {
|
||||
Accounts._unstoreLoginToken();
|
||||
Meteor.default_connection.setUserId(null);
|
||||
Meteor.default_connection.onReconnect = null;
|
||||
};
|
||||
|
||||
Accounts._makeClientLoggedIn = function(userId, token) {
|
||||
Accounts._storeLoginToken(userId, token);
|
||||
Meteor.default_connection.setUserId(userId);
|
||||
};
|
||||
|
||||
Meteor.logout = function (callback) {
|
||||
Meteor.apply('logout', [], {wait: true}, function(error, result) {
|
||||
if (error) {
|
||||
callback && callback(error);
|
||||
} else {
|
||||
Accounts._makeClientLoggedOut();
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// If we're using Handlebars, register the {{currentUser}} and
|
||||
// {{loggingIn}} global helpers.
|
||||
if (typeof Handlebars !== 'undefined') {
|
||||
Handlebars.registerHelper('currentUser', function () {
|
||||
return Meteor.user();
|
||||
});
|
||||
Handlebars.registerHelper('loggingIn', function () {
|
||||
return Meteor.loggingIn();
|
||||
});
|
||||
var loggingIn = false;
|
||||
var loggingInDeps = new Deps.Dependency;
|
||||
// This is mostly just called within this file, but Meteor.loginWithPassword
|
||||
// also uses it to make loggingIn() be true during the beginPasswordExchange
|
||||
// method call too.
|
||||
Accounts._setLoggingIn = function (x) {
|
||||
if (loggingIn !== x) {
|
||||
loggingIn = x;
|
||||
loggingInDeps.changed();
|
||||
}
|
||||
};
|
||||
Meteor.loggingIn = function () {
|
||||
loggingInDeps.depend();
|
||||
return loggingIn;
|
||||
};
|
||||
|
||||
// XXX this can be simplified if we merge in
|
||||
// https://github.com/meteor/meteor/pull/273
|
||||
var loginServicesConfigured = false;
|
||||
var loginServicesConfiguredDeps = new Deps.Dependency;
|
||||
Meteor.subscribe("meteor.loginServiceConfiguration", function () {
|
||||
loginServicesConfigured = true;
|
||||
loginServicesConfiguredDeps.changed();
|
||||
// This calls userId, which is reactive.
|
||||
Meteor.user = function () {
|
||||
var userId = Meteor.userId();
|
||||
if (!userId)
|
||||
return null;
|
||||
return Meteor.users.findOne(userId);
|
||||
};
|
||||
|
||||
// Call a login method on the server.
|
||||
//
|
||||
// A login method is a method which on success calls `this.setUserId(id)` on
|
||||
// the server and returns an object with fields 'id' (containing the user id)
|
||||
// and 'token' (containing a resume token).
|
||||
//
|
||||
// This function takes care of:
|
||||
// - Updating the Meteor.loggingIn() reactive data source
|
||||
// - Calling the method in 'wait' mode
|
||||
// - On success, saving the resume token to localStorage
|
||||
// - On success, calling Meteor.default_connection.setUserId()
|
||||
// - Setting up an onReconnect handler which logs in with
|
||||
// the resume token
|
||||
//
|
||||
// Options:
|
||||
// - methodName: The method to call (default 'login')
|
||||
// - methodArguments: The arguments for the method
|
||||
// - validateResult: If provided, will be called with the result of the
|
||||
// method. If it throws, the client will not be logged in (and
|
||||
// its error will be passed to the callback).
|
||||
// - userCallback: Will be called with no arguments once the user is fully
|
||||
// logged in, or with the error on error.
|
||||
Accounts.callLoginMethod = function (options) {
|
||||
options = _.extend({
|
||||
methodName: 'login',
|
||||
methodArguments: [],
|
||||
_suppressLoggingIn: false
|
||||
}, options);
|
||||
// Set defaults for callback arguments to no-op functions; make sure we
|
||||
// override falsey values too.
|
||||
_.each(['validateResult', 'userCallback'], function (f) {
|
||||
if (!options[f])
|
||||
options[f] = function () {};
|
||||
});
|
||||
|
||||
// A reactive function returning whether the
|
||||
// loginServiceConfiguration subscription is ready. Used by
|
||||
// accounts-ui to hide the login button until we have all the
|
||||
// configuration loaded
|
||||
Accounts.loginServicesConfigured = function () {
|
||||
if (loginServicesConfigured)
|
||||
return true;
|
||||
var reconnected = false;
|
||||
|
||||
// not yet complete, save the context for invalidation once we are.
|
||||
Deps.depend(loginServicesConfiguredDeps);
|
||||
return false;
|
||||
// We want to set up onReconnect as soon as we get a result token back from
|
||||
// the server, without having to wait for subscriptions to rerun. This is
|
||||
// because if we disconnect and reconnect between getting the result and
|
||||
// getting the results of subscription rerun, we WILL NOT re-send this
|
||||
// method (because we never re-send methods whose results we've received)
|
||||
// but we WILL call loggedInAndDataReadyCallback at "reconnect quiesce"
|
||||
// time. This will lead to _makeClientLoggedIn(result.id) even though we
|
||||
// haven't actually sent a login method!
|
||||
//
|
||||
// But by making sure that we send this "resume" login in that case (and
|
||||
// calling _makeClientLoggedOut if it fails), we'll end up with an accurate
|
||||
// client-side userId. (It's important that livedata_connection guarantees
|
||||
// that the "reconnect quiesce"-time call to loggedInAndDataReadyCallback
|
||||
// will occur before the callback from the resume login call.)
|
||||
var onResultReceived = function (err, result) {
|
||||
if (err || !result || !result.token) {
|
||||
Meteor.default_connection.onReconnect = null;
|
||||
} else {
|
||||
Meteor.default_connection.onReconnect = function() {
|
||||
reconnected = true;
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
Accounts._makeClientLoggedOut();
|
||||
}
|
||||
options.userCallback(error);
|
||||
}});
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// This callback is called once the local cache of the current-user
|
||||
// subscription (and all subscriptions, in fact) are guaranteed to be up to
|
||||
// date.
|
||||
var loggedInAndDataReadyCallback = function (error, result) {
|
||||
// If the login method returns its result but the connection is lost
|
||||
// before the data is in the local cache, it'll set an onReconnect (see
|
||||
// above). The onReconnect will try to log in using the token, and *it*
|
||||
// will call userCallback via its own version of this
|
||||
// loggedInAndDataReadyCallback. So we don't have to do anything here.
|
||||
if (reconnected)
|
||||
return;
|
||||
|
||||
// Note that we need to call this even if _suppressLoggingIn is true,
|
||||
// because it could be matching a _setLoggingIn(true) from a
|
||||
// half-completed pre-reconnect login method.
|
||||
Accounts._setLoggingIn(false);
|
||||
if (error || !result) {
|
||||
error = error || new Error(
|
||||
"No result from call to " + options.methodName);
|
||||
options.userCallback(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
options.validateResult(result);
|
||||
} catch (e) {
|
||||
options.userCallback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the client logged in. (The user data should already be loaded!)
|
||||
Accounts._makeClientLoggedIn(result.id, result.token);
|
||||
options.userCallback();
|
||||
};
|
||||
|
||||
if (!options._suppressLoggingIn)
|
||||
Accounts._setLoggingIn(true);
|
||||
Meteor.apply(
|
||||
options.methodName,
|
||||
options.methodArguments,
|
||||
{wait: true, onResultReceived: onResultReceived},
|
||||
loggedInAndDataReadyCallback);
|
||||
};
|
||||
|
||||
Accounts._makeClientLoggedOut = function() {
|
||||
Accounts._unstoreLoginToken();
|
||||
Meteor.default_connection.setUserId(null);
|
||||
Meteor.default_connection.onReconnect = null;
|
||||
};
|
||||
|
||||
Accounts._makeClientLoggedIn = function(userId, token) {
|
||||
Accounts._storeLoginToken(userId, token);
|
||||
Meteor.default_connection.setUserId(userId);
|
||||
};
|
||||
|
||||
Meteor.logout = function (callback) {
|
||||
Meteor.apply('logout', [], {wait: true}, function(error, result) {
|
||||
if (error) {
|
||||
callback && callback(error);
|
||||
} else {
|
||||
Accounts._makeClientLoggedOut();
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// If we're using Handlebars, register the {{currentUser}} and
|
||||
// {{loggingIn}} global helpers.
|
||||
if (typeof Handlebars !== 'undefined') {
|
||||
Handlebars.registerHelper('currentUser', function () {
|
||||
return Meteor.user();
|
||||
});
|
||||
Handlebars.registerHelper('loggingIn', function () {
|
||||
return Meteor.loggingIn();
|
||||
});
|
||||
}
|
||||
|
||||
// XXX this can be simplified if we merge in
|
||||
// https://github.com/meteor/meteor/pull/273
|
||||
var loginServicesConfigured = false;
|
||||
var loginServicesConfiguredDeps = new Deps.Dependency;
|
||||
Meteor.subscribe("meteor.loginServiceConfiguration", function () {
|
||||
loginServicesConfigured = true;
|
||||
loginServicesConfiguredDeps.changed();
|
||||
});
|
||||
|
||||
// A reactive function returning whether the
|
||||
// loginServiceConfiguration subscription is ready. Used by
|
||||
// accounts-ui to hide the login button until we have all the
|
||||
// configuration loaded
|
||||
Accounts.loginServicesConfigured = function () {
|
||||
if (loginServicesConfigured)
|
||||
return true;
|
||||
|
||||
// not yet complete, save the context for invalidation once we are.
|
||||
loginServicesConfiguredDeps.depend();
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,344 +1,341 @@
|
||||
(function () {
|
||||
///
|
||||
/// LOGIN HANDLERS
|
||||
///
|
||||
|
||||
Meteor.methods({
|
||||
// @returns {Object|null}
|
||||
// If successful, returns {token: reconnectToken, id: userId}
|
||||
// If unsuccessful (for example, if the user closed the oauth login popup),
|
||||
// returns null
|
||||
login: function(options) {
|
||||
var result = tryAllLoginHandlers(options);
|
||||
if (result !== null)
|
||||
this.setUserId(result.id);
|
||||
return result;
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
this.setUserId(null);
|
||||
}
|
||||
});
|
||||
|
||||
Accounts._loginHandlers = [];
|
||||
|
||||
// Try all of the registered login handlers until one of them doesn't return
|
||||
// `undefined`, meaning it handled this call to `login`. Return that return
|
||||
// value, which ought to be a {id/token} pair.
|
||||
var tryAllLoginHandlers = function (options) {
|
||||
var result = undefined;
|
||||
|
||||
_.find(Accounts._loginHandlers, function(handler) {
|
||||
|
||||
var maybeResult = handler(options);
|
||||
if (maybeResult !== undefined) {
|
||||
result = maybeResult;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (result === undefined) {
|
||||
throw new Meteor.Error(400, "Unrecognized options for login request");
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// @param handler {Function} A function that receives an options object
|
||||
// (as passed as an argument to the `login` method) and returns one of:
|
||||
// - `undefined`, meaning don't handle;
|
||||
// - {id: userId, token: *}, if the user logged in successfully.
|
||||
// - throw an error, if the user failed to log in.
|
||||
Accounts.registerLoginHandler = function(handler) {
|
||||
Accounts._loginHandlers.push(handler);
|
||||
};
|
||||
|
||||
// support reconnecting using a meteor login token
|
||||
Accounts._generateStampedLoginToken = function () {
|
||||
return {token: Random.id(), when: +(new Date)};
|
||||
};
|
||||
|
||||
Accounts.registerLoginHandler(function(options) {
|
||||
if (options.resume) {
|
||||
var user = Meteor.users.findOne(
|
||||
{"services.resume.loginTokens.token": options.resume});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Couldn't find login token");
|
||||
|
||||
return {
|
||||
token: options.resume,
|
||||
id: user._id
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
///
|
||||
/// CURRENT USER
|
||||
///
|
||||
Meteor.userId = function () {
|
||||
// This function only works if called inside a method. In theory, it
|
||||
// could also be called from publish statements, since they also
|
||||
// have a userId associated with them. However, given that publish
|
||||
// functions aren't reactive, using any of the infomation from
|
||||
// Meteor.user() in a publish function will always use the value
|
||||
// from when the function first runs. This is likely not what the
|
||||
// user expects. The way to make this work in a publish is to do
|
||||
// Meteor.find(this.userId()).observe and recompute when the user
|
||||
// record changes.
|
||||
var currentInvocation = Meteor._CurrentInvocation.get();
|
||||
if (!currentInvocation)
|
||||
throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.");
|
||||
return currentInvocation.userId;
|
||||
};
|
||||
|
||||
Meteor.user = function () {
|
||||
var userId = Meteor.userId();
|
||||
if (!userId)
|
||||
return null;
|
||||
return Meteor.users.findOne(userId);
|
||||
};
|
||||
|
||||
///
|
||||
/// CREATE USER HOOKS
|
||||
///
|
||||
var onCreateUserHook = null;
|
||||
Accounts.onCreateUser = function (func) {
|
||||
if (onCreateUserHook)
|
||||
throw new Error("Can only call onCreateUser once");
|
||||
else
|
||||
onCreateUserHook = func;
|
||||
};
|
||||
|
||||
// XXX see comment on Accounts.createUser in passwords_server about adding a
|
||||
// second "server options" argument.
|
||||
var defaultCreateUserHook = function (options, user) {
|
||||
if (options.profile)
|
||||
user.profile = options.profile;
|
||||
return user;
|
||||
};
|
||||
Accounts.insertUserDoc = function (options, user) {
|
||||
// - clone user document, to protect from modification
|
||||
// - add createdAt timestamp
|
||||
// - prepare an _id, so that you can modify other collections (eg
|
||||
// create a first task for every new user)
|
||||
//
|
||||
// XXX If the onCreateUser or validateNewUser hooks fail, we might
|
||||
// end up having modified some other collection
|
||||
// inappropriately. The solution is probably to have onCreateUser
|
||||
// accept two callbacks - one that gets called before inserting
|
||||
// the user document (in which you can modify its contents), and
|
||||
// one that gets called after (in which you should change other
|
||||
// collections)
|
||||
user = _.extend({createdAt: +(new Date), _id: Random.id()}, user);
|
||||
|
||||
var result = {};
|
||||
if (options.generateLoginToken) {
|
||||
var stampedToken = Accounts._generateStampedLoginToken();
|
||||
result.token = stampedToken.token;
|
||||
Meteor._ensure(user, 'services', 'resume');
|
||||
if (_.has(user.services.resume, 'loginTokens'))
|
||||
user.services.resume.loginTokens.push(stampedToken);
|
||||
else
|
||||
user.services.resume.loginTokens = [stampedToken];
|
||||
}
|
||||
|
||||
var fullUser;
|
||||
if (onCreateUserHook) {
|
||||
fullUser = onCreateUserHook(options, user);
|
||||
|
||||
// This is *not* part of the API. We need this because we can't isolate
|
||||
// the global server environment between tests, meaning we can't test
|
||||
// both having a create user hook set and not having one set.
|
||||
if (fullUser === 'TEST DEFAULT HOOK')
|
||||
fullUser = defaultCreateUserHook(options, user);
|
||||
} else {
|
||||
fullUser = defaultCreateUserHook(options, user);
|
||||
}
|
||||
|
||||
_.each(validateNewUserHooks, function (hook) {
|
||||
if (!hook(fullUser))
|
||||
throw new Meteor.Error(403, "User validation failed");
|
||||
});
|
||||
|
||||
try {
|
||||
result.id = Meteor.users.insert(fullUser);
|
||||
} catch (e) {
|
||||
// XXX string parsing sucks, maybe
|
||||
// https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day
|
||||
if (e.name !== 'MongoError') throw e;
|
||||
var match = e.err.match(/^E11000 duplicate key error index: ([^ ]+)/);
|
||||
if (!match) throw e;
|
||||
if (match[1].indexOf('$emails.address') !== -1)
|
||||
throw new Meteor.Error(403, "Email already exists.");
|
||||
if (match[1].indexOf('username') !== -1)
|
||||
throw new Meteor.Error(403, "Username already exists.");
|
||||
// XXX better error reporting for services.facebook.id duplicate, etc
|
||||
throw e;
|
||||
}
|
||||
///
|
||||
/// LOGIN HANDLERS
|
||||
///
|
||||
|
||||
Meteor.methods({
|
||||
// @returns {Object|null}
|
||||
// If successful, returns {token: reconnectToken, id: userId}
|
||||
// If unsuccessful (for example, if the user closed the oauth login popup),
|
||||
// returns null
|
||||
login: function(options) {
|
||||
var result = tryAllLoginHandlers(options);
|
||||
if (result !== null)
|
||||
this.setUserId(result.id);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
var validateNewUserHooks = [];
|
||||
Accounts.validateNewUser = function (func) {
|
||||
validateNewUserHooks.push(func);
|
||||
};
|
||||
logout: function() {
|
||||
this.setUserId(null);
|
||||
}
|
||||
});
|
||||
|
||||
Accounts._loginHandlers = [];
|
||||
|
||||
///
|
||||
/// MANAGING USER OBJECTS
|
||||
///
|
||||
// Try all of the registered login handlers until one of them doesn't return
|
||||
// `undefined`, meaning it handled this call to `login`. Return that return
|
||||
// value, which ought to be a {id/token} pair.
|
||||
var tryAllLoginHandlers = function (options) {
|
||||
var result = undefined;
|
||||
|
||||
// Updates or creates a user after we authenticate with a 3rd party.
|
||||
//
|
||||
// @param serviceName {String} Service name (eg, twitter).
|
||||
// @param serviceData {Object} Data to store in the user's record
|
||||
// under services[serviceName]. Must include an "id" field
|
||||
// which is a unique identifier for the user in the service.
|
||||
// @param options {Object, optional} Other options to pass to insertUserDoc
|
||||
// (eg, profile)
|
||||
// @returns {Object} Object with token and id keys, like the result
|
||||
// of the "login" method.
|
||||
Accounts.updateOrCreateUserFromExternalService = function(
|
||||
serviceName, serviceData, options) {
|
||||
options = _.clone(options || {});
|
||||
|
||||
if (serviceName === "password" || serviceName === "resume")
|
||||
throw new Error(
|
||||
"Can't use updateOrCreateUserFromExternalService with internal service "
|
||||
+ serviceName);
|
||||
if (!_.has(serviceData, 'id'))
|
||||
throw new Error(
|
||||
"Service data for service " + serviceName + " must include id");
|
||||
|
||||
// Look for a user with the appropriate service user id.
|
||||
var selector = {};
|
||||
var serviceIdKey = "services." + serviceName + ".id";
|
||||
|
||||
// XXX Temporary special case for Twitter. (Issue #629)
|
||||
// The serviceData.id will be a string representation of an integer.
|
||||
// We want it to match either a stored string or int representation.
|
||||
// This is to cater to earlier versions of Meteor storing twitter
|
||||
// user IDs in number form, and recent versions storing them as strings.
|
||||
// This can be removed once migration technology is in place, and twitter
|
||||
// users stored with integer IDs have been migrated to string IDs.
|
||||
if (serviceName === "twitter" && !isNaN(serviceData.id)) {
|
||||
selector["$or"] = [{},{}];
|
||||
selector["$or"][0][serviceIdKey] = serviceData.id;
|
||||
selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10);
|
||||
} else {
|
||||
selector[serviceIdKey] = serviceData.id;
|
||||
}
|
||||
|
||||
var user = Meteor.users.findOne(selector);
|
||||
|
||||
if (user) {
|
||||
// We *don't* process options (eg, profile) for update, but we do replace
|
||||
// the serviceData (eg, so that we keep an unexpired access token and
|
||||
// don't cache old email addresses in serviceData.email).
|
||||
// XXX provide an onUpdateUser hook which would let apps update
|
||||
// the profile too
|
||||
var stampedToken = Accounts._generateStampedLoginToken();
|
||||
var setAttrs = {};
|
||||
_.each(serviceData, function(value, key) {
|
||||
setAttrs["services." + serviceName + "." + key] = value;
|
||||
});
|
||||
|
||||
// XXX Maybe we should re-use the selector above and notice if the update
|
||||
// touches nothing?
|
||||
Meteor.users.update(
|
||||
user._id,
|
||||
{$set: setAttrs,
|
||||
$push: {'services.resume.loginTokens': stampedToken}});
|
||||
return {token: stampedToken.token, id: user._id};
|
||||
} else {
|
||||
// Create a new user with the service data. Pass other options through to
|
||||
// insertUserDoc.
|
||||
user = {services: {}};
|
||||
user.services[serviceName] = serviceData;
|
||||
options.generateLoginToken = true;
|
||||
return Accounts.insertUserDoc(options, user);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
///
|
||||
/// PUBLISHING DATA
|
||||
///
|
||||
|
||||
// Publish the current user's record to the client.
|
||||
Meteor.publish(null, function() {
|
||||
if (this.userId)
|
||||
return Meteor.users.find(
|
||||
{_id: this.userId},
|
||||
{fields: {profile: 1, username: 1, emails: 1}});
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}, {is_auto: true});
|
||||
|
||||
// If autopublish is on, also publish everyone else's user record.
|
||||
Meteor.default_server.onAutopublish(function () {
|
||||
var handler = function () {
|
||||
return Meteor.users.find(
|
||||
{}, {fields: {profile: 1, username: 1}});
|
||||
};
|
||||
Meteor.default_server.publish(null, handler, {is_auto: true});
|
||||
});
|
||||
|
||||
// Publish all login service configuration fields other than secret.
|
||||
Meteor.publish("meteor.loginServiceConfiguration", function () {
|
||||
return Accounts.loginServiceConfiguration.find({}, {fields: {secret: 0}});
|
||||
}, {is_auto: true}); // not techincally autopublish, but stops the warning.
|
||||
|
||||
// Allow a one-time configuration for a login service. Modifications
|
||||
// to this collection are also allowed in insecure mode.
|
||||
Meteor.methods({
|
||||
"configureLoginService": function(options) {
|
||||
// Don't let random users configure a service we haven't added yet (so
|
||||
// that when we do later add it, it's set up with their configuration
|
||||
// instead of ours).
|
||||
if (!Accounts[options.service])
|
||||
throw new Meteor.Error(403, "Service unknown");
|
||||
if (Accounts.loginServiceConfiguration.findOne({service: options.service}))
|
||||
throw new Meteor.Error(403, "Service " + options.service + " already configured");
|
||||
Accounts.loginServiceConfiguration.insert(options);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
///
|
||||
/// RESTRICTING WRITES TO USER OBJECTS
|
||||
///
|
||||
|
||||
Meteor.users.allow({
|
||||
// clients can modify the profile field of their own document, and
|
||||
// nothing else.
|
||||
update: function (userId, user, fields, modifier) {
|
||||
// make sure it is our record
|
||||
if (user._id !== userId)
|
||||
return false;
|
||||
|
||||
// user can only modify the 'profile' field. sets to multiple
|
||||
// sub-keys (eg profile.foo and profile.bar) are merged into entry
|
||||
// in the fields list.
|
||||
if (fields.length !== 1 || fields[0] !== 'profile')
|
||||
return false;
|
||||
_.find(Accounts._loginHandlers, function(handler) {
|
||||
|
||||
var maybeResult = handler(options);
|
||||
if (maybeResult !== undefined) {
|
||||
result = maybeResult;
|
||||
return true;
|
||||
},
|
||||
fetch: ['_id'] // we only look at _id.
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/// DEFAULT INDEXES ON USERS
|
||||
Meteor.users._ensureIndex('username', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.address', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('services.resume.loginTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
}) ();
|
||||
if (result === undefined) {
|
||||
throw new Meteor.Error(400, "Unrecognized options for login request");
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// @param handler {Function} A function that receives an options object
|
||||
// (as passed as an argument to the `login` method) and returns one of:
|
||||
// - `undefined`, meaning don't handle;
|
||||
// - {id: userId, token: *}, if the user logged in successfully.
|
||||
// - throw an error, if the user failed to log in.
|
||||
Accounts.registerLoginHandler = function(handler) {
|
||||
Accounts._loginHandlers.push(handler);
|
||||
};
|
||||
|
||||
// support reconnecting using a meteor login token
|
||||
Accounts._generateStampedLoginToken = function () {
|
||||
return {token: Random.id(), when: +(new Date)};
|
||||
};
|
||||
|
||||
Accounts.registerLoginHandler(function(options) {
|
||||
if (options.resume) {
|
||||
var user = Meteor.users.findOne(
|
||||
{"services.resume.loginTokens.token": options.resume});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Couldn't find login token");
|
||||
|
||||
return {
|
||||
token: options.resume,
|
||||
id: user._id
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
///
|
||||
/// CURRENT USER
|
||||
///
|
||||
Meteor.userId = function () {
|
||||
// This function only works if called inside a method. In theory, it
|
||||
// could also be called from publish statements, since they also
|
||||
// have a userId associated with them. However, given that publish
|
||||
// functions aren't reactive, using any of the infomation from
|
||||
// Meteor.user() in a publish function will always use the value
|
||||
// from when the function first runs. This is likely not what the
|
||||
// user expects. The way to make this work in a publish is to do
|
||||
// Meteor.find(this.userId()).observe and recompute when the user
|
||||
// record changes.
|
||||
var currentInvocation = Meteor._CurrentInvocation.get();
|
||||
if (!currentInvocation)
|
||||
throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.");
|
||||
return currentInvocation.userId;
|
||||
};
|
||||
|
||||
Meteor.user = function () {
|
||||
var userId = Meteor.userId();
|
||||
if (!userId)
|
||||
return null;
|
||||
return Meteor.users.findOne(userId);
|
||||
};
|
||||
|
||||
///
|
||||
/// CREATE USER HOOKS
|
||||
///
|
||||
var onCreateUserHook = null;
|
||||
Accounts.onCreateUser = function (func) {
|
||||
if (onCreateUserHook)
|
||||
throw new Error("Can only call onCreateUser once");
|
||||
else
|
||||
onCreateUserHook = func;
|
||||
};
|
||||
|
||||
// XXX see comment on Accounts.createUser in passwords_server about adding a
|
||||
// second "server options" argument.
|
||||
var defaultCreateUserHook = function (options, user) {
|
||||
if (options.profile)
|
||||
user.profile = options.profile;
|
||||
return user;
|
||||
};
|
||||
Accounts.insertUserDoc = function (options, user) {
|
||||
// - clone user document, to protect from modification
|
||||
// - add createdAt timestamp
|
||||
// - prepare an _id, so that you can modify other collections (eg
|
||||
// create a first task for every new user)
|
||||
//
|
||||
// XXX If the onCreateUser or validateNewUser hooks fail, we might
|
||||
// end up having modified some other collection
|
||||
// inappropriately. The solution is probably to have onCreateUser
|
||||
// accept two callbacks - one that gets called before inserting
|
||||
// the user document (in which you can modify its contents), and
|
||||
// one that gets called after (in which you should change other
|
||||
// collections)
|
||||
user = _.extend({createdAt: +(new Date), _id: Random.id()}, user);
|
||||
|
||||
var result = {};
|
||||
if (options.generateLoginToken) {
|
||||
var stampedToken = Accounts._generateStampedLoginToken();
|
||||
result.token = stampedToken.token;
|
||||
Meteor._ensure(user, 'services', 'resume');
|
||||
if (_.has(user.services.resume, 'loginTokens'))
|
||||
user.services.resume.loginTokens.push(stampedToken);
|
||||
else
|
||||
user.services.resume.loginTokens = [stampedToken];
|
||||
}
|
||||
|
||||
var fullUser;
|
||||
if (onCreateUserHook) {
|
||||
fullUser = onCreateUserHook(options, user);
|
||||
|
||||
// This is *not* part of the API. We need this because we can't isolate
|
||||
// the global server environment between tests, meaning we can't test
|
||||
// both having a create user hook set and not having one set.
|
||||
if (fullUser === 'TEST DEFAULT HOOK')
|
||||
fullUser = defaultCreateUserHook(options, user);
|
||||
} else {
|
||||
fullUser = defaultCreateUserHook(options, user);
|
||||
}
|
||||
|
||||
_.each(validateNewUserHooks, function (hook) {
|
||||
if (!hook(fullUser))
|
||||
throw new Meteor.Error(403, "User validation failed");
|
||||
});
|
||||
|
||||
try {
|
||||
result.id = Meteor.users.insert(fullUser);
|
||||
} catch (e) {
|
||||
// XXX string parsing sucks, maybe
|
||||
// https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day
|
||||
if (e.name !== 'MongoError') throw e;
|
||||
var match = e.err.match(/^E11000 duplicate key error index: ([^ ]+)/);
|
||||
if (!match) throw e;
|
||||
if (match[1].indexOf('$emails.address') !== -1)
|
||||
throw new Meteor.Error(403, "Email already exists.");
|
||||
if (match[1].indexOf('username') !== -1)
|
||||
throw new Meteor.Error(403, "Username already exists.");
|
||||
// XXX better error reporting for services.facebook.id duplicate, etc
|
||||
throw e;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var validateNewUserHooks = [];
|
||||
Accounts.validateNewUser = function (func) {
|
||||
validateNewUserHooks.push(func);
|
||||
};
|
||||
|
||||
|
||||
///
|
||||
/// MANAGING USER OBJECTS
|
||||
///
|
||||
|
||||
// Updates or creates a user after we authenticate with a 3rd party.
|
||||
//
|
||||
// @param serviceName {String} Service name (eg, twitter).
|
||||
// @param serviceData {Object} Data to store in the user's record
|
||||
// under services[serviceName]. Must include an "id" field
|
||||
// which is a unique identifier for the user in the service.
|
||||
// @param options {Object, optional} Other options to pass to insertUserDoc
|
||||
// (eg, profile)
|
||||
// @returns {Object} Object with token and id keys, like the result
|
||||
// of the "login" method.
|
||||
Accounts.updateOrCreateUserFromExternalService = function(
|
||||
serviceName, serviceData, options) {
|
||||
options = _.clone(options || {});
|
||||
|
||||
if (serviceName === "password" || serviceName === "resume")
|
||||
throw new Error(
|
||||
"Can't use updateOrCreateUserFromExternalService with internal service "
|
||||
+ serviceName);
|
||||
if (!_.has(serviceData, 'id'))
|
||||
throw new Error(
|
||||
"Service data for service " + serviceName + " must include id");
|
||||
|
||||
// Look for a user with the appropriate service user id.
|
||||
var selector = {};
|
||||
var serviceIdKey = "services." + serviceName + ".id";
|
||||
|
||||
// XXX Temporary special case for Twitter. (Issue #629)
|
||||
// The serviceData.id will be a string representation of an integer.
|
||||
// We want it to match either a stored string or int representation.
|
||||
// This is to cater to earlier versions of Meteor storing twitter
|
||||
// user IDs in number form, and recent versions storing them as strings.
|
||||
// This can be removed once migration technology is in place, and twitter
|
||||
// users stored with integer IDs have been migrated to string IDs.
|
||||
if (serviceName === "twitter" && !isNaN(serviceData.id)) {
|
||||
selector["$or"] = [{},{}];
|
||||
selector["$or"][0][serviceIdKey] = serviceData.id;
|
||||
selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10);
|
||||
} else {
|
||||
selector[serviceIdKey] = serviceData.id;
|
||||
}
|
||||
|
||||
var user = Meteor.users.findOne(selector);
|
||||
|
||||
if (user) {
|
||||
// We *don't* process options (eg, profile) for update, but we do replace
|
||||
// the serviceData (eg, so that we keep an unexpired access token and
|
||||
// don't cache old email addresses in serviceData.email).
|
||||
// XXX provide an onUpdateUser hook which would let apps update
|
||||
// the profile too
|
||||
var stampedToken = Accounts._generateStampedLoginToken();
|
||||
var setAttrs = {};
|
||||
_.each(serviceData, function(value, key) {
|
||||
setAttrs["services." + serviceName + "." + key] = value;
|
||||
});
|
||||
|
||||
// XXX Maybe we should re-use the selector above and notice if the update
|
||||
// touches nothing?
|
||||
Meteor.users.update(
|
||||
user._id,
|
||||
{$set: setAttrs,
|
||||
$push: {'services.resume.loginTokens': stampedToken}});
|
||||
return {token: stampedToken.token, id: user._id};
|
||||
} else {
|
||||
// Create a new user with the service data. Pass other options through to
|
||||
// insertUserDoc.
|
||||
user = {services: {}};
|
||||
user.services[serviceName] = serviceData;
|
||||
options.generateLoginToken = true;
|
||||
return Accounts.insertUserDoc(options, user);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
///
|
||||
/// PUBLISHING DATA
|
||||
///
|
||||
|
||||
// Publish the current user's record to the client.
|
||||
Meteor.publish(null, function() {
|
||||
if (this.userId)
|
||||
return Meteor.users.find(
|
||||
{_id: this.userId},
|
||||
{fields: {profile: 1, username: 1, emails: 1}});
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}, {is_auto: true});
|
||||
|
||||
// If autopublish is on, also publish everyone else's user record.
|
||||
Meteor.default_server.onAutopublish(function () {
|
||||
var handler = function () {
|
||||
return Meteor.users.find(
|
||||
{}, {fields: {profile: 1, username: 1}});
|
||||
};
|
||||
Meteor.default_server.publish(null, handler, {is_auto: true});
|
||||
});
|
||||
|
||||
// Publish all login service configuration fields other than secret.
|
||||
Meteor.publish("meteor.loginServiceConfiguration", function () {
|
||||
return Accounts.loginServiceConfiguration.find({}, {fields: {secret: 0}});
|
||||
}, {is_auto: true}); // not techincally autopublish, but stops the warning.
|
||||
|
||||
// Allow a one-time configuration for a login service. Modifications
|
||||
// to this collection are also allowed in insecure mode.
|
||||
Meteor.methods({
|
||||
"configureLoginService": function(options) {
|
||||
// Don't let random users configure a service we haven't added yet (so
|
||||
// that when we do later add it, it's set up with their configuration
|
||||
// instead of ours).
|
||||
if (!Accounts[options.service])
|
||||
throw new Meteor.Error(403, "Service unknown");
|
||||
if (Accounts.loginServiceConfiguration.findOne({service: options.service}))
|
||||
throw new Meteor.Error(403, "Service " + options.service + " already configured");
|
||||
Accounts.loginServiceConfiguration.insert(options);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
///
|
||||
/// RESTRICTING WRITES TO USER OBJECTS
|
||||
///
|
||||
|
||||
Meteor.users.allow({
|
||||
// clients can modify the profile field of their own document, and
|
||||
// nothing else.
|
||||
update: function (userId, user, fields, modifier) {
|
||||
// make sure it is our record
|
||||
if (user._id !== userId)
|
||||
return false;
|
||||
|
||||
// user can only modify the 'profile' field. sets to multiple
|
||||
// sub-keys (eg profile.foo and profile.bar) are merged into entry
|
||||
// in the fields list.
|
||||
if (fields.length !== 1 || fields[0] !== 'profile')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
fetch: ['_id'] // we only look at _id.
|
||||
});
|
||||
|
||||
/// DEFAULT INDEXES ON USERS
|
||||
Meteor.users._ensureIndex('username', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.address', {unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('services.resume.loginTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
|
||||
@@ -54,9 +54,9 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', functio
|
||||
var weiboId2 = Random.id();
|
||||
|
||||
// users that have different service ids get different users
|
||||
uid1 = Accounts.updateOrCreateUserFromExternalService(
|
||||
var uid1 = Accounts.updateOrCreateUserFromExternalService(
|
||||
'weibo', {id: weiboId1}, {profile: {foo: 1}}).id;
|
||||
uid2 = Accounts.updateOrCreateUserFromExternalService(
|
||||
var 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);
|
||||
@@ -70,8 +70,8 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', functio
|
||||
});
|
||||
|
||||
Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', function (test) {
|
||||
var twitterIdOld = 123;
|
||||
var twitterIdNew = '123';
|
||||
var twitterIdOld = parseInt(Random.hexString(4), 16);
|
||||
var twitterIdNew = ''+twitterIdOld;
|
||||
|
||||
// create an account with twitter using the old ID format of integer
|
||||
var uid1 = Accounts.updateOrCreateUserFromExternalService(
|
||||
|
||||
@@ -1,94 +1,91 @@
|
||||
(function() {
|
||||
// To be used as the local storage key
|
||||
var loginTokenKey = "Meteor.loginToken";
|
||||
var userIdKey = "Meteor.userId";
|
||||
// To be used as the local storage key
|
||||
var loginTokenKey = "Meteor.loginToken";
|
||||
var userIdKey = "Meteor.userId";
|
||||
|
||||
// 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.
|
||||
Accounts._isolateLoginTokenForTest = function () {
|
||||
loginTokenKey = loginTokenKey + Random.id();
|
||||
userIdKey = userIdKey + Random.id();
|
||||
};
|
||||
// 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.
|
||||
Accounts._isolateLoginTokenForTest = function () {
|
||||
loginTokenKey = loginTokenKey + Random.id();
|
||||
userIdKey = userIdKey + Random.id();
|
||||
};
|
||||
|
||||
Accounts._storeLoginToken = function(userId, token) {
|
||||
localStorage.setItem(userIdKey, userId);
|
||||
localStorage.setItem(loginTokenKey, token);
|
||||
Accounts._storeLoginToken = function(userId, token) {
|
||||
localStorage.setItem(userIdKey, userId);
|
||||
localStorage.setItem(loginTokenKey, token);
|
||||
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
Accounts._lastLoginTokenWhenPolled = token;
|
||||
};
|
||||
|
||||
Accounts._unstoreLoginToken = function() {
|
||||
localStorage.removeItem(userIdKey);
|
||||
localStorage.removeItem(loginTokenKey);
|
||||
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
Accounts._lastLoginTokenWhenPolled = null;
|
||||
};
|
||||
|
||||
Accounts._storedLoginToken = function() {
|
||||
return localStorage.getItem(loginTokenKey);
|
||||
};
|
||||
|
||||
Accounts._storedUserId = function() {
|
||||
return localStorage.getItem(userIdKey);
|
||||
};
|
||||
|
||||
// Login with a Meteor access token
|
||||
//
|
||||
Meteor.loginWithToken = function (token, callback) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: token}],
|
||||
userCallback: callback});
|
||||
};
|
||||
|
||||
if (!Accounts._preventAutoLogin) {
|
||||
// Immediately try to log in via local storage, so that any DDP
|
||||
// messages are sent after we have established our user account
|
||||
var token = Accounts._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 = Accounts._storedUserId();
|
||||
userId && Meteor.default_connection.setUserId(userId);
|
||||
Meteor.loginWithToken(token, function (err) {
|
||||
if (err) {
|
||||
Meteor._debug("Error logging in with token: " + err);
|
||||
Accounts._makeClientLoggedOut();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Poll local storage every 3 seconds to login if someone logged in in
|
||||
// another tab
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
Accounts._lastLoginTokenWhenPolled = token;
|
||||
Accounts._pollStoredLoginToken = function() {
|
||||
if (Accounts._preventAutoLogin)
|
||||
return;
|
||||
};
|
||||
|
||||
var currentLoginToken = Accounts._storedLoginToken();
|
||||
Accounts._unstoreLoginToken = function() {
|
||||
localStorage.removeItem(userIdKey);
|
||||
localStorage.removeItem(loginTokenKey);
|
||||
|
||||
// != instead of !== just to make sure undefined and null are treated the same
|
||||
if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) {
|
||||
if (currentLoginToken)
|
||||
Meteor.loginWithToken(currentLoginToken); // XXX should we pass a callback here?
|
||||
else
|
||||
Meteor.logout();
|
||||
}
|
||||
Accounts._lastLoginTokenWhenPolled = currentLoginToken;
|
||||
};
|
||||
// to ensure that the localstorage poller doesn't end up trying to
|
||||
// connect a second time
|
||||
Accounts._lastLoginTokenWhenPolled = null;
|
||||
};
|
||||
|
||||
// Semi-internal API. Call this function to re-enable auto login after
|
||||
// if it was disabled at startup.
|
||||
Accounts._enableAutoLogin = function () {
|
||||
Accounts._preventAutoLogin = false;
|
||||
Accounts._pollStoredLoginToken();
|
||||
};
|
||||
Accounts._storedLoginToken = function() {
|
||||
return localStorage.getItem(loginTokenKey);
|
||||
};
|
||||
|
||||
setInterval(Accounts._pollStoredLoginToken, 3000);
|
||||
})();
|
||||
Accounts._storedUserId = function() {
|
||||
return localStorage.getItem(userIdKey);
|
||||
};
|
||||
|
||||
// Login with a Meteor access token
|
||||
//
|
||||
Meteor.loginWithToken = function (token, callback) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: token}],
|
||||
userCallback: callback});
|
||||
};
|
||||
|
||||
if (!Accounts._preventAutoLogin) {
|
||||
// Immediately try to log in via local storage, so that any DDP
|
||||
// messages are sent after we have established our user account
|
||||
var token = Accounts._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 = Accounts._storedUserId();
|
||||
userId && Meteor.default_connection.setUserId(userId);
|
||||
Meteor.loginWithToken(token, function (err) {
|
||||
if (err) {
|
||||
Meteor._debug("Error logging in with token: " + err);
|
||||
Accounts._makeClientLoggedOut();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Poll local storage every 3 seconds to login if someone logged in in
|
||||
// another tab
|
||||
Accounts._lastLoginTokenWhenPolled = token;
|
||||
Accounts._pollStoredLoginToken = function() {
|
||||
if (Accounts._preventAutoLogin)
|
||||
return;
|
||||
|
||||
var currentLoginToken = Accounts._storedLoginToken();
|
||||
|
||||
// != instead of !== just to make sure undefined and null are treated the same
|
||||
if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) {
|
||||
if (currentLoginToken)
|
||||
Meteor.loginWithToken(currentLoginToken); // XXX should we pass a callback here?
|
||||
else
|
||||
Meteor.logout();
|
||||
}
|
||||
Accounts._lastLoginTokenWhenPolled = currentLoginToken;
|
||||
};
|
||||
|
||||
// Semi-internal API. Call this function to re-enable auto login after
|
||||
// if it was disabled at startup.
|
||||
Accounts._enableAutoLogin = function () {
|
||||
Accounts._preventAutoLogin = false;
|
||||
Accounts._pollStoredLoginToken();
|
||||
};
|
||||
|
||||
setInterval(Accounts._pollStoredLoginToken, 3000);
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
(function () {
|
||||
Meteor.loginWithFacebook = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
Meteor.loginWithFacebook = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = Random.id();
|
||||
var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
var display = mobile ? 'touch' : 'popup';
|
||||
|
||||
var scope = "email";
|
||||
if (options && options.requestPermissions)
|
||||
scope = options.requestPermissions.join(',');
|
||||
|
||||
var loginUrl =
|
||||
'https://www.facebook.com/dialog/oauth?client_id=' + config.appId +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') +
|
||||
'&display=' + display + '&scope=' + scope + '&state=' + state;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
})();
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = Random.id();
|
||||
var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
var display = mobile ? 'touch' : 'popup';
|
||||
|
||||
var scope = "email";
|
||||
if (options && options.requestPermissions)
|
||||
scope = options.requestPermissions.join(',');
|
||||
|
||||
var loginUrl =
|
||||
'https://www.facebook.com/dialog/oauth?client_id=' + config.appId +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') +
|
||||
'&display=' + display + '&scope=' + scope + '&state=' + state;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
|
||||
@@ -1,90 +1,87 @@
|
||||
(function () {
|
||||
var querystring = Npm.require('querystring');
|
||||
|
||||
var querystring = __meteor_bootstrap__.require('querystring');
|
||||
Accounts.oauth.registerService('facebook', 2, function(query) {
|
||||
|
||||
Accounts.oauth.registerService('facebook', 2, function(query) {
|
||||
var response = getTokenResponse(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
var response = getTokenResponse(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
// include all fields from facebook
|
||||
// http://developers.facebook.com/docs/reference/login/public-profile-and-friend-list/
|
||||
var whitelisted = ['id', 'email', 'name', 'first_name',
|
||||
'last_name', 'link', 'username', 'gender', 'locale', 'age_range'];
|
||||
|
||||
// include all fields from facebook
|
||||
// http://developers.facebook.com/docs/reference/login/public-profile-and-friend-list/
|
||||
var whitelisted = ['id', 'email', 'name', 'first_name',
|
||||
'last_name', 'link', 'username', 'gender', 'locale', 'age_range'];
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
var getTokenResponse = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
// Request an access token
|
||||
var result = Meteor.http.get(
|
||||
"https://graph.facebook.com/oauth/access_token", {
|
||||
params: {
|
||||
client_id: config.appId,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/facebook?close"),
|
||||
client_secret: config.secret,
|
||||
code: query.code
|
||||
}
|
||||
});
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
var response = result.content;
|
||||
|
||||
// Errors come back as JSON but success looks like a query encoded
|
||||
// in a url
|
||||
var error_response;
|
||||
try {
|
||||
// Just try to parse so that we know if we failed or not,
|
||||
// while storing the parsed results
|
||||
error_response = JSON.parse(response);
|
||||
} catch (e) {
|
||||
error_response = null;
|
||||
}
|
||||
|
||||
if (error_response) {
|
||||
throw new Meteor.Error(500, "Error trying to get access token from Facebook", error_response);
|
||||
} else {
|
||||
// Success! Extract the facebook access token and expiration
|
||||
// time from the response
|
||||
var parsedResponse = querystring.parse(response);
|
||||
var fbAccessToken = parsedResponse.access_token;
|
||||
var fbExpires = parsedResponse.expires;
|
||||
|
||||
if (!fbAccessToken)
|
||||
throw new Meteor.Error(500, "Couldn't find access token in HTTP response.");
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
var getTokenResponse = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'facebook'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get("https://graph.facebook.com/me", {
|
||||
params: {access_token: accessToken}});
|
||||
|
||||
// Request an access token
|
||||
var result = Meteor.http.get(
|
||||
"https://graph.facebook.com/oauth/access_token", {
|
||||
params: {
|
||||
client_id: config.appId,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/facebook?close"),
|
||||
client_secret: config.secret,
|
||||
code: query.code
|
||||
}
|
||||
});
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
var response = result.content;
|
||||
|
||||
// Errors come back as JSON but success looks like a query encoded
|
||||
// in a url
|
||||
var error_response;
|
||||
try {
|
||||
// Just try to parse so that we know if we failed or not,
|
||||
// while storing the parsed results
|
||||
error_response = JSON.parse(response);
|
||||
} catch (e) {
|
||||
error_response = null;
|
||||
}
|
||||
|
||||
if (error_response) {
|
||||
throw new Meteor.Error(500, "Error trying to get access token from Facebook", error_response);
|
||||
} else {
|
||||
// Success! Extract the facebook access token and expiration
|
||||
// time from the response
|
||||
var parsedResponse = querystring.parse(response);
|
||||
var fbAccessToken = parsedResponse.access_token;
|
||||
var fbExpires = parsedResponse.expires;
|
||||
|
||||
if (!fbAccessToken)
|
||||
throw new Meteor.Error(500, "Couldn't find access token in HTTP response.");
|
||||
return {
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get("https://graph.facebook.com/me", {
|
||||
params: {access_token: accessToken}});
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
}) ();
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
(function () {
|
||||
Meteor.loginWithGithub = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
Meteor.loginWithGithub = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'github'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var state = Random.id();
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'github'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var state = Random.id();
|
||||
|
||||
var scope = (options && options.requestPermissions) || [];
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
var scope = (options && options.requestPermissions) || [];
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
|
||||
var loginUrl =
|
||||
'https://github.com/login/oauth/authorize' +
|
||||
'?client_id=' + config.clientId +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') +
|
||||
'&state=' + state;
|
||||
var loginUrl =
|
||||
'https://github.com/login/oauth/authorize' +
|
||||
'?client_id=' + config.clientId +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') +
|
||||
'&state=' + state;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450});
|
||||
};
|
||||
}) ();
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450});
|
||||
};
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
(function () {
|
||||
Accounts.oauth.registerService('github', 2, function(query) {
|
||||
Accounts.oauth.registerService('github', 2, function(query) {
|
||||
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
id: identity.id,
|
||||
accessToken: accessToken,
|
||||
email: identity.email,
|
||||
username: identity.login
|
||||
},
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'github'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://github.com/login/oauth/access_token", {headers: {Accept: 'application/json'}, params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/github?close"),
|
||||
state: query.state
|
||||
}});
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
return result.data.access_token;
|
||||
return {
|
||||
serviceData: {
|
||||
id: identity.id,
|
||||
accessToken: accessToken,
|
||||
email: identity.email,
|
||||
username: identity.login
|
||||
},
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://api.github.com/user",
|
||||
{params: {access_token: accessToken}});
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
}) ();
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'github'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://github.com/login/oauth/access_token", {headers: {Accept: 'application/json'}, params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/github?close"),
|
||||
state: query.state
|
||||
}});
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
return result.data.access_token;
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://api.github.com/user",
|
||||
{params: {access_token: accessToken}});
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
(function () {
|
||||
Meteor.loginWithGoogle = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
Meteor.loginWithGoogle = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
} else if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'google'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'google'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = Random.id();
|
||||
var state = Random.id();
|
||||
|
||||
// always need this to get user id from google.
|
||||
var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile'];
|
||||
var scope = ['https://www.googleapis.com/auth/userinfo.email'];
|
||||
if (options && options.requestPermissions)
|
||||
scope = options.requestPermissions;
|
||||
scope = _.union(scope, requiredScope);
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
// always need this to get user id from google.
|
||||
var requiredScope = ['https://www.googleapis.com/auth/userinfo.profile'];
|
||||
var scope = ['https://www.googleapis.com/auth/userinfo.email'];
|
||||
if (options.requestPermissions)
|
||||
scope = options.requestPermissions;
|
||||
scope = _.union(scope, requiredScope);
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
|
||||
// https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl
|
||||
var accessType = options.requestOfflineToken ? 'offline' : 'online';
|
||||
// https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl
|
||||
var accessType = options.requestOfflineToken ? 'offline' : 'online';
|
||||
|
||||
var loginUrl =
|
||||
'https://accounts.google.com/o/oauth2/auth' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + config.clientId +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') +
|
||||
'&state=' + state +
|
||||
'&access_type=' + accessType;
|
||||
var loginUrl =
|
||||
'https://accounts.google.com/o/oauth2/auth' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + config.clientId +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') +
|
||||
'&state=' + state +
|
||||
'&access_type=' + accessType;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
}) ();
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
|
||||
@@ -1,73 +1,70 @@
|
||||
(function () {
|
||||
Accounts.oauth.registerService('google', 2, function(query) {
|
||||
|
||||
Accounts.oauth.registerService('google', 2, function(query) {
|
||||
var response = getTokens(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
var response = getTokens(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
|
||||
// include all fields from google
|
||||
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
|
||||
var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name',
|
||||
'family_name', 'picture', 'locale', 'timezone', 'gender'];
|
||||
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
// only set the token in serviceData if it's there. this ensures
|
||||
// that we don't lose old ones (since we only get this on the first
|
||||
// log in attempt)
|
||||
if (response.refreshToken)
|
||||
serviceData.refreshToken = response.refreshToken;
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
// - refreshToken, if this is the first authorization request
|
||||
var getTokens = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'google'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://accounts.google.com/o/oauth2/token", {params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/google?close"),
|
||||
grant_type: 'authorization_code'
|
||||
}});
|
||||
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
|
||||
return {
|
||||
accessToken: result.data.access_token,
|
||||
refreshToken: result.data.refresh_token,
|
||||
expiresIn: result.data.expires_in
|
||||
};
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
{params: {access_token: accessToken}});
|
||||
// include all fields from google
|
||||
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
|
||||
var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name',
|
||||
'family_name', 'picture', 'locale', 'timezone', 'gender'];
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
// only set the token in serviceData if it's there. this ensures
|
||||
// that we don't lose old ones (since we only get this on the first
|
||||
// log in attempt)
|
||||
if (response.refreshToken)
|
||||
serviceData.refreshToken = response.refreshToken;
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
})();
|
||||
});
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
// - refreshToken, if this is the first authorization request
|
||||
var getTokens = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'google'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://accounts.google.com/o/oauth2/token", {params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/google?close"),
|
||||
grant_type: 'authorization_code'
|
||||
}});
|
||||
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
|
||||
return {
|
||||
accessToken: result.data.access_token,
|
||||
refreshToken: result.data.refresh_token,
|
||||
expiresIn: result.data.expires_in
|
||||
};
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
{params: {access_token: accessToken}});
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
(function () {
|
||||
Meteor.loginWithMeetup = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
Meteor.loginWithMeetup = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var state = Random.id();
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var state = Random.id();
|
||||
|
||||
var scope = (options && options.requestPermissions) || [];
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
var scope = (options && options.requestPermissions) || [];
|
||||
var flatScope = _.map(scope, encodeURIComponent).join('+');
|
||||
|
||||
var loginUrl =
|
||||
'https://secure.meetup.com/oauth2/authorize' +
|
||||
'?client_id=' + config.clientId +
|
||||
'&response_type=code' +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/meetup?close') +
|
||||
'&state=' + state;
|
||||
var loginUrl =
|
||||
'https://secure.meetup.com/oauth2/authorize' +
|
||||
'?client_id=' + config.clientId +
|
||||
'&response_type=code' +
|
||||
'&scope=' + flatScope +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/meetup?close') +
|
||||
'&state=' + state;
|
||||
|
||||
// meetup box gets taller when permissions requested.
|
||||
var height = 620;
|
||||
if (_.without(scope, 'basic').length)
|
||||
height += 130;
|
||||
// meetup box gets taller when permissions requested.
|
||||
var height = 620;
|
||||
if (_.without(scope, 'basic').length)
|
||||
height += 130;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback,
|
||||
{width: 900, height: height});
|
||||
};
|
||||
}) ();
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback,
|
||||
{width: 900, height: height});
|
||||
};
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
(function () {
|
||||
Accounts.oauth.registerService('meetup', 2, function(query) {
|
||||
Accounts.oauth.registerService('meetup', 2, function(query) {
|
||||
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
id: identity.id,
|
||||
accessToken: accessToken
|
||||
},
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/meetup?close"),
|
||||
state: query.state
|
||||
}});
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
|
||||
return result.data.access_token;
|
||||
return {
|
||||
serviceData: {
|
||||
id: identity.id,
|
||||
accessToken: accessToken
|
||||
},
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://secure.meetup.com/2/members",
|
||||
{params: {member_id: 'self', access_token: accessToken}});
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'meetup'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
return result.data.results && result.data.results[0];
|
||||
};
|
||||
}) ();
|
||||
var result = Meteor.http.post(
|
||||
"https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/meetup?close"),
|
||||
state: query.state
|
||||
}});
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (result.data.error) // if the http response was a json object with an error attribute
|
||||
throw result.data;
|
||||
|
||||
return result.data.access_token;
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
var result = Meteor.http.get(
|
||||
"https://secure.meetup.com/2/members",
|
||||
{params: {member_id: 'self', access_token: accessToken}});
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
|
||||
return result.data.results && result.data.results[0];
|
||||
};
|
||||
|
||||
@@ -1,71 +1,69 @@
|
||||
(function () {
|
||||
// Open a popup window pointing to a OAuth handshake page
|
||||
//
|
||||
// @param state {String} The OAuth state generated by the client
|
||||
// @param url {String} url to page
|
||||
// @param callback {Function} Callback function to call on
|
||||
// completion. Takes one argument, null on success, or Error on
|
||||
// error.
|
||||
// @param dimensions {optional Object(width, height)} The dimensions of
|
||||
// the popup. If not passed defaults to something sane
|
||||
Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) {
|
||||
// XXX these dimensions worked well for facebook and google, but
|
||||
// it's sort of weird to have these here. Maybe an optional
|
||||
// argument instead?
|
||||
var popup = openCenteredPopup(
|
||||
url,
|
||||
(dimensions && dimensions.width) || 650,
|
||||
(dimensions && dimensions.height) || 331);
|
||||
// Open a popup window pointing to a OAuth handshake page
|
||||
//
|
||||
// @param state {String} The OAuth state generated by the client
|
||||
// @param url {String} url to page
|
||||
// @param callback {Function} Callback function to call on
|
||||
// completion. Takes one argument, null on success, or Error on
|
||||
// error.
|
||||
// @param dimensions {optional Object(width, height)} The dimensions of
|
||||
// the popup. If not passed defaults to something sane
|
||||
Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) {
|
||||
// XXX these dimensions worked well for facebook and google, but
|
||||
// it's sort of weird to have these here. Maybe an optional
|
||||
// argument instead?
|
||||
var popup = openCenteredPopup(
|
||||
url,
|
||||
(dimensions && dimensions.width) || 650,
|
||||
(dimensions && dimensions.height) || 331);
|
||||
|
||||
var checkPopupOpen = setInterval(function() {
|
||||
// Fix for #328 - added a second test criteria (popup.closed === undefined)
|
||||
// to humour this Android quirk:
|
||||
// http://code.google.com/p/android/issues/detail?id=21061
|
||||
if (popup.closed || popup.closed === undefined) {
|
||||
clearInterval(checkPopupOpen);
|
||||
tryLoginAfterPopupClosed(state, callback);
|
||||
var checkPopupOpen = setInterval(function() {
|
||||
// Fix for #328 - added a second test criteria (popup.closed === undefined)
|
||||
// to humour this Android quirk:
|
||||
// http://code.google.com/p/android/issues/detail?id=21061
|
||||
if (popup.closed || popup.closed === undefined) {
|
||||
clearInterval(checkPopupOpen);
|
||||
tryLoginAfterPopupClosed(state, callback);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 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.
|
||||
var tryLoginAfterPopupClosed = function(state, callback) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{oauth: {state: state}}],
|
||||
userCallback: callback && function (err) {
|
||||
// Allow server to specify a specify subclass of errors. We should come
|
||||
// up with a more generic way to do this!
|
||||
if (err && err instanceof Meteor.Error &&
|
||||
err.error === Accounts.LoginCancelledError.numericError) {
|
||||
callback(new Accounts.LoginCancelledError(err.details));
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
}});
|
||||
};
|
||||
|
||||
// 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.
|
||||
var tryLoginAfterPopupClosed = function(state, callback) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{oauth: {state: state}}],
|
||||
userCallback: callback && function (err) {
|
||||
// Allow server to specify a specify subclass of errors. We should come
|
||||
// up with a more generic way to do this!
|
||||
if (err && err instanceof Meteor.Error &&
|
||||
err.error === Accounts.LoginCancelledError.numericError) {
|
||||
callback(new Accounts.LoginCancelledError(err.details));
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
}});
|
||||
};
|
||||
var openCenteredPopup = function(url, width, height) {
|
||||
var screenX = typeof window.screenX !== 'undefined'
|
||||
? window.screenX : window.screenLeft;
|
||||
var screenY = typeof window.screenY !== 'undefined'
|
||||
? window.screenY : window.screenTop;
|
||||
var outerWidth = typeof window.outerWidth !== 'undefined'
|
||||
? window.outerWidth : document.body.clientWidth;
|
||||
var outerHeight = typeof window.outerHeight !== 'undefined'
|
||||
? window.outerHeight : (document.body.clientHeight - 22);
|
||||
|
||||
var openCenteredPopup = function(url, width, height) {
|
||||
var screenX = typeof window.screenX !== 'undefined'
|
||||
? window.screenX : window.screenLeft;
|
||||
var screenY = typeof window.screenY !== 'undefined'
|
||||
? window.screenY : window.screenTop;
|
||||
var outerWidth = typeof window.outerWidth !== 'undefined'
|
||||
? window.outerWidth : document.body.clientWidth;
|
||||
var outerHeight = typeof window.outerHeight !== 'undefined'
|
||||
? window.outerHeight : (document.body.clientHeight - 22);
|
||||
// Use `outerWidth - width` and `outerHeight - height` for help in
|
||||
// positioning the popup centered relative to the current window
|
||||
var left = screenX + (outerWidth - width) / 2;
|
||||
var top = screenY + (outerHeight - height) / 2;
|
||||
var features = ('width=' + width + ',height=' + height +
|
||||
',left=' + left + ',top=' + top);
|
||||
|
||||
// Use `outerWidth - width` and `outerHeight - height` for help in
|
||||
// positioning the popup centered relative to the current window
|
||||
var left = screenX + (outerWidth - width) / 2;
|
||||
var top = screenY + (outerHeight - height) / 2;
|
||||
var features = ('width=' + width + ',height=' + height +
|
||||
',left=' + left + ',top=' + top);
|
||||
|
||||
var newwindow = window.open(url, 'Login', features);
|
||||
if (newwindow.focus)
|
||||
newwindow.focus();
|
||||
return newwindow;
|
||||
};
|
||||
})();
|
||||
var newwindow = window.open(url, 'Login', features);
|
||||
if (newwindow.focus)
|
||||
newwindow.focus();
|
||||
return newwindow;
|
||||
};
|
||||
|
||||
@@ -1,192 +1,196 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
var connect = Npm.require("connect");
|
||||
|
||||
Meteor._routePolicy.declare('/_oauth/', 'network');
|
||||
Meteor._routePolicy.declare('/_oauth/', 'network');
|
||||
|
||||
Accounts.oauth._services = {};
|
||||
Accounts.oauth._services = {};
|
||||
|
||||
// Register a handler for an OAuth service. The handler will be called
|
||||
// when we get an incoming http request on /_oauth/{serviceName}. This
|
||||
// handler should use that information to fetch data about the user
|
||||
// logging in.
|
||||
//
|
||||
// @param name {String} e.g. "google", "facebook"
|
||||
// @param version {Number} OAuth version (1 or 2)
|
||||
// @param handleOauthRequest {Function(oauthBinding|query)}
|
||||
// - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider
|
||||
// - (For OAuth2 only) query {Object} parameters passed in query string
|
||||
// - return value is:
|
||||
// - {serviceData:, (optional options:)} where serviceData should end
|
||||
// up in the user's services[name] field
|
||||
// - `null` if the user declined to give permissions
|
||||
Accounts.oauth.registerService = function (name, version, handleOauthRequest) {
|
||||
if (Accounts.oauth._services[name])
|
||||
throw new Error("Already registered the " + name + " OAuth service");
|
||||
// Register a handler for an OAuth service. The handler will be called
|
||||
// when we get an incoming http request on /_oauth/{serviceName}. This
|
||||
// handler should use that information to fetch data about the user
|
||||
// logging in.
|
||||
//
|
||||
// @param name {String} e.g. "google", "facebook"
|
||||
// @param version {Number} OAuth version (1 or 2)
|
||||
// @param handleOauthRequest {Function(oauthBinding|query)}
|
||||
// - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider
|
||||
// - (For OAuth2 only) query {Object} parameters passed in query string
|
||||
// - return value is:
|
||||
// - {serviceData:, (optional options:)} where serviceData should end
|
||||
// up in the user's services[name] field
|
||||
// - `null` if the user declined to give permissions
|
||||
Accounts.oauth.registerService = function (name, version, handleOauthRequest) {
|
||||
if (Accounts.oauth._services[name])
|
||||
throw new Error("Already registered the " + name + " OAuth service");
|
||||
|
||||
// Accounts.updateOrCreateUserFromExternalService does a lookup by this id,
|
||||
// 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});
|
||||
// Accounts.updateOrCreateUserFromExternalService does a lookup by this id,
|
||||
// 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});
|
||||
|
||||
Accounts.oauth._services[name] = {
|
||||
serviceName: name,
|
||||
version: version,
|
||||
handleOauthRequest: handleOauthRequest
|
||||
};
|
||||
Accounts.oauth._services[name] = {
|
||||
serviceName: name,
|
||||
version: version,
|
||||
handleOauthRequest: handleOauthRequest
|
||||
};
|
||||
};
|
||||
|
||||
// When we get an incoming OAuth http request we complete the oauth
|
||||
// handshake, account and token setup before responding. The
|
||||
// results are stored in this map which is then read when the login
|
||||
// method is called. Maps state --> return value of `login`
|
||||
//
|
||||
// XXX we should periodically clear old entries
|
||||
Accounts.oauth._loginResultForState = {};
|
||||
// For test cleanup only. (Mongo has a limit as to how many indexes it can have
|
||||
// per collection.)
|
||||
Accounts.oauth._unregisterService = function (name) {
|
||||
delete Accounts.oauth._services[name];
|
||||
var index = {};
|
||||
index['services.' + name + '.id'] = 1;
|
||||
Meteor.users._dropIndex(index);
|
||||
};
|
||||
|
||||
// Listen to calls to `login` with an oauth option set
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
// When we get an incoming OAuth http request we complete the oauth
|
||||
// handshake, account and token setup before responding. The
|
||||
// results are stored in this map which is then read when the login
|
||||
// method is called. Maps state --> return value of `login`
|
||||
//
|
||||
// XXX we should periodically clear old entries
|
||||
Accounts.oauth._loginResultForState = {};
|
||||
|
||||
var result = Accounts.oauth._loginResultForState[options.oauth.state];
|
||||
if (!result) {
|
||||
// OAuth state is not recognized, which could be either because the popup
|
||||
// was closed by the user before completion, or some sort of error where
|
||||
// the oauth provider didn't talk to our server correctly and closed the
|
||||
// popup somehow.
|
||||
//
|
||||
// we assume it was user canceled, and report it as such, using a
|
||||
// Meteor.Error which the client can recognize. this will mask failures
|
||||
// where things are misconfigured such that the server doesn't see the
|
||||
// request but does close the window. This seems unlikely.
|
||||
throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
|
||||
'No matching login attempt found');
|
||||
} else if (result instanceof Error)
|
||||
// We tried to login, but there was a fatal error. Report it back
|
||||
// to the user.
|
||||
throw result;
|
||||
else
|
||||
return result;
|
||||
// Listen to calls to `login` with an oauth option set
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
|
||||
var result = Accounts.oauth._loginResultForState[options.oauth.state];
|
||||
if (!result) {
|
||||
// OAuth state is not recognized, which could be either because the popup
|
||||
// was closed by the user before completion, or some sort of error where
|
||||
// the oauth provider didn't talk to our server correctly and closed the
|
||||
// popup somehow.
|
||||
//
|
||||
// we assume it was user canceled, and report it as such, using a
|
||||
// Meteor.Error which the client can recognize. this will mask failures
|
||||
// where things are misconfigured such that the server doesn't see the
|
||||
// request but does close the window. This seems unlikely.
|
||||
throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
|
||||
'No matching login attempt found');
|
||||
} else if (result instanceof Error)
|
||||
// We tried to login, but there was a fatal error. Report it back
|
||||
// to the user.
|
||||
throw result;
|
||||
else
|
||||
return result;
|
||||
});
|
||||
|
||||
var Fiber = Npm.require('fibers');
|
||||
// Listen to incoming OAuth http requests
|
||||
__meteor_bootstrap__.app
|
||||
.use(connect.query())
|
||||
.use(function(req, res, next) {
|
||||
// Need to create a Fiber since we're using synchronous http
|
||||
// calls and nothing else is wrapping this in a fiber
|
||||
// automatically
|
||||
Fiber(function () {
|
||||
Accounts.oauth._middleware(req, res, next);
|
||||
}).run();
|
||||
});
|
||||
|
||||
var Fiber = __meteor_bootstrap__.require('fibers');
|
||||
// Listen to incoming OAuth http requests
|
||||
__meteor_bootstrap__.app
|
||||
.use(connect.query())
|
||||
.use(function(req, res, next) {
|
||||
// Need to create a Fiber since we're using synchronous http
|
||||
// calls and nothing else is wrapping this in a fiber
|
||||
// automatically
|
||||
Fiber(function () {
|
||||
Accounts.oauth._middleware(req, res, next);
|
||||
}).run();
|
||||
});
|
||||
|
||||
Accounts.oauth._middleware = function (req, res, next) {
|
||||
// Make sure to catch any exceptions because otherwise we'd crash
|
||||
// the runner
|
||||
try {
|
||||
var serviceName = oauthServiceName(req);
|
||||
if (!serviceName) {
|
||||
// not an oauth request. pass to next middleware.
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var service = Accounts.oauth._services[serviceName];
|
||||
|
||||
// Skip everything if there's no service set by the oauth middleware
|
||||
if (!service)
|
||||
throw new Error("Unexpected OAuth service " + serviceName);
|
||||
|
||||
// Make sure we're configured
|
||||
ensureConfigured(serviceName);
|
||||
|
||||
if (service.version === 1)
|
||||
Accounts.oauth1._handleRequest(service, req.query, res);
|
||||
else if (service.version === 2)
|
||||
Accounts.oauth2._handleRequest(service, req.query, res);
|
||||
else
|
||||
throw new Error("Unexpected OAuth version " + service.version);
|
||||
} catch (err) {
|
||||
// if we got thrown an error, save it off, it will get passed to
|
||||
// the approporiate login call (if any) and reported there.
|
||||
//
|
||||
// The other option would be to display it in the popup tab that
|
||||
// is still open at this point, ignoring the 'close' or 'redirect'
|
||||
// we were passed. But then the developer wouldn't be able to
|
||||
// style the error or react to it in any way.
|
||||
if (req.query.state && err instanceof Error)
|
||||
Accounts.oauth._loginResultForState[req.query.state] = err;
|
||||
|
||||
// also log to the server console, so the developer sees it.
|
||||
Meteor._debug("Exception in oauth request handler", err);
|
||||
|
||||
// XXX the following is actually wrong. if someone wants to
|
||||
// redirect rather than close once we are done with the OAuth
|
||||
// flow, as supported by
|
||||
// Accounts.oauth_renderOauthResults, this will still
|
||||
// close the popup instead. Once we fully support the redirect
|
||||
// flow (by supporting that in places such as
|
||||
// packages/facebook/facebook_client.js) we should revisit this.
|
||||
//
|
||||
// close the popup. because nobody likes them just hanging
|
||||
// there. when someone sees this multiple times they might
|
||||
// think to check server logs (we hope?)
|
||||
closePopup(res);
|
||||
Accounts.oauth._middleware = function (req, res, next) {
|
||||
// Make sure to catch any exceptions because otherwise we'd crash
|
||||
// the runner
|
||||
try {
|
||||
var serviceName = oauthServiceName(req);
|
||||
if (!serviceName) {
|
||||
// not an oauth request. pass to next middleware.
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var service = Accounts.oauth._services[serviceName];
|
||||
|
||||
// Skip everything if there's no service set by the oauth middleware
|
||||
if (!service)
|
||||
throw new Error("Unexpected OAuth service " + serviceName);
|
||||
|
||||
// Make sure we're configured
|
||||
ensureConfigured(serviceName);
|
||||
|
||||
if (service.version === 1)
|
||||
Accounts.oauth1._handleRequest(service, req.query, res);
|
||||
else if (service.version === 2)
|
||||
Accounts.oauth2._handleRequest(service, req.query, res);
|
||||
else
|
||||
throw new Error("Unexpected OAuth version " + service.version);
|
||||
} catch (err) {
|
||||
// if we got thrown an error, save it off, it will get passed to
|
||||
// the approporiate login call (if any) and reported there.
|
||||
//
|
||||
// The other option would be to display it in the popup tab that
|
||||
// is still open at this point, ignoring the 'close' or 'redirect'
|
||||
// we were passed. But then the developer wouldn't be able to
|
||||
// style the error or react to it in any way.
|
||||
if (req.query.state && err instanceof Error)
|
||||
Accounts.oauth._loginResultForState[req.query.state] = err;
|
||||
|
||||
// also log to the server console, so the developer sees it.
|
||||
Meteor._debug("Exception in oauth request handler", err);
|
||||
|
||||
// XXX the following is actually wrong. if someone wants to
|
||||
// redirect rather than close once we are done with the OAuth
|
||||
// flow, as supported by
|
||||
// Accounts.oauth_renderOauthResults, this will still
|
||||
// close the popup instead. Once we fully support the redirect
|
||||
// flow (by supporting that in places such as
|
||||
// packages/facebook/facebook_client.js) we should revisit this.
|
||||
//
|
||||
// close the popup. because nobody likes them just hanging
|
||||
// there. when someone sees this multiple times they might
|
||||
// think to check server logs (we hope?)
|
||||
closePopup(res);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle /_oauth/* paths and extract the service name
|
||||
//
|
||||
// @returns {String|null} e.g. "facebook", or null if this isn't an
|
||||
// oauth request
|
||||
var oauthServiceName = function (req) {
|
||||
|
||||
// req.url will be "/_oauth/<service name>?<action>"
|
||||
var barePath = req.url.substring(0, req.url.indexOf('?'));
|
||||
var splitPath = barePath.split('/');
|
||||
|
||||
// Any non-oauth request will continue down the default
|
||||
// middlewares.
|
||||
if (splitPath[1] !== '_oauth')
|
||||
return null;
|
||||
|
||||
// Find service based on url
|
||||
var serviceName = splitPath[2];
|
||||
return serviceName;
|
||||
};
|
||||
|
||||
// Make sure we're configured
|
||||
var ensureConfigured = function(serviceName) {
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: serviceName})) {
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
};
|
||||
};
|
||||
|
||||
// Handle /_oauth/* paths and extract the service name
|
||||
//
|
||||
// @returns {String|null} e.g. "facebook", or null if this isn't an
|
||||
// oauth request
|
||||
var oauthServiceName = function (req) {
|
||||
|
||||
// req.url will be "/_oauth/<service name>?<action>"
|
||||
var barePath = req.url.substring(0, req.url.indexOf('?'));
|
||||
var splitPath = barePath.split('/');
|
||||
|
||||
// Any non-oauth request will continue down the default
|
||||
// middlewares.
|
||||
if (splitPath[1] !== '_oauth')
|
||||
return null;
|
||||
|
||||
// Find service based on url
|
||||
var serviceName = splitPath[2];
|
||||
return serviceName;
|
||||
};
|
||||
|
||||
// Make sure we're configured
|
||||
var ensureConfigured = function(serviceName) {
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: serviceName})) {
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
};
|
||||
};
|
||||
|
||||
Accounts.oauth._renderOauthResults = function(res, query) {
|
||||
// We support ?close and ?redirect=URL. Any other query should
|
||||
// just serve a blank page
|
||||
if ('close' in query) { // check with 'in' because we don't set a value
|
||||
closePopup(res);
|
||||
} else if (query.redirect) {
|
||||
res.writeHead(302, {'Location': query.redirect});
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('', 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
var closePopup = function(res) {
|
||||
Accounts.oauth._renderOauthResults = function(res, query) {
|
||||
// We support ?close and ?redirect=URL. Any other query should
|
||||
// just serve a blank page
|
||||
if ('close' in query) { // check with 'in' because we don't set a value
|
||||
closePopup(res);
|
||||
} else if (query.redirect) {
|
||||
res.writeHead(302, {'Location': query.redirect});
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
var content =
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
res.end('', 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
var closePopup = function(res) {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
var content =
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var crypto = __meteor_bootstrap__.require("crypto");
|
||||
var querystring = __meteor_bootstrap__.require("querystring");
|
||||
var crypto = Npm.require("crypto");
|
||||
var querystring = Npm.require("querystring");
|
||||
|
||||
// An OAuth1 wrapper around http calls which helps get tokens and
|
||||
// takes care of HTTP headers
|
||||
|
||||
@@ -1,67 +1,64 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
var connect = Npm.require("connect");
|
||||
|
||||
// A place to store request tokens pending verification
|
||||
Accounts.oauth1._requestTokens = {};
|
||||
// A place to store request tokens pending verification
|
||||
Accounts.oauth1._requestTokens = {};
|
||||
|
||||
// connect middleware
|
||||
Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
// connect middleware
|
||||
Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: service.serviceName});
|
||||
if (!config) {
|
||||
throw new Accounts.ConfigError("Service " + service.serviceName + " not configured");
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: service.serviceName});
|
||||
if (!config) {
|
||||
throw new Accounts.ConfigError("Service " + service.serviceName + " not configured");
|
||||
}
|
||||
|
||||
var urls = Accounts[service.serviceName]._urls;
|
||||
var oauthBinding = new OAuth1Binding(
|
||||
config.consumerKey, config.secret, urls);
|
||||
|
||||
if (query.requestTokenAndRedirect) {
|
||||
// step 1 - get and store a request token
|
||||
|
||||
// Get a request token to start auth process
|
||||
oauthBinding.prepareRequestToken(query.requestTokenAndRedirect);
|
||||
|
||||
// Keep track of request token so we can verify it on the next step
|
||||
Accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken;
|
||||
|
||||
// redirect to provider login, which will redirect back to "step 2" below
|
||||
var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken;
|
||||
res.writeHead(302, {'Location': redirectUrl});
|
||||
res.end();
|
||||
|
||||
} else {
|
||||
// step 2, redirected from provider login - complete the login
|
||||
// process: if the user authorized permissions, get an access
|
||||
// token and access token secret and log in as user
|
||||
|
||||
// Get the user's request token so we can verify it and clear it
|
||||
var requestToken = Accounts.oauth1._requestTokens[query.state];
|
||||
delete Accounts.oauth1._requestTokens[query.state];
|
||||
|
||||
// Verify user authorized access and the oauth_token matches
|
||||
// the requestToken from previous step
|
||||
if (query.oauth_token && query.oauth_token === requestToken) {
|
||||
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Get the access token for signing requests
|
||||
oauthBinding.prepareAccessToken(query);
|
||||
|
||||
// Run service-specific handler.
|
||||
var oauthResult = service.handleOauthRequest(oauthBinding);
|
||||
|
||||
// Get or create user doc and login token for reconnect.
|
||||
Accounts.oauth._loginResultForState[query.state] =
|
||||
Accounts.updateOrCreateUserFromExternalService(
|
||||
service.serviceName, oauthResult.serviceData, oauthResult.options);
|
||||
}
|
||||
}
|
||||
|
||||
var urls = Accounts[service.serviceName]._urls;
|
||||
var oauthBinding = new OAuth1Binding(
|
||||
config.consumerKey, config.secret, urls);
|
||||
|
||||
if (query.requestTokenAndRedirect) {
|
||||
// step 1 - get and store a request token
|
||||
|
||||
// Get a request token to start auth process
|
||||
oauthBinding.prepareRequestToken(query.requestTokenAndRedirect);
|
||||
|
||||
// Keep track of request token so we can verify it on the next step
|
||||
Accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken;
|
||||
|
||||
// redirect to provider login, which will redirect back to "step 2" below
|
||||
var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken;
|
||||
res.writeHead(302, {'Location': redirectUrl});
|
||||
res.end();
|
||||
|
||||
} else {
|
||||
// step 2, redirected from provider login - complete the login
|
||||
// process: if the user authorized permissions, get an access
|
||||
// token and access token secret and log in as user
|
||||
|
||||
// Get the user's request token so we can verify it and clear it
|
||||
var requestToken = Accounts.oauth1._requestTokens[query.state];
|
||||
delete Accounts.oauth1._requestTokens[query.state];
|
||||
|
||||
// Verify user authorized access and the oauth_token matches
|
||||
// the requestToken from previous step
|
||||
if (query.oauth_token && query.oauth_token === requestToken) {
|
||||
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Get the access token for signing requests
|
||||
oauthBinding.prepareAccessToken(query);
|
||||
|
||||
// Run service-specific handler.
|
||||
var oauthResult = service.handleOauthRequest(oauthBinding);
|
||||
|
||||
// Get or create user doc and login token for reconnect.
|
||||
Accounts.oauth._loginResultForState[query.state] =
|
||||
Accounts.updateOrCreateUserFromExternalService(
|
||||
service.serviceName, oauthResult.serviceData, oauthResult.options);
|
||||
}
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
})();
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
|
||||
Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var http = Npm.require('http');
|
||||
var twitterfooId = Random.id();
|
||||
var twitterfooName = 'nickname' + Random.id();
|
||||
var twitterfooAccessToken = Random.id();
|
||||
var twitterfooAccessTokenSecret = Random.id();
|
||||
var state = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
OAuth1Binding.prototype.prepareRequestToken = function() {};
|
||||
OAuth1Binding.prototype.prepareAccessToken = function() {
|
||||
@@ -13,13 +14,12 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
this.accessTokenSecret = twitterfooAccessTokenSecret;
|
||||
};
|
||||
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: 'twitterfoo'}))
|
||||
Accounts.loginServiceConfiguration.insert({service: 'twitterfoo'});
|
||||
Accounts.twitterfoo = {};
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// register a fake login service - twitterfoo
|
||||
Accounts.oauth.registerService("twitterfoo", 1, function (query) {
|
||||
// register a fake login service
|
||||
Accounts.oauth.registerService(serviceName, 1, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: twitterfooId,
|
||||
@@ -35,7 +35,7 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/twitterfoo?close",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {
|
||||
state: state,
|
||||
oauth_token: twitterfooAccessToken
|
||||
@@ -44,12 +44,13 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var user = Meteor.users.findOne(
|
||||
{"services.twitterfoo.screenName": twitterfooName});
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".screenName"] = twitterfooName;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.notEqual(user, undefined);
|
||||
test.equal(user.services.twitterfoo.accessToken,
|
||||
test.equal(user.services[serviceName].accessToken,
|
||||
twitterfooAccessToken);
|
||||
test.equal(user.services.twitterfoo.accessTokenSecret,
|
||||
test.equal(user.services[serviceName].accessTokenSecret,
|
||||
twitterfooAccessTokenSecret);
|
||||
|
||||
// and that that user has a login token
|
||||
@@ -63,29 +64,29 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].token, token);
|
||||
} finally {
|
||||
delete Accounts.oauth._services.twitterfoo;
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var http = Npm.require('http');
|
||||
var state = Random.id();
|
||||
var twitterfailId = Random.id();
|
||||
var twitterfailName = 'nickname' + Random.id();
|
||||
var twitterfailAccessToken = Random.id();
|
||||
var twitterfailAccessTokenSecret = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: 'twitterfail'}))
|
||||
Accounts.loginServiceConfiguration.insert({service: 'twitterfail'});
|
||||
Accounts.twitterfail = {};
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
// Wire up access token so that verification passes
|
||||
Accounts.oauth1._requestTokens[state] = twitterfailAccessToken;
|
||||
|
||||
try {
|
||||
// register a failing login service
|
||||
Accounts.oauth.registerService("twitterfail", 1, function (query) {
|
||||
Accounts.oauth.registerService(serviceName, 1, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: twitterfailId,
|
||||
@@ -109,7 +110,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
Meteor._suppress_log(1);
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/twitterfail?close",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {
|
||||
state: state,
|
||||
oauth_token: twitterfailAccessToken
|
||||
@@ -119,7 +120,9 @@ Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
var user = Meteor.users.findOne({"services.twitter.screenName": twitterfailName});
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".screenName"] = twitterfailName;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.equal(user, undefined);
|
||||
|
||||
// verify an error is stored in login state
|
||||
@@ -130,7 +133,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
Meteor.apply('login', [{oauth: {version: 1, state: state}}]);
|
||||
});
|
||||
} finally {
|
||||
delete Accounts.oauth._services.twitterfail;
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
var connect = Npm.require("connect");
|
||||
|
||||
// connect middleware
|
||||
Accounts.oauth2._handleRequest = function (service, query, res) {
|
||||
// check if user authorized access
|
||||
if (!query.error) {
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
// connect middleware
|
||||
Accounts.oauth2._handleRequest = function (service, query, res) {
|
||||
// check if user authorized access
|
||||
if (!query.error) {
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Run service-specific handler.
|
||||
var oauthResult = service.handleOauthRequest(query);
|
||||
// Run service-specific handler.
|
||||
var oauthResult = service.handleOauthRequest(query);
|
||||
|
||||
// Get or create user doc and login token for reconnect.
|
||||
Accounts.oauth._loginResultForState[query.state] =
|
||||
Accounts.updateOrCreateUserFromExternalService(
|
||||
service.serviceName, oauthResult.serviceData, oauthResult.options);
|
||||
}
|
||||
// Get or create user doc and login token for reconnect.
|
||||
Accounts.oauth._loginResultForState[query.state] =
|
||||
Accounts.updateOrCreateUserFromExternalService(
|
||||
service.serviceName, oauthResult.serviceData, oauthResult.options);
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
})();
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var http = Npm.require('http');
|
||||
var foobookId = Random.id();
|
||||
var state = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: 'foobook'}))
|
||||
Accounts.loginServiceConfiguration.insert({service: 'foobook'});
|
||||
Accounts.foobook = {};
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// register a fake login service - foobook
|
||||
Accounts.oauth.registerService("foobook", 2, function (query) {
|
||||
// register a fake login service
|
||||
Accounts.oauth.registerService(serviceName, 2, function (query) {
|
||||
return {serviceData: {id: foobookId}};
|
||||
});
|
||||
|
||||
// simulate logging in using foobook
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/foobook?close",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {state: state}};
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var user = Meteor.users.findOne({"services.foobook.id": foobookId});
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".id"] = foobookId;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.notEqual(user, undefined);
|
||||
test.equal(user.services.foobook.id, foobookId);
|
||||
test.equal(user.services[serviceName].id, foobookId);
|
||||
|
||||
// and that that user has a login token
|
||||
test.equal(user.services.resume.loginTokens.length, 1);
|
||||
@@ -35,23 +37,23 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].token, token);
|
||||
} finally {
|
||||
delete Accounts.oauth._services.foobook;
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var http = Npm.require('http');
|
||||
var state = Random.id();
|
||||
var failbookId = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: 'failbook'}))
|
||||
Accounts.loginServiceConfiguration.insert({service: 'failbook'});
|
||||
Accounts.failbook = {};
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// register a failing login service
|
||||
Accounts.oauth.registerService("failbook", 2, function (query) {
|
||||
Accounts.oauth.registerService(serviceName, 2, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: failbookId
|
||||
@@ -71,12 +73,14 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
// simulate logging in with failure
|
||||
Meteor._suppress_log(1);
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/failbook?close",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {state: state}};
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
var user = Meteor.users.findOne({"services.failbook.id": failbookId});
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".id"] = failbookId;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.equal(user, undefined);
|
||||
|
||||
// verify an error is stored in login state
|
||||
@@ -87,7 +91,7 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
Meteor.apply('login', [{oauth: {version: 2, state: state}}]);
|
||||
});
|
||||
} finally {
|
||||
delete Accounts.oauth._services.failbook;
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,218 +1,218 @@
|
||||
(function () {
|
||||
// intentionally initialize later so that we can debug tests after
|
||||
// they fail without trying to recreate a user with the same email
|
||||
// address
|
||||
var email1;
|
||||
var email2;
|
||||
var email3;
|
||||
var email4;
|
||||
// intentionally initialize later so that we can debug tests after
|
||||
// they fail without trying to recreate a user with the same email
|
||||
// address
|
||||
var email1;
|
||||
var email2;
|
||||
var email3;
|
||||
var email4;
|
||||
|
||||
var resetPasswordToken;
|
||||
var verifyEmailToken;
|
||||
var enrollAccountToken;
|
||||
var resetPasswordToken;
|
||||
var verifyEmailToken;
|
||||
var enrollAccountToken;
|
||||
|
||||
Accounts._isolateLoginTokenForTest();
|
||||
Accounts._isolateLoginTokenForTest();
|
||||
|
||||
testAsyncMulti("accounts emails - reset password flow", [
|
||||
function (test, expect) {
|
||||
email1 = Random.id() + "-intercept@example.com";
|
||||
Accounts.createUser({email: email1, password: 'foobar'},
|
||||
expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.forgotPassword({email: email1}, expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email1, expect(function (error, result) {
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 2); // the first is the email verification
|
||||
var content = result[1];
|
||||
testAsyncMulti("accounts emails - reset password flow", [
|
||||
function (test, expect) {
|
||||
email1 = Random.id() + "-intercept@example.com";
|
||||
Accounts.createUser({email: email1, password: 'foobar'},
|
||||
expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.forgotPassword({email: email1}, expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email1, expect(function (error, result) {
|
||||
test.equal(error, undefined);
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 2); // the first is the email verification
|
||||
var content = result[1];
|
||||
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
resetPasswordToken = match[1];
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.resetPassword(resetPasswordToken, "newPassword", expect(function(error) {
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
resetPasswordToken = match[1];
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.resetPassword(resetPasswordToken, "newPassword", expect(function(error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(
|
||||
{email: email1}, "newPassword",
|
||||
expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword(
|
||||
{email: email1}, "newPassword",
|
||||
expect(function (error) {
|
||||
test.isFalse(error);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
var getVerifyEmailToken = function (email, test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email, expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 1);
|
||||
var content = result[0];
|
||||
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/verify-email/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
verifyEmailToken = match[1];
|
||||
}));
|
||||
};
|
||||
|
||||
var loggedIn = function (test, expect) {
|
||||
return expect(function (error) {
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.isTrue(Meteor.user());
|
||||
});
|
||||
};
|
||||
|
||||
testAsyncMulti("accounts emails - verify email flow", [
|
||||
function (test, expect) {
|
||||
email2 = Random.id() + "-intercept@example.com";
|
||||
email3 = Random.id() + "-intercept@example.com";
|
||||
Accounts.createUser(
|
||||
{email: email2, password: 'foobar'},
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email2);
|
||||
test.isFalse(Meteor.user().emails[0].verified);
|
||||
// We should NOT be publishing things like verification tokens!
|
||||
test.isFalse(_.has(Meteor.user(), 'services'));
|
||||
},
|
||||
function (test, expect) {
|
||||
getVerifyEmailToken(email2, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
// Log out, to test that verifyEmail logs us back in.
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.verifyEmail(verifyEmailToken,
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email2);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call(
|
||||
"addEmailForTestAndVerify", email3,
|
||||
expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
test.equal(Meteor.user().emails.length, 2);
|
||||
test.equal(Meteor.user().emails[1].address, email3);
|
||||
test.isFalse(Meteor.user().emails[1].verified);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
getVerifyEmailToken(email3, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
// Log out, to test that verifyEmail logs us back in. (And if we don't
|
||||
// do that, waitUntilLoggedIn won't be able to prevent race conditions.)
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.verifyEmail(verifyEmailToken,
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails[1].address, email3);
|
||||
test.isTrue(Meteor.user().emails[1].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
var getEnrollAccountToken = function (email, test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email, expect(function (error, result) {
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 1);
|
||||
var content = result[0];
|
||||
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
enrollAccountToken = match[1];
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("accounts emails - enroll account flow", [
|
||||
function (test, expect) {
|
||||
email4 = Random.id() + "-intercept@example.com";
|
||||
Meteor.call("createUserOnServer", email4,
|
||||
expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
var user = result;
|
||||
test.equal(user.emails.length, 1);
|
||||
test.equal(user.emails[0].address, email4);
|
||||
test.isFalse(user.emails[0].verified);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
getEnrollAccountToken(email4, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.resetPassword(enrollAccountToken, 'password',
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email4);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
var getVerifyEmailToken = function (email, test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email, expect(function (error, result) {
|
||||
test.equal(error, undefined);
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 1);
|
||||
var content = result[0];
|
||||
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/verify-email/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
verifyEmailToken = match[1];
|
||||
}));
|
||||
};
|
||||
|
||||
var loggedIn = function (test, expect) {
|
||||
return expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.isTrue(Meteor.user());
|
||||
});
|
||||
};
|
||||
|
||||
testAsyncMulti("accounts emails - verify email flow", [
|
||||
function (test, expect) {
|
||||
email2 = Random.id() + "-intercept@example.com";
|
||||
email3 = Random.id() + "-intercept@example.com";
|
||||
Accounts.createUser(
|
||||
{email: email2, password: 'foobar'},
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email2);
|
||||
test.isFalse(Meteor.user().emails[0].verified);
|
||||
// We should NOT be publishing things like verification tokens!
|
||||
test.isFalse(_.has(Meteor.user(), 'services'));
|
||||
},
|
||||
function (test, expect) {
|
||||
getVerifyEmailToken(email2, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
// Log out, to test that verifyEmail logs us back in.
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.verifyEmail(verifyEmailToken,
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email2);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.call(
|
||||
"addEmailForTestAndVerify", email3,
|
||||
expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
test.equal(Meteor.user().emails.length, 2);
|
||||
test.equal(Meteor.user().emails[1].address, email3);
|
||||
test.isFalse(Meteor.user().emails[1].verified);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword({email: email4}, 'password',
|
||||
loggedIn(test ,expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email4);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
},
|
||||
function (test, expect) {
|
||||
getVerifyEmailToken(email3, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
// Log out, to test that verifyEmail logs us back in. (And if we don't
|
||||
// do that, waitUntilLoggedIn won't be able to prevent race conditions.)
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.verifyEmail(verifyEmailToken,
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails[1].address, email3);
|
||||
test.isTrue(Meteor.user().emails[1].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
var getEnrollAccountToken = function (email, test, expect) {
|
||||
Meteor.call("getInterceptedEmails", email, expect(function (error, result) {
|
||||
test.equal(error, undefined);
|
||||
test.notEqual(result, undefined);
|
||||
test.equal(result.length, 1);
|
||||
var content = result[0];
|
||||
|
||||
var match = content.match(
|
||||
new RegExp(Meteor.absoluteUrl() + "#/enroll-account/(\\S*)"));
|
||||
test.isTrue(match);
|
||||
enrollAccountToken = match[1];
|
||||
}));
|
||||
};
|
||||
|
||||
testAsyncMulti("accounts emails - enroll account flow", [
|
||||
function (test, expect) {
|
||||
email4 = Random.id() + "-intercept@example.com";
|
||||
Meteor.call("createUserOnServer", email4,
|
||||
expect(function (error, result) {
|
||||
test.isFalse(error);
|
||||
var user = result;
|
||||
test.equal(user.emails.length, 1);
|
||||
test.equal(user.emails[0].address, email4);
|
||||
test.isFalse(user.emails[0].verified);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
}) ();
|
||||
},
|
||||
function (test, expect) {
|
||||
getEnrollAccountToken(email4, test, expect);
|
||||
},
|
||||
function (test, expect) {
|
||||
Accounts.resetPassword(enrollAccountToken, 'password',
|
||||
loggedIn(test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email4);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.loginWithPassword({email: email4}, 'password',
|
||||
loggedIn(test ,expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
test.equal(Meteor.user().emails.length, 1);
|
||||
test.equal(Meteor.user().emails[0].address, email4);
|
||||
test.isTrue(Meteor.user().emails[0].verified);
|
||||
},
|
||||
function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
(function () {
|
||||
//
|
||||
// a mechanism to intercept emails sent to addressing including
|
||||
// the string "intercept", storing them in an array that can then
|
||||
// be retrieved using the getInterceptedEmails method
|
||||
//
|
||||
var oldEmailSend = Email.send;
|
||||
var interceptedEmails = {}; // (email address) -> (array of contents)
|
||||
//
|
||||
// a mechanism to intercept emails sent to addressing including
|
||||
// the string "intercept", storing them in an array that can then
|
||||
// be retrieved using the getInterceptedEmails method
|
||||
//
|
||||
var oldEmailSend = Email.send;
|
||||
var interceptedEmails = {}; // (email address) -> (array of contents)
|
||||
|
||||
Email.send = function (options) {
|
||||
var to = options.to;
|
||||
if (to.indexOf('intercept') === -1) {
|
||||
oldEmailSend(options);
|
||||
} else {
|
||||
if (!interceptedEmails[to])
|
||||
interceptedEmails[to] = [];
|
||||
Email.send = function (options) {
|
||||
var to = options.to;
|
||||
if (to.indexOf('intercept') === -1) {
|
||||
oldEmailSend(options);
|
||||
} else {
|
||||
if (!interceptedEmails[to])
|
||||
interceptedEmails[to] = [];
|
||||
|
||||
interceptedEmails[to].push(options.text);
|
||||
}
|
||||
};
|
||||
interceptedEmails[to].push(options.text);
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
getInterceptedEmails: function (email) {
|
||||
return interceptedEmails[email];
|
||||
},
|
||||
Meteor.methods({
|
||||
getInterceptedEmails: function (email) {
|
||||
return interceptedEmails[email];
|
||||
},
|
||||
|
||||
addEmailForTestAndVerify: function (email) {
|
||||
Meteor.users.update(
|
||||
{_id: this.userId},
|
||||
{$push: {emails: {address: email, verified: false}}});
|
||||
Accounts.sendVerificationEmail(this.userId, email);
|
||||
},
|
||||
addEmailForTestAndVerify: function (email) {
|
||||
Meteor.users.update(
|
||||
{_id: this.userId},
|
||||
{$push: {emails: {address: email, verified: false}}});
|
||||
Accounts.sendVerificationEmail(this.userId, email);
|
||||
},
|
||||
|
||||
createUserOnServer: function (email) {
|
||||
var userId = Accounts.createUser({email: email});
|
||||
Accounts.sendEnrollmentEmail(userId);
|
||||
return Meteor.users.findOne(userId);
|
||||
}
|
||||
});
|
||||
}) ();
|
||||
createUserOnServer: function (email) {
|
||||
var userId = Accounts.createUser({email: email});
|
||||
Accounts.sendEnrollmentEmail(userId);
|
||||
return Meteor.users.findOne(userId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,158 +1,156 @@
|
||||
(function () {
|
||||
Accounts.createUser = function (options, callback) {
|
||||
options = _.clone(options); // we'll be modifying options
|
||||
Accounts.createUser = function (options, callback) {
|
||||
options = _.clone(options); // we'll be modifying options
|
||||
|
||||
if (!options.password)
|
||||
throw new Error("Must set options.password");
|
||||
var verifier = Meteor._srp.generateVerifier(options.password);
|
||||
// strip old password, replacing with the verifier object
|
||||
delete options.password;
|
||||
options.srp = verifier;
|
||||
if (!options.password)
|
||||
throw new Error("Must set options.password");
|
||||
var verifier = Meteor._srp.generateVerifier(options.password);
|
||||
// strip old password, replacing with the verifier object
|
||||
delete options.password;
|
||||
options.srp = verifier;
|
||||
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'createUser',
|
||||
methodArguments: [options],
|
||||
userCallback: callback
|
||||
});
|
||||
};
|
||||
|
||||
// @param selector {String|Object} One of the following:
|
||||
// - {username: (username)}
|
||||
// - {email: (email)}
|
||||
// - a string which may be a username or email, depending on whether
|
||||
// it contains "@".
|
||||
// @param password {String}
|
||||
// @param callback {Function(error|undefined)}
|
||||
Meteor.loginWithPassword = function (selector, password, callback) {
|
||||
var srp = new Meteor._srp.Client(password);
|
||||
var request = srp.startExchange();
|
||||
|
||||
if (typeof selector === 'string')
|
||||
if (selector.indexOf('@') === -1)
|
||||
selector = {username: selector};
|
||||
else
|
||||
selector = {email: selector};
|
||||
|
||||
request.user = selector;
|
||||
|
||||
// Normally, we only set Meteor.loggingIn() to true within
|
||||
// Accounts.callLoginMethod, but we'd also like it to be true during the
|
||||
// password exchange. So we set it to true here, and clear it on error; in
|
||||
// the non-error case, it gets cleared by callLoginMethod.
|
||||
Accounts._setLoggingIn(true);
|
||||
Meteor.apply('beginPasswordExchange', [request], function (error, result) {
|
||||
if (error || !result) {
|
||||
Accounts._setLoggingIn(false);
|
||||
error = error || new Error("No result from call to beginPasswordExchange");
|
||||
callback && callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = srp.respondToChallenge(result);
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'createUser',
|
||||
methodArguments: [options],
|
||||
userCallback: callback
|
||||
methodArguments: [{srp: response}],
|
||||
validateResult: function (result) {
|
||||
if (!srp.verifyConfirmation({HAMK: result.HAMK}))
|
||||
throw new Error("Server is cheating!");
|
||||
},
|
||||
userCallback: callback});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// @param oldPassword {String|null}
|
||||
// @param newPassword {String}
|
||||
// @param callback {Function(error|undefined)}
|
||||
Accounts.changePassword = function (oldPassword, newPassword, callback) {
|
||||
if (!Meteor.user()) {
|
||||
callback && callback(new Error("Must be logged in to change password."));
|
||||
return;
|
||||
}
|
||||
|
||||
var verifier = Meteor._srp.generateVerifier(newPassword);
|
||||
|
||||
if (!oldPassword) {
|
||||
Meteor.apply('changePassword', [{srp: verifier}], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// @param selector {String|Object} One of the following:
|
||||
// - {username: (username)}
|
||||
// - {email: (email)}
|
||||
// - a string which may be a username or email, depending on whether
|
||||
// it contains "@".
|
||||
// @param password {String}
|
||||
// @param callback {Function(error|undefined)}
|
||||
Meteor.loginWithPassword = function (selector, password, callback) {
|
||||
var srp = new Meteor._srp.Client(password);
|
||||
} else { // oldPassword
|
||||
var srp = new Meteor._srp.Client(oldPassword);
|
||||
var request = srp.startExchange();
|
||||
|
||||
if (typeof selector === 'string')
|
||||
if (selector.indexOf('@') === -1)
|
||||
selector = {username: selector};
|
||||
else
|
||||
selector = {email: selector};
|
||||
|
||||
request.user = selector;
|
||||
|
||||
// Normally, we only set Meteor.loggingIn() to true within
|
||||
// Accounts.callLoginMethod, but we'd also like it to be true during the
|
||||
// password exchange. So we set it to true here, and clear it on error; in
|
||||
// the non-error case, it gets cleared by callLoginMethod.
|
||||
Accounts._setLoggingIn(true);
|
||||
request.user = {id: Meteor.user()._id};
|
||||
Meteor.apply('beginPasswordExchange', [request], function (error, result) {
|
||||
if (error || !result) {
|
||||
Accounts._setLoggingIn(false);
|
||||
error = error || new Error("No result from call to beginPasswordExchange");
|
||||
callback && callback(error);
|
||||
callback && callback(
|
||||
error || new Error("No result from call to beginPasswordExchange"));
|
||||
return;
|
||||
}
|
||||
|
||||
var response = srp.respondToChallenge(result);
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{srp: response}],
|
||||
validateResult: function (result) {
|
||||
if (!srp.verifyConfirmation({HAMK: result.HAMK}))
|
||||
throw new Error("Server is cheating!");
|
||||
},
|
||||
userCallback: callback});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// @param oldPassword {String|null}
|
||||
// @param newPassword {String}
|
||||
// @param callback {Function(error|undefined)}
|
||||
Accounts.changePassword = function (oldPassword, newPassword, callback) {
|
||||
if (!Meteor.user()) {
|
||||
callback && callback(new Error("Must be logged in to change password."));
|
||||
return;
|
||||
}
|
||||
|
||||
var verifier = Meteor._srp.generateVerifier(newPassword);
|
||||
|
||||
if (!oldPassword) {
|
||||
Meteor.apply('changePassword', [{srp: verifier}], function (error, result) {
|
||||
response.srp = verifier;
|
||||
Meteor.apply('changePassword', [response], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
} else { // oldPassword
|
||||
var srp = new Meteor._srp.Client(oldPassword);
|
||||
var request = srp.startExchange();
|
||||
request.user = {id: Meteor.user()._id};
|
||||
Meteor.apply('beginPasswordExchange', [request], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from call to beginPasswordExchange"));
|
||||
return;
|
||||
}
|
||||
|
||||
var response = srp.respondToChallenge(result);
|
||||
response.srp = verifier;
|
||||
Meteor.apply('changePassword', [response], function (error, result) {
|
||||
if (error || !result) {
|
||||
callback && callback(
|
||||
error || new Error("No result from changePassword."));
|
||||
if (!srp.verifyConfirmation(result)) {
|
||||
// Monkey business!
|
||||
callback && callback(new Error("Old password verification failed."));
|
||||
} else {
|
||||
if (!srp.verifyConfirmation(result)) {
|
||||
// Monkey business!
|
||||
callback && callback(new Error("Old password verification failed."));
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
callback && callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Sends an email to a user with a link that can be used to reset
|
||||
// their password
|
||||
//
|
||||
// @param options {Object}
|
||||
// - email: (email)
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.forgotPassword = function(options, callback) {
|
||||
if (!options.email)
|
||||
throw new Error("Must pass options.email");
|
||||
Meteor.call("forgotPassword", options, callback);
|
||||
};
|
||||
// Sends an email to a user with a link that can be used to reset
|
||||
// their password
|
||||
//
|
||||
// @param options {Object}
|
||||
// - email: (email)
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.forgotPassword = function(options, callback) {
|
||||
if (!options.email)
|
||||
throw new Error("Must pass options.email");
|
||||
Meteor.call("forgotPassword", options, callback);
|
||||
};
|
||||
|
||||
// Resets a password based on a token originally created by
|
||||
// Accounts.forgotPassword, and then logs in the matching user.
|
||||
//
|
||||
// @param token {String}
|
||||
// @param newPassword {String}
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.resetPassword = function(token, newPassword, callback) {
|
||||
if (!token)
|
||||
throw new Error("Need to pass token");
|
||||
if (!newPassword)
|
||||
throw new Error("Need to pass newPassword");
|
||||
// Resets a password based on a token originally created by
|
||||
// Accounts.forgotPassword, and then logs in the matching user.
|
||||
//
|
||||
// @param token {String}
|
||||
// @param newPassword {String}
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.resetPassword = function(token, newPassword, callback) {
|
||||
if (!token)
|
||||
throw new Error("Need to pass token");
|
||||
if (!newPassword)
|
||||
throw new Error("Need to pass newPassword");
|
||||
|
||||
var verifier = Meteor._srp.generateVerifier(newPassword);
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'resetPassword',
|
||||
methodArguments: [token, verifier],
|
||||
userCallback: callback});
|
||||
};
|
||||
var verifier = Meteor._srp.generateVerifier(newPassword);
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'resetPassword',
|
||||
methodArguments: [token, verifier],
|
||||
userCallback: callback});
|
||||
};
|
||||
|
||||
// Verifies a user's email address based on a token originally
|
||||
// created by Accounts.sendVerificationEmail
|
||||
//
|
||||
// @param token {String}
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.verifyEmail = function(token, callback) {
|
||||
if (!token)
|
||||
throw new Error("Need to pass token");
|
||||
// Verifies a user's email address based on a token originally
|
||||
// created by Accounts.sendVerificationEmail
|
||||
//
|
||||
// @param token {String}
|
||||
// @param callback (optional) {Function(error|undefined)}
|
||||
Accounts.verifyEmail = function(token, callback) {
|
||||
if (!token)
|
||||
throw new Error("Need to pass token");
|
||||
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'verifyEmail',
|
||||
methodArguments: [token],
|
||||
userCallback: callback});
|
||||
};
|
||||
})();
|
||||
Accounts.callLoginMethod({
|
||||
methodName: 'verifyEmail',
|
||||
methodArguments: [token],
|
||||
userCallback: callback});
|
||||
};
|
||||
|
||||
@@ -1,334 +1,33 @@
|
||||
(function () {
|
||||
var selectorFromUserQuery = function (user) {
|
||||
if (!user)
|
||||
throw new Meteor.Error(400, "Must pass a user property in request");
|
||||
if (_.keys(user).length !== 1)
|
||||
throw new Meteor.Error(400, "User property must have exactly one field");
|
||||
var selectorFromUserQuery = function (user) {
|
||||
if (!user)
|
||||
throw new Meteor.Error(400, "Must pass a user property in request");
|
||||
if (_.keys(user).length !== 1)
|
||||
throw new Meteor.Error(400, "User property must have exactly one field");
|
||||
|
||||
var selector;
|
||||
if (user.id)
|
||||
selector = {_id: user.id};
|
||||
else if (user.username)
|
||||
selector = {username: user.username};
|
||||
else if (user.email)
|
||||
selector = {"emails.address": user.email};
|
||||
else
|
||||
throw new Meteor.Error(400, "Must pass username, email, or id in request.user");
|
||||
var selector;
|
||||
if (user.id)
|
||||
selector = {_id: user.id};
|
||||
else if (user.username)
|
||||
selector = {username: user.username};
|
||||
else if (user.email)
|
||||
selector = {"emails.address": user.email};
|
||||
else
|
||||
throw new Meteor.Error(400, "Must pass username, email, or id in request.user");
|
||||
|
||||
return selector;
|
||||
};
|
||||
return selector;
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
// @param request {Object} with fields:
|
||||
// user: either {username: (username)}, {email: (email)}, or {id: (userId)}
|
||||
// A: hex encoded int. the client's public key for this exchange
|
||||
// @returns {Object} with fields:
|
||||
// identity: random string ID
|
||||
// salt: random string ID
|
||||
// B: hex encoded int. server's public key for this exchange
|
||||
beginPasswordExchange: function (request) {
|
||||
var selector = selectorFromUserQuery(request.user);
|
||||
Meteor.methods({
|
||||
// @param request {Object} with fields:
|
||||
// user: either {username: (username)}, {email: (email)}, or {id: (userId)}
|
||||
// A: hex encoded int. the client's public key for this exchange
|
||||
// @returns {Object} with fields:
|
||||
// identity: random string ID
|
||||
// salt: random string ID
|
||||
// B: hex encoded int. server's public key for this exchange
|
||||
beginPasswordExchange: function (request) {
|
||||
var selector = selectorFromUserQuery(request.user);
|
||||
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
if (!user.services || !user.services.password ||
|
||||
!user.services.password.srp)
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
var verifier = user.services.password.srp;
|
||||
var srp = new Meteor._srp.Server(verifier);
|
||||
var challenge = srp.issueChallenge({A: request.A});
|
||||
|
||||
// save off results in the current session so we can verify them
|
||||
// later.
|
||||
this._sessionData.srpChallenge =
|
||||
{ userId: user._id, M: srp.M, HAMK: srp.HAMK };
|
||||
|
||||
return challenge;
|
||||
},
|
||||
|
||||
changePassword: function (options) {
|
||||
if (!this.userId)
|
||||
throw new Meteor.Error(401, "Must be logged in");
|
||||
|
||||
// If options.M is set, it means we went through a challenge with
|
||||
// the old password.
|
||||
|
||||
if (!options.M /* could allow unsafe password changes here */) {
|
||||
throw new Meteor.Error(403, "Old password required.");
|
||||
}
|
||||
|
||||
if (options.M) {
|
||||
var serialized = this._sessionData.srpChallenge;
|
||||
if (!serialized || serialized.M !== options.M)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
if (serialized.userId !== this.userId)
|
||||
// No monkey business!
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// Only can use challenges once.
|
||||
delete this._sessionData.srpChallenge;
|
||||
}
|
||||
|
||||
var verifier = options.srp;
|
||||
if (!verifier && options.password) {
|
||||
verifier = Meteor._srp.generateVerifier(options.password);
|
||||
}
|
||||
if (!verifier || !verifier.identity || !verifier.salt ||
|
||||
!verifier.verifier)
|
||||
throw new Meteor.Error(400, "Invalid verifier");
|
||||
|
||||
// XXX this should invalidate all login tokens other than the current one
|
||||
// (or it should assign a new login token, replacing existing ones)
|
||||
Meteor.users.update({_id: this.userId},
|
||||
{$set: {'services.password.srp': verifier}});
|
||||
|
||||
var ret = {passwordChanged: true};
|
||||
if (serialized)
|
||||
ret.HAMK = serialized.HAMK;
|
||||
return ret;
|
||||
},
|
||||
|
||||
forgotPassword: function (options) {
|
||||
var email = options.email;
|
||||
if (!email)
|
||||
throw new Meteor.Error(400, "Need to set options.email");
|
||||
|
||||
var user = Meteor.users.findOne({"emails.address": email});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
Accounts.sendResetPasswordEmail(user._id, email);
|
||||
},
|
||||
|
||||
resetPassword: function (token, newVerifier) {
|
||||
if (!token)
|
||||
throw new Meteor.Error(400, "Need to pass token");
|
||||
if (!newVerifier)
|
||||
throw new Meteor.Error(400, "Need to pass newVerifier");
|
||||
|
||||
var user = Meteor.users.findOne({"services.password.reset.token": token});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Token expired");
|
||||
var email = user.services.password.reset.email;
|
||||
if (!_.include(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Meteor.Error(403, "Token has invalid email address");
|
||||
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
|
||||
// Update the user record by:
|
||||
// - Changing the password verifier to the new one
|
||||
// - Replacing all valid login tokens with new ones (changing
|
||||
// password should invalidate existing sessions).
|
||||
// - Forgetting about the reset token that was just used
|
||||
// - Verifying their email, since they got the password reset via email.
|
||||
Meteor.users.update({_id: user._id, 'emails.address': email}, {
|
||||
$set: {'services.password.srp': newVerifier,
|
||||
'services.resume.loginTokens': [stampedLoginToken],
|
||||
'emails.$.verified': true},
|
||||
$unset: {'services.password.reset': 1}
|
||||
});
|
||||
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
},
|
||||
|
||||
verifyEmail: function (token) {
|
||||
if (!token)
|
||||
throw new Meteor.Error(400, "Need to pass token");
|
||||
|
||||
var 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;
|
||||
});
|
||||
if (!tokenRecord)
|
||||
throw new Meteor.Error(403, "Verify email link expired");
|
||||
|
||||
var emailsRecord = _.find(user.emails, function (e) {
|
||||
return e.address == tokenRecord.address;
|
||||
});
|
||||
if (!emailsRecord)
|
||||
throw new Meteor.Error(403, "Verify email link is for unknown address");
|
||||
|
||||
// Log the user in with a new login token.
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
|
||||
// By including the address in the query, we can use 'emails.$' in the
|
||||
// modifier to get a reference to the specific object in the emails
|
||||
// array. See
|
||||
// http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator)
|
||||
// http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull
|
||||
Meteor.users.update(
|
||||
{_id: user._id,
|
||||
'emails.address': tokenRecord.address},
|
||||
{$set: {'emails.$.verified': true},
|
||||
$pull: {'services.email.verificationTokens': {token: token}},
|
||||
$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// send the user an email with a link that when opened allows the user
|
||||
// to set a new password, without the old password.
|
||||
Accounts.sendResetPasswordEmail = function (userId, email) {
|
||||
// Make sure the user exists, and email is one of their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first email if we weren't passed an email.
|
||||
if (!email && user.emails && user.emails[0])
|
||||
email = user.emails[0].address;
|
||||
// make sure we have a valid email
|
||||
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Error("No such email for user.");
|
||||
|
||||
var token = Random.id();
|
||||
var when = +(new Date);
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
}});
|
||||
|
||||
var resetPasswordUrl = Accounts.urls.resetPassword(token);
|
||||
Email.send({
|
||||
to: email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.resetPassword.subject(user),
|
||||
text: Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl)});
|
||||
};
|
||||
|
||||
|
||||
// send the user an email with a link that when opened marks that
|
||||
// address as verified
|
||||
Accounts.sendVerificationEmail = function (userId, address) {
|
||||
// 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.
|
||||
|
||||
// Make sure the user exists, and address is one of their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first unverified address if we weren't passed an address.
|
||||
if (!address) {
|
||||
var email = _.find(user.emails || [],
|
||||
function (e) { return !e.verified; });
|
||||
address = (email || {}).address;
|
||||
}
|
||||
// make sure we have a valid address
|
||||
if (!address || !_.contains(_.pluck(user.emails || [], 'address'), address))
|
||||
throw new Error("No such email address for user.");
|
||||
|
||||
|
||||
var tokenRecord = {
|
||||
token: Random.id(),
|
||||
address: address,
|
||||
when: +(new Date)};
|
||||
Meteor.users.update(
|
||||
{_id: userId},
|
||||
{$push: {'services.email.verificationTokens': tokenRecord}});
|
||||
|
||||
var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token);
|
||||
Email.send({
|
||||
to: address,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.verifyEmail.subject(user),
|
||||
text: Accounts.emailTemplates.verifyEmail.text(user, verifyEmailUrl)
|
||||
});
|
||||
};
|
||||
|
||||
// send the user an email informing them that their account was created, with
|
||||
// a link that when opened both marks their email as verified and forces them
|
||||
// to choose their password. The email must be one of the addresses in the
|
||||
// user's emails field, or undefined to pick the first email automatically.
|
||||
Accounts.sendEnrollmentEmail = function (userId, email) {
|
||||
// XXX refactor! This is basically identical to sendResetPasswordEmail.
|
||||
|
||||
// Make sure the user exists, and email is in their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first email if we weren't passed an email.
|
||||
if (!email && user.emails && user.emails[0])
|
||||
email = user.emails[0].address;
|
||||
// make sure we have a valid email
|
||||
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Error("No such email for user.");
|
||||
|
||||
|
||||
var token = Random.id();
|
||||
var when = +(new Date);
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
}});
|
||||
|
||||
var enrollAccountUrl = Accounts.urls.enrollAccount(token);
|
||||
Email.send({
|
||||
to: email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.enrollAccount.subject(user),
|
||||
text: Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// handler to login with password
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.srp)
|
||||
return undefined; // don't handle
|
||||
if (!options.srp.M)
|
||||
throw new Meteor.Error(400, "Must pass M in options.srp");
|
||||
|
||||
// we're always called from within a 'login' method, so this should
|
||||
// be safe.
|
||||
var currentInvocation = Meteor._CurrentInvocation.get();
|
||||
var serialized = currentInvocation._sessionData.srpChallenge;
|
||||
if (!serialized || serialized.M !== options.srp.M)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// Only can use challenges once.
|
||||
delete currentInvocation._sessionData.srpChallenge;
|
||||
|
||||
var userId = serialized.userId;
|
||||
var user = Meteor.users.findOne(userId);
|
||||
// Was the user deleted since the start of this challenge?
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
Meteor.users.update(
|
||||
userId, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
return {token: stampedLoginToken.token, id: userId, HAMK: serialized.HAMK};
|
||||
});
|
||||
|
||||
// handler to login with plaintext password.
|
||||
//
|
||||
// The meteor client doesn't use this, it is for other DDP clients who
|
||||
// haven't implemented SRP. Since it sends the password in plaintext
|
||||
// over the wire, it should only be run over SSL!
|
||||
//
|
||||
// Also, it might be nice if servers could turn this off. Or maybe it
|
||||
// should be opt-in, not opt-out? Accounts.config option?
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.password || !options.user)
|
||||
return undefined; // don't handle
|
||||
|
||||
var selector = selectorFromUserQuery(options.user);
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
@@ -337,125 +36,424 @@
|
||||
!user.services.password.srp)
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
// Just check the verifier output when the same identity and salt
|
||||
// are passed. Don't bother with a full exchange.
|
||||
var verifier = user.services.password.srp;
|
||||
var newVerifier = Meteor._srp.generateVerifier(options.password, {
|
||||
identity: verifier.identity, salt: verifier.salt});
|
||||
var srp = new Meteor._srp.Server(verifier);
|
||||
var challenge = srp.issueChallenge({A: request.A});
|
||||
|
||||
if (verifier.verifier !== newVerifier.verifier)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// save off results in the current session so we can verify them
|
||||
// later.
|
||||
this._sessionData.srpChallenge =
|
||||
{ userId: user._id, M: srp.M, HAMK: srp.HAMK };
|
||||
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
Meteor.users.update(
|
||||
user._id, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
return challenge;
|
||||
},
|
||||
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
});
|
||||
changePassword: function (options) {
|
||||
if (!this.userId)
|
||||
throw new Meteor.Error(401, "Must be logged in");
|
||||
|
||||
// If options.M is set, it means we went through a challenge with
|
||||
// the old password.
|
||||
|
||||
Accounts.setPassword = function (userId, newPassword) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!options.M /* could allow unsafe password changes here */) {
|
||||
throw new Meteor.Error(403, "Old password required.");
|
||||
}
|
||||
|
||||
if (options.M) {
|
||||
var serialized = this._sessionData.srpChallenge;
|
||||
if (!serialized || serialized.M !== options.M)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
if (serialized.userId !== this.userId)
|
||||
// No monkey business!
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// Only can use challenges once.
|
||||
delete this._sessionData.srpChallenge;
|
||||
}
|
||||
|
||||
var verifier = options.srp;
|
||||
if (!verifier && options.password) {
|
||||
verifier = Meteor._srp.generateVerifier(options.password);
|
||||
}
|
||||
if (!verifier || !verifier.identity || !verifier.salt ||
|
||||
!verifier.verifier)
|
||||
throw new Meteor.Error(400, "Invalid verifier");
|
||||
|
||||
// XXX this should invalidate all login tokens other than the current one
|
||||
// (or it should assign a new login token, replacing existing ones)
|
||||
Meteor.users.update({_id: this.userId},
|
||||
{$set: {'services.password.srp': verifier}});
|
||||
|
||||
var ret = {passwordChanged: true};
|
||||
if (serialized)
|
||||
ret.HAMK = serialized.HAMK;
|
||||
return ret;
|
||||
},
|
||||
|
||||
forgotPassword: function (options) {
|
||||
var email = options.email;
|
||||
if (!email)
|
||||
throw new Meteor.Error(400, "Need to set options.email");
|
||||
|
||||
var user = Meteor.users.findOne({"emails.address": email});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
var newVerifier = Meteor._srp.generateVerifier(newPassword);
|
||||
|
||||
Meteor.users.update({_id: user._id}, {
|
||||
$set: {'services.password.srp': newVerifier}});
|
||||
};
|
||||
Accounts.sendResetPasswordEmail(user._id, email);
|
||||
},
|
||||
|
||||
resetPassword: function (token, newVerifier) {
|
||||
if (!token)
|
||||
throw new Meteor.Error(400, "Need to pass token");
|
||||
if (!newVerifier)
|
||||
throw new Meteor.Error(400, "Need to pass newVerifier");
|
||||
|
||||
var user = Meteor.users.findOne({"services.password.reset.token": token});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Token expired");
|
||||
var email = user.services.password.reset.email;
|
||||
if (!_.include(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Meteor.Error(403, "Token has invalid email address");
|
||||
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
|
||||
// Update the user record by:
|
||||
// - Changing the password verifier to the new one
|
||||
// - Replacing all valid login tokens with new ones (changing
|
||||
// password should invalidate existing sessions).
|
||||
// - Forgetting about the reset token that was just used
|
||||
// - Verifying their email, since they got the password reset via email.
|
||||
Meteor.users.update({_id: user._id, 'emails.address': email}, {
|
||||
$set: {'services.password.srp': newVerifier,
|
||||
'services.resume.loginTokens': [stampedLoginToken],
|
||||
'emails.$.verified': true},
|
||||
$unset: {'services.password.reset': 1}
|
||||
});
|
||||
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
},
|
||||
|
||||
verifyEmail: function (token) {
|
||||
if (!token)
|
||||
throw new Meteor.Error(400, "Need to pass token");
|
||||
|
||||
var 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;
|
||||
});
|
||||
if (!tokenRecord)
|
||||
throw new Meteor.Error(403, "Verify email link expired");
|
||||
|
||||
var emailsRecord = _.find(user.emails, function (e) {
|
||||
return e.address == tokenRecord.address;
|
||||
});
|
||||
if (!emailsRecord)
|
||||
throw new Meteor.Error(403, "Verify email link is for unknown address");
|
||||
|
||||
// Log the user in with a new login token.
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
|
||||
// By including the address in the query, we can use 'emails.$' in the
|
||||
// modifier to get a reference to the specific object in the emails
|
||||
// array. See
|
||||
// http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator)
|
||||
// http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull
|
||||
Meteor.users.update(
|
||||
{_id: user._id,
|
||||
'emails.address': tokenRecord.address},
|
||||
{$set: {'emails.$.verified': true},
|
||||
$pull: {'services.email.verificationTokens': {token: token}},
|
||||
$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
this.setUserId(user._id);
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
////////////
|
||||
// Creating users:
|
||||
// send the user an email with a link that when opened allows the user
|
||||
// to set a new password, without the old password.
|
||||
Accounts.sendResetPasswordEmail = function (userId, email) {
|
||||
// Make sure the user exists, and email is one of their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first email if we weren't passed an email.
|
||||
if (!email && user.emails && user.emails[0])
|
||||
email = user.emails[0].address;
|
||||
// make sure we have a valid email
|
||||
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Error("No such email for user.");
|
||||
|
||||
|
||||
// Shared createUser function called from the createUser method, both
|
||||
// if originates in client or server code. Calls user provided hooks,
|
||||
// does the actual user insertion.
|
||||
//
|
||||
// returns an object with id: userId, and (if options.generateLoginToken is
|
||||
// set) token: loginToken.
|
||||
var createUser = function (options) {
|
||||
var username = options.username;
|
||||
var email = options.email;
|
||||
if (!username && !email)
|
||||
throw new Meteor.Error(400, "Need to set a username or email");
|
||||
|
||||
// Raw password. The meteor client doesn't send this, but a DDP
|
||||
// client that didn't implement SRP could send this. This should
|
||||
// only be done over SSL.
|
||||
if (options.password) {
|
||||
if (options.srp)
|
||||
throw new Meteor.Error(400, "Don't pass both password and srp in options");
|
||||
options.srp = Meteor._srp.generateVerifier(options.password);
|
||||
var token = Random.id();
|
||||
var when = +(new Date);
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
}});
|
||||
|
||||
var user = {services: {}};
|
||||
if (options.srp)
|
||||
user.services.password = {srp: options.srp}; // XXX validate verifier
|
||||
if (username)
|
||||
user.username = username;
|
||||
if (email)
|
||||
user.emails = [{address: email, verified: false}];
|
||||
var resetPasswordUrl = Accounts.urls.resetPassword(token);
|
||||
Email.send({
|
||||
to: email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.resetPassword.subject(user),
|
||||
text: Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl)});
|
||||
};
|
||||
|
||||
return Accounts.insertUserDoc(options, user);
|
||||
};
|
||||
|
||||
// method for create user. Requests come from the client.
|
||||
Meteor.methods({
|
||||
createUser: function (options) {
|
||||
options = _.clone(options);
|
||||
options.generateLoginToken = true;
|
||||
if (Accounts._options.forbidClientAccountCreation)
|
||||
throw new Meteor.Error(403, "Signups forbidden");
|
||||
// send the user an email with a link that when opened marks that
|
||||
// address as verified
|
||||
Accounts.sendVerificationEmail = function (userId, address) {
|
||||
// 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.
|
||||
|
||||
// Create user. result contains id and token.
|
||||
var result = createUser(options);
|
||||
// safety belt. createUser is supposed to throw on error. send 500 error
|
||||
// instead of sending a verification email with empty userid.
|
||||
if (!result.id)
|
||||
throw new Error("createUser failed to insert new user");
|
||||
// Make sure the user exists, and address is one of their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first unverified address if we weren't passed an address.
|
||||
if (!address) {
|
||||
var email = _.find(user.emails || [],
|
||||
function (e) { return !e.verified; });
|
||||
address = (email || {}).address;
|
||||
}
|
||||
// make sure we have a valid address
|
||||
if (!address || !_.contains(_.pluck(user.emails || [], 'address'), address))
|
||||
throw new Error("No such email address for user.");
|
||||
|
||||
// If `Accounts._options.sendVerificationEmail` is set, register
|
||||
// a token to verify the user's primary email, and send it to
|
||||
// that address.
|
||||
if (options.email && Accounts._options.sendVerificationEmail)
|
||||
Accounts.sendVerificationEmail(result.id, options.email);
|
||||
|
||||
// client gets logged in as the new user afterwards.
|
||||
this.setUserId(result.id);
|
||||
return result;
|
||||
}
|
||||
var tokenRecord = {
|
||||
token: Random.id(),
|
||||
address: address,
|
||||
when: +(new Date)};
|
||||
Meteor.users.update(
|
||||
{_id: userId},
|
||||
{$push: {'services.email.verificationTokens': tokenRecord}});
|
||||
|
||||
var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token);
|
||||
Email.send({
|
||||
to: address,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.verifyEmail.subject(user),
|
||||
text: Accounts.emailTemplates.verifyEmail.text(user, verifyEmailUrl)
|
||||
});
|
||||
};
|
||||
|
||||
// Create user directly on the server.
|
||||
//
|
||||
// Unlike the client version, this does not log you in as this user
|
||||
// after creation.
|
||||
//
|
||||
// returns userId or throws an error if it can't create
|
||||
//
|
||||
// XXX add another argument ("server options") that gets sent to onCreateUser,
|
||||
// which is always empty when called from the createUser method? eg, "admin:
|
||||
// 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);
|
||||
options.generateLoginToken = false;
|
||||
// send the user an email informing them that their account was created, with
|
||||
// a link that when opened both marks their email as verified and forces them
|
||||
// to choose their password. The email must be one of the addresses in the
|
||||
// user's emails field, or undefined to pick the first email automatically.
|
||||
Accounts.sendEnrollmentEmail = function (userId, email) {
|
||||
// XXX refactor! This is basically identical to sendResetPasswordEmail.
|
||||
|
||||
// XXX allow an optional callback?
|
||||
if (callback) {
|
||||
throw new Error("Accounts.createUser with callback not supported on the server yet.");
|
||||
// Make sure the user exists, and email is in their addresses.
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Error("Can't find user");
|
||||
// pick the first email if we weren't passed an email.
|
||||
if (!email && user.emails && user.emails[0])
|
||||
email = user.emails[0].address;
|
||||
// make sure we have a valid email
|
||||
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
|
||||
throw new Error("No such email for user.");
|
||||
|
||||
|
||||
var token = Random.id();
|
||||
var when = +(new Date);
|
||||
Meteor.users.update(userId, {$set: {
|
||||
"services.password.reset": {
|
||||
token: token,
|
||||
email: email,
|
||||
when: when
|
||||
}
|
||||
}});
|
||||
|
||||
var userId = createUser(options).id;
|
||||
var enrollAccountUrl = Accounts.urls.enrollAccount(token);
|
||||
Email.send({
|
||||
to: email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: Accounts.emailTemplates.enrollAccount.subject(user),
|
||||
text: Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl)
|
||||
});
|
||||
};
|
||||
|
||||
return userId;
|
||||
};
|
||||
|
||||
// PASSWORD-SPECIFIC INDEXES ON USERS
|
||||
Meteor.users._ensureIndex('emails.validationTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.password.reset.token',
|
||||
{unique: 1, sparse: 1});
|
||||
})();
|
||||
// handler to login with password
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.srp)
|
||||
return undefined; // don't handle
|
||||
if (!options.srp.M)
|
||||
throw new Meteor.Error(400, "Must pass M in options.srp");
|
||||
|
||||
// we're always called from within a 'login' method, so this should
|
||||
// be safe.
|
||||
var currentInvocation = Meteor._CurrentInvocation.get();
|
||||
var serialized = currentInvocation._sessionData.srpChallenge;
|
||||
if (!serialized || serialized.M !== options.srp.M)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
// Only can use challenges once.
|
||||
delete currentInvocation._sessionData.srpChallenge;
|
||||
|
||||
var userId = serialized.userId;
|
||||
var user = Meteor.users.findOne(userId);
|
||||
// Was the user deleted since the start of this challenge?
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
Meteor.users.update(
|
||||
userId, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
return {token: stampedLoginToken.token, id: userId, HAMK: serialized.HAMK};
|
||||
});
|
||||
|
||||
// handler to login with plaintext password.
|
||||
//
|
||||
// The meteor client doesn't use this, it is for other DDP clients who
|
||||
// haven't implemented SRP. Since it sends the password in plaintext
|
||||
// over the wire, it should only be run over SSL!
|
||||
//
|
||||
// Also, it might be nice if servers could turn this off. Or maybe it
|
||||
// should be opt-in, not opt-out? Accounts.config option?
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.password || !options.user)
|
||||
return undefined; // don't handle
|
||||
|
||||
var selector = selectorFromUserQuery(options.user);
|
||||
var user = Meteor.users.findOne(selector);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
if (!user.services || !user.services.password ||
|
||||
!user.services.password.srp)
|
||||
throw new Meteor.Error(403, "User has no password set");
|
||||
|
||||
// Just check the verifier output when the same identity and salt
|
||||
// are passed. Don't bother with a full exchange.
|
||||
var verifier = user.services.password.srp;
|
||||
var newVerifier = Meteor._srp.generateVerifier(options.password, {
|
||||
identity: verifier.identity, salt: verifier.salt});
|
||||
|
||||
if (verifier.verifier !== newVerifier.verifier)
|
||||
throw new Meteor.Error(403, "Incorrect password");
|
||||
|
||||
var stampedLoginToken = Accounts._generateStampedLoginToken();
|
||||
Meteor.users.update(
|
||||
user._id, {$push: {'services.resume.loginTokens': stampedLoginToken}});
|
||||
|
||||
return {token: stampedLoginToken.token, id: user._id};
|
||||
});
|
||||
|
||||
|
||||
Accounts.setPassword = function (userId, newPassword) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
var newVerifier = Meteor._srp.generateVerifier(newPassword);
|
||||
|
||||
Meteor.users.update({_id: user._id}, {
|
||||
$set: {'services.password.srp': newVerifier}});
|
||||
};
|
||||
|
||||
|
||||
////////////
|
||||
// Creating users:
|
||||
|
||||
|
||||
// Shared createUser function called from the createUser method, both
|
||||
// if originates in client or server code. Calls user provided hooks,
|
||||
// does the actual user insertion.
|
||||
//
|
||||
// returns an object with id: userId, and (if options.generateLoginToken is
|
||||
// set) token: loginToken.
|
||||
var createUser = function (options) {
|
||||
var username = options.username;
|
||||
var email = options.email;
|
||||
if (!username && !email)
|
||||
throw new Meteor.Error(400, "Need to set a username or email");
|
||||
|
||||
// Raw password. The meteor client doesn't send this, but a DDP
|
||||
// client that didn't implement SRP could send this. This should
|
||||
// only be done over SSL.
|
||||
if (options.password) {
|
||||
if (options.srp)
|
||||
throw new Meteor.Error(400, "Don't pass both password and srp in options");
|
||||
options.srp = Meteor._srp.generateVerifier(options.password);
|
||||
}
|
||||
|
||||
var user = {services: {}};
|
||||
if (options.srp)
|
||||
user.services.password = {srp: options.srp}; // XXX validate verifier
|
||||
if (username)
|
||||
user.username = username;
|
||||
if (email)
|
||||
user.emails = [{address: email, verified: false}];
|
||||
|
||||
return Accounts.insertUserDoc(options, user);
|
||||
};
|
||||
|
||||
// method for create user. Requests come from the client.
|
||||
Meteor.methods({
|
||||
createUser: function (options) {
|
||||
options = _.clone(options);
|
||||
options.generateLoginToken = true;
|
||||
if (Accounts._options.forbidClientAccountCreation)
|
||||
throw new Meteor.Error(403, "Signups forbidden");
|
||||
|
||||
// Create user. result contains id and token.
|
||||
var result = createUser(options);
|
||||
// safety belt. createUser is supposed to throw on error. send 500 error
|
||||
// instead of sending a verification email with empty userid.
|
||||
if (!result.id)
|
||||
throw new Error("createUser failed to insert new user");
|
||||
|
||||
// If `Accounts._options.sendVerificationEmail` is set, register
|
||||
// a token to verify the user's primary email, and send it to
|
||||
// that address.
|
||||
if (options.email && Accounts._options.sendVerificationEmail)
|
||||
Accounts.sendVerificationEmail(result.id, options.email);
|
||||
|
||||
// client gets logged in as the new user afterwards.
|
||||
this.setUserId(result.id);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Create user directly on the server.
|
||||
//
|
||||
// Unlike the client version, this does not log you in as this user
|
||||
// after creation.
|
||||
//
|
||||
// returns userId or throws an error if it can't create
|
||||
//
|
||||
// XXX add another argument ("server options") that gets sent to onCreateUser,
|
||||
// which is always empty when called from the createUser method? eg, "admin:
|
||||
// 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);
|
||||
options.generateLoginToken = false;
|
||||
|
||||
// XXX allow an optional callback?
|
||||
if (callback) {
|
||||
throw new Error("Accounts.createUser with callback not supported on the server yet.");
|
||||
}
|
||||
|
||||
var userId = createUser(options).id;
|
||||
|
||||
return userId;
|
||||
};
|
||||
|
||||
// PASSWORD-SPECIFIC INDEXES ON USERS
|
||||
Meteor.users._ensureIndex('emails.validationTokens.token',
|
||||
{unique: 1, sparse: 1});
|
||||
Meteor.users._ensureIndex('emails.password.reset.token',
|
||||
{unique: 1, sparse: 1});
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
(function () {
|
||||
// XXX support options.requestPermissions as we do for Facebook, Google, Github
|
||||
Meteor.loginWithTwitter = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
// XXX support options.requestPermissions as we do for Facebook, Google, Github
|
||||
Meteor.loginWithTwitter = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'twitter'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'twitter'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = Random.id();
|
||||
// We need to keep state across the next two 'steps' so we're adding
|
||||
// a state parameter to the url and the callback url that we'll be returned
|
||||
// to by oauth provider
|
||||
var state = Random.id();
|
||||
// We need to keep state across the next two 'steps' so we're adding
|
||||
// a state parameter to the url and the callback url that we'll be returned
|
||||
// to by oauth provider
|
||||
|
||||
// url back to app, enters "step 2" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state);
|
||||
// url back to app, enters "step 2" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state);
|
||||
|
||||
// url to app, enters "step 1" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var url = '/_oauth/twitter/?requestTokenAndRedirect='
|
||||
+ encodeURIComponent(callbackUrl)
|
||||
+ '&state=' + state;
|
||||
// url to app, enters "step 1" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var url = '/_oauth/twitter/?requestTokenAndRedirect='
|
||||
+ encodeURIComponent(callbackUrl)
|
||||
+ '&state=' + state;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, url, callback);
|
||||
};
|
||||
})();
|
||||
Accounts.oauth.initiateLogin(state, url, callback);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
(function () {
|
||||
Accounts.oauth.registerService('twitter', 1, function(oauthBinding) {
|
||||
var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data;
|
||||
|
||||
Accounts.oauth.registerService('twitter', 1, function(oauthBinding) {
|
||||
var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data;
|
||||
var serviceData = {
|
||||
id: identity.id_str,
|
||||
screenName: identity.screen_name,
|
||||
accessToken: oauthBinding.accessToken,
|
||||
accessTokenSecret: oauthBinding.accessTokenSecret
|
||||
};
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
id: identity.id_str,
|
||||
screenName: identity.screen_name,
|
||||
accessToken: oauthBinding.accessToken,
|
||||
accessTokenSecret: oauthBinding.accessTokenSecret
|
||||
},
|
||||
options: {
|
||||
profile: {
|
||||
name: identity.name
|
||||
}
|
||||
// include helpful fields from twitter
|
||||
// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials
|
||||
var whitelisted = ['profile_image_url', 'profile_image_url_https', 'lang'];
|
||||
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {
|
||||
profile: {
|
||||
name: identity.name
|
||||
}
|
||||
};
|
||||
});
|
||||
}) ();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,205 +1,202 @@
|
||||
(function () {
|
||||
if (!Accounts._loginButtons)
|
||||
Accounts._loginButtons = {};
|
||||
if (!Accounts._loginButtons)
|
||||
Accounts._loginButtons = {};
|
||||
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
Handlebars.registerHelper(
|
||||
"loginButtons",
|
||||
function (options) {
|
||||
if (options.hash.align === "right")
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "right"}));
|
||||
else
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "left"}));
|
||||
Handlebars.registerHelper(
|
||||
"loginButtons",
|
||||
function (options) {
|
||||
if (options.hash.align === "right")
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "right"}));
|
||||
else
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "left"}));
|
||||
});
|
||||
|
||||
// shared between dropdown and single mode
|
||||
Template._loginButtons.events({
|
||||
'click #login-buttons-logout': function() {
|
||||
Meteor.logout(function () {
|
||||
loginButtonsSession.closeDropdown();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// shared between dropdown and single mode
|
||||
Template._loginButtons.events({
|
||||
'click #login-buttons-logout': function() {
|
||||
Meteor.logout(function () {
|
||||
loginButtonsSession.closeDropdown();
|
||||
});
|
||||
}
|
||||
});
|
||||
Template._loginButtons.preserve({
|
||||
'input[id]': Spark._labelFromIdOrName
|
||||
});
|
||||
|
||||
Template._loginButtons.preserve({
|
||||
'input[id]': Spark._labelFromIdOrName
|
||||
});
|
||||
//
|
||||
// loginButtonLoggedOut template
|
||||
//
|
||||
|
||||
//
|
||||
// loginButtonLoggedOut template
|
||||
//
|
||||
Template._loginButtonsLoggedOut.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOut.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
Template._loginButtonsLoggedOut.services = function () {
|
||||
return Accounts._loginButtons.getLoginServices();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOut.services = function () {
|
||||
return Accounts._loginButtons.getLoginServices();
|
||||
};
|
||||
Template._loginButtonsLoggedOut.singleService = function () {
|
||||
var services = Accounts._loginButtons.getLoginServices();
|
||||
if (services.length !== 1)
|
||||
throw new Error(
|
||||
"Shouldn't be rendering this template with more than one configured service");
|
||||
return services[0];
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOut.singleService = function () {
|
||||
var services = Accounts._loginButtons.getLoginServices();
|
||||
if (services.length !== 1)
|
||||
throw new Error(
|
||||
"Shouldn't be rendering this template with more than one configured service");
|
||||
return services[0];
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOut.configurationLoaded = function () {
|
||||
return Accounts.loginServicesConfigured();
|
||||
};
|
||||
Template._loginButtonsLoggedOut.configurationLoaded = function () {
|
||||
return Accounts.loginServicesConfigured();
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsLoggedIn template
|
||||
//
|
||||
//
|
||||
// loginButtonsLoggedIn template
|
||||
//
|
||||
|
||||
// decide whether we should show a dropdown rather than a row of
|
||||
// buttons
|
||||
Template._loginButtonsLoggedIn.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
// decide whether we should show a dropdown rather than a row of
|
||||
// buttons
|
||||
Template._loginButtonsLoggedIn.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsLoggedInSingleLogoutButton template
|
||||
//
|
||||
//
|
||||
// loginButtonsLoggedInSingleLogoutButton template
|
||||
//
|
||||
|
||||
Template._loginButtonsLoggedInSingleLogoutButton.displayName = function () {
|
||||
return Accounts._loginButtons.displayName();
|
||||
};
|
||||
Template._loginButtonsLoggedInSingleLogoutButton.displayName = function () {
|
||||
return Accounts._loginButtons.displayName();
|
||||
};
|
||||
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsMessage template
|
||||
//
|
||||
//
|
||||
// loginButtonsMessage template
|
||||
//
|
||||
|
||||
Template._loginButtonsMessages.errorMessage = function () {
|
||||
return loginButtonsSession.get('errorMessage');
|
||||
};
|
||||
Template._loginButtonsMessages.errorMessage = function () {
|
||||
return loginButtonsSession.get('errorMessage');
|
||||
};
|
||||
|
||||
Template._loginButtonsMessages.infoMessage = function () {
|
||||
return loginButtonsSession.get('infoMessage');
|
||||
};
|
||||
Template._loginButtonsMessages.infoMessage = function () {
|
||||
return loginButtonsSession.get('infoMessage');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsLoggingInPadding template
|
||||
//
|
||||
//
|
||||
// loginButtonsLoggingInPadding template
|
||||
//
|
||||
|
||||
Template._loginButtonsLoggingInPadding.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
Template._loginButtonsLoggingInPadding.dropdown = function () {
|
||||
return Accounts._loginButtons.dropdown();
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// helpers
|
||||
//
|
||||
|
||||
Accounts._loginButtons.displayName = function () {
|
||||
var user = Meteor.user();
|
||||
if (!user)
|
||||
return '';
|
||||
|
||||
if (user.profile && user.profile.name)
|
||||
return user.profile.name;
|
||||
if (user.username)
|
||||
return user.username;
|
||||
if (user.emails && user.emails[0] && user.emails[0].address)
|
||||
return user.emails[0].address;
|
||||
//
|
||||
// helpers
|
||||
//
|
||||
|
||||
Accounts._loginButtons.displayName = function () {
|
||||
var user = Meteor.user();
|
||||
if (!user)
|
||||
return '';
|
||||
};
|
||||
|
||||
// returns an array of the login services used by this app. each
|
||||
// element of the array is an object (eg {name: 'facebook'}), since
|
||||
// that makes it useful in combination with handlebars {{#each}}.
|
||||
if (user.profile && user.profile.name)
|
||||
return user.profile.name;
|
||||
if (user.username)
|
||||
return user.username;
|
||||
if (user.emails && user.emails[0] && user.emails[0].address)
|
||||
return user.emails[0].address;
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// returns an array of the login services used by this app. each
|
||||
// element of the array is an object (eg {name: 'facebook'}), since
|
||||
// that makes it useful in combination with handlebars {{#each}}.
|
||||
//
|
||||
// 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
|
||||
Accounts._loginButtons.getLoginServices = function () {
|
||||
var self = this;
|
||||
var services = [];
|
||||
|
||||
// find all methods of the form: `Meteor.loginWithFoo`, where
|
||||
// `Foo` corresponds to a login service
|
||||
//
|
||||
// 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
|
||||
Accounts._loginButtons.getLoginServices = function () {
|
||||
var self = this;
|
||||
var services = [];
|
||||
// XXX we should consider having a client-side
|
||||
// Accounts.oauth.registerService function which records the
|
||||
// active services and encapsulates boilerplate code now found in
|
||||
// files such as facebook_client.js. This would have the added
|
||||
// benefit of allow us to unify facebook_{client,common,server}.js
|
||||
// into one file, which would encourage people to build more login
|
||||
// services packages.
|
||||
_.each(_.keys(Meteor), function(methodName) {
|
||||
var match;
|
||||
if ((match = methodName.match(/^loginWith(.*)/))) {
|
||||
var serviceName = match[1].toLowerCase();
|
||||
|
||||
// find all methods of the form: `Meteor.loginWithFoo`, where
|
||||
// `Foo` corresponds to a login service
|
||||
//
|
||||
// XXX we should consider having a client-side
|
||||
// Accounts.oauth.registerService function which records the
|
||||
// active services and encapsulates boilerplate code now found in
|
||||
// files such as facebook_client.js. This would have the added
|
||||
// benefit of allow us to unify facebook_{client,common,server}.js
|
||||
// into one file, which would encourage people to build more login
|
||||
// services packages.
|
||||
_.each(_.keys(Meteor), function(methodName) {
|
||||
var match;
|
||||
if ((match = methodName.match(/^loginWith(.*)/))) {
|
||||
var serviceName = match[1].toLowerCase();
|
||||
|
||||
// HACKETY HACK. needed to not match
|
||||
// Meteor.loginWithToken. See XXX above.
|
||||
if (Accounts[serviceName])
|
||||
services.push(match[1].toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
// Be equally kind to all login services. This also preserves
|
||||
// backwards-compatibility. (But maybe order should be
|
||||
// configurable?)
|
||||
services.sort();
|
||||
|
||||
// ensure password is last
|
||||
if (_.contains(services, 'password'))
|
||||
services = _.without(services, 'password').concat(['password']);
|
||||
|
||||
return _.map(services, function(name) {
|
||||
return {name: name};
|
||||
});
|
||||
};
|
||||
|
||||
Accounts._loginButtons.hasPasswordService = function () {
|
||||
return Accounts.password;
|
||||
};
|
||||
|
||||
Accounts._loginButtons.dropdown = function () {
|
||||
return Accounts._loginButtons.hasPasswordService() || Accounts._loginButtons.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.
|
||||
Accounts._loginButtons.validateUsername = function (username) {
|
||||
if (username.length >= 3) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
|
||||
return false;
|
||||
// HACKETY HACK. needed to not match
|
||||
// Meteor.loginWithToken. See XXX above.
|
||||
if (Accounts[serviceName])
|
||||
services.push(match[1].toLowerCase());
|
||||
}
|
||||
};
|
||||
Accounts._loginButtons.validateEmail = function (email) {
|
||||
if (Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
|
||||
return true;
|
||||
});
|
||||
|
||||
if (email.indexOf('@') !== -1) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Invalid email");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Accounts._loginButtons.validatePassword = function (password) {
|
||||
if (password.length >= 6) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// Be equally kind to all login services. This also preserves
|
||||
// backwards-compatibility. (But maybe order should be
|
||||
// configurable?)
|
||||
services.sort();
|
||||
|
||||
})();
|
||||
// ensure password is last
|
||||
if (_.contains(services, 'password'))
|
||||
services = _.without(services, 'password').concat(['password']);
|
||||
|
||||
return _.map(services, function(name) {
|
||||
return {name: name};
|
||||
});
|
||||
};
|
||||
|
||||
Accounts._loginButtons.hasPasswordService = function () {
|
||||
return Accounts.password;
|
||||
};
|
||||
|
||||
Accounts._loginButtons.dropdown = function () {
|
||||
return Accounts._loginButtons.hasPasswordService() || Accounts._loginButtons.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.
|
||||
Accounts._loginButtons.validateUsername = function (username) {
|
||||
if (username.length >= 3) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Accounts._loginButtons.validateEmail = function (email) {
|
||||
if (Accounts.ui._passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
|
||||
return true;
|
||||
|
||||
if (email.indexOf('@') !== -1) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Invalid email");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Accounts._loginButtons.validatePassword = function (password) {
|
||||
if (password.length >= 6) {
|
||||
return true;
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,240 +1,237 @@
|
||||
(function () {
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
|
||||
//
|
||||
// populate the session so that the appropriate dialogs are
|
||||
// displayed by reading variables set by accounts-urls, which parses
|
||||
// special URLs. since accounts-ui depends on accounts-urls, we are
|
||||
// guaranteed to have these set at this point.
|
||||
//
|
||||
//
|
||||
// populate the session so that the appropriate dialogs are
|
||||
// displayed by reading variables set by accounts-urls, which parses
|
||||
// special URLs. since accounts-ui depends on accounts-urls, we are
|
||||
// guaranteed to have these set at this point.
|
||||
//
|
||||
|
||||
if (Accounts._resetPasswordToken) {
|
||||
loginButtonsSession.set('resetPasswordToken', Accounts._resetPasswordToken);
|
||||
}
|
||||
if (Accounts._resetPasswordToken) {
|
||||
loginButtonsSession.set('resetPasswordToken', Accounts._resetPasswordToken);
|
||||
}
|
||||
|
||||
if (Accounts._enrollAccountToken) {
|
||||
loginButtonsSession.set('enrollAccountToken', Accounts._enrollAccountToken);
|
||||
}
|
||||
if (Accounts._enrollAccountToken) {
|
||||
loginButtonsSession.set('enrollAccountToken', Accounts._enrollAccountToken);
|
||||
}
|
||||
|
||||
// Needs to be in Meteor.startup because of a package loading order
|
||||
// issue. We can't be sure that accounts-password is loaded earlier
|
||||
// than accounts-ui so Accounts.verifyEmail might not be defined.
|
||||
Meteor.startup(function () {
|
||||
if (Accounts._verifyEmailToken) {
|
||||
Accounts.verifyEmail(Accounts._verifyEmailToken, function(error) {
|
||||
Accounts._enableAutoLogin();
|
||||
if (!error)
|
||||
loginButtonsSession.set('justVerifiedEmail', true);
|
||||
// XXX show something if there was an error.
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// resetPasswordDialog template
|
||||
//
|
||||
|
||||
Template._resetPasswordDialog.events({
|
||||
'click #login-buttons-reset-password-button': function () {
|
||||
resetPassword();
|
||||
},
|
||||
'keypress #reset-password-new-password': function (event) {
|
||||
if (event.keyCode === 13)
|
||||
resetPassword();
|
||||
},
|
||||
'click #login-buttons-cancel-reset-password': function () {
|
||||
loginButtonsSession.set('resetPasswordToken', null);
|
||||
// Needs to be in Meteor.startup because of a package loading order
|
||||
// issue. We can't be sure that accounts-password is loaded earlier
|
||||
// than accounts-ui so Accounts.verifyEmail might not be defined.
|
||||
Meteor.startup(function () {
|
||||
if (Accounts._verifyEmailToken) {
|
||||
Accounts.verifyEmail(Accounts._verifyEmailToken, function(error) {
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
|
||||
var resetPassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
var newPassword = document.getElementById('reset-password-new-password').value;
|
||||
if (!Accounts._loginButtons.validatePassword(newPassword))
|
||||
return;
|
||||
|
||||
Accounts.resetPassword(
|
||||
loginButtonsSession.get('resetPasswordToken'), newPassword,
|
||||
function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('resetPasswordToken', null);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template._resetPasswordDialog.inResetPasswordFlow = function () {
|
||||
return loginButtonsSession.get('resetPasswordToken');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// 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);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
|
||||
var enrollAccount = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
var password = document.getElementById('enroll-account-password').value;
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
return;
|
||||
|
||||
Accounts.resetPassword(
|
||||
loginButtonsSession.get('enrollAccountToken'), password,
|
||||
function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('enrollAccountToken', null);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template._enrollAccountDialog.inEnrollAccountFlow = function () {
|
||||
return loginButtonsSession.get('enrollAccountToken');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// justVerifiedEmailDialog template
|
||||
//
|
||||
|
||||
Template._justVerifiedEmailDialog.events({
|
||||
'click #just-verified-dismiss-button': function () {
|
||||
loginButtonsSession.set('justVerifiedEmail', false);
|
||||
}
|
||||
});
|
||||
|
||||
Template._justVerifiedEmailDialog.visible = function () {
|
||||
return loginButtonsSession.get('justVerifiedEmail');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsMessagesDialog template
|
||||
//
|
||||
|
||||
Template._loginButtonsMessagesDialog.events({
|
||||
'click #messages-dialog-dismiss-button': function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
}
|
||||
});
|
||||
|
||||
Template._loginButtonsMessagesDialog.visible = function () {
|
||||
var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
|
||||
return !Accounts._loginButtons.dropdown() && hasMessage;
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// configureLoginServiceDialog template
|
||||
//
|
||||
|
||||
Template._configureLoginServiceDialog.events({
|
||||
'click .configure-login-service-dismiss-button': function () {
|
||||
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
|
||||
},
|
||||
'click #configure-login-service-dialog-save-configuration': function () {
|
||||
if (loginButtonsSession.get('configureLoginServiceDialogVisible') &&
|
||||
! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) {
|
||||
// Prepare the configuration document for this login service
|
||||
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
|
||||
var configuration = {
|
||||
service: serviceName
|
||||
};
|
||||
|
||||
// Fetch the value of each input field
|
||||
_.each(configurationFields(), function(field) {
|
||||
configuration[field.property] = document.getElementById(
|
||||
'configure-login-service-dialog-' + field.property).value
|
||||
.replace(/^\s*|\s*$/g, ""); // trim;
|
||||
});
|
||||
|
||||
// Configure this login service
|
||||
Meteor.call("configureLoginService", configuration, function (error, result) {
|
||||
if (error)
|
||||
Meteor._debug("Error configuring login service " + serviceName, error);
|
||||
else
|
||||
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
// 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) {
|
||||
// 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)
|
||||
updateSaveDisabled();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 === '';
|
||||
if (!error)
|
||||
loginButtonsSession.set('justVerifiedEmail', true);
|
||||
// XXX show something if there was an error.
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty);
|
||||
};
|
||||
|
||||
// Returns the appropriate template for this login service. This
|
||||
// template should be defined in the service's package
|
||||
var configureLoginServiceDialogTemplateForService = function () {
|
||||
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
|
||||
return Template['configureLoginServiceDialogFor' + capitalize(serviceName)];
|
||||
};
|
||||
//
|
||||
// resetPasswordDialog template
|
||||
//
|
||||
|
||||
var configurationFields = function () {
|
||||
var template = configureLoginServiceDialogTemplateForService();
|
||||
return template.fields();
|
||||
};
|
||||
Template._resetPasswordDialog.events({
|
||||
'click #login-buttons-reset-password-button': function () {
|
||||
resetPassword();
|
||||
},
|
||||
'keypress #reset-password-new-password': function (event) {
|
||||
if (event.keyCode === 13)
|
||||
resetPassword();
|
||||
},
|
||||
'click #login-buttons-cancel-reset-password': function () {
|
||||
loginButtonsSession.set('resetPasswordToken', null);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
|
||||
Template._configureLoginServiceDialog.configurationFields = function () {
|
||||
return configurationFields();
|
||||
};
|
||||
var resetPassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
var newPassword = document.getElementById('reset-password-new-password').value;
|
||||
if (!Accounts._loginButtons.validatePassword(newPassword))
|
||||
return;
|
||||
|
||||
Template._configureLoginServiceDialog.visible = function () {
|
||||
return loginButtonsSession.get('configureLoginServiceDialogVisible');
|
||||
};
|
||||
Accounts.resetPassword(
|
||||
loginButtonsSession.get('resetPasswordToken'), newPassword,
|
||||
function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('resetPasswordToken', null);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.configurationSteps = function () {
|
||||
// renders the appropriate template
|
||||
return configureLoginServiceDialogTemplateForService()();
|
||||
};
|
||||
Template._resetPasswordDialog.inResetPasswordFlow = function () {
|
||||
return loginButtonsSession.get('resetPasswordToken');
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.saveDisabled = function () {
|
||||
return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled');
|
||||
};
|
||||
|
||||
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
|
||||
var capitalize = function(str){
|
||||
str = str == null ? '' : String(str);
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
//
|
||||
// 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);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
|
||||
var enrollAccount = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
var password = document.getElementById('enroll-account-password').value;
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
return;
|
||||
|
||||
Accounts.resetPassword(
|
||||
loginButtonsSession.get('enrollAccountToken'), password,
|
||||
function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('enrollAccountToken', null);
|
||||
Accounts._enableAutoLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template._enrollAccountDialog.inEnrollAccountFlow = function () {
|
||||
return loginButtonsSession.get('enrollAccountToken');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// justVerifiedEmailDialog template
|
||||
//
|
||||
|
||||
Template._justVerifiedEmailDialog.events({
|
||||
'click #just-verified-dismiss-button': function () {
|
||||
loginButtonsSession.set('justVerifiedEmail', false);
|
||||
}
|
||||
});
|
||||
|
||||
Template._justVerifiedEmailDialog.visible = function () {
|
||||
return loginButtonsSession.get('justVerifiedEmail');
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// loginButtonsMessagesDialog template
|
||||
//
|
||||
|
||||
Template._loginButtonsMessagesDialog.events({
|
||||
'click #messages-dialog-dismiss-button': function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
}
|
||||
});
|
||||
|
||||
Template._loginButtonsMessagesDialog.visible = function () {
|
||||
var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage');
|
||||
return !Accounts._loginButtons.dropdown() && hasMessage;
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// configureLoginServiceDialog template
|
||||
//
|
||||
|
||||
Template._configureLoginServiceDialog.events({
|
||||
'click .configure-login-service-dismiss-button': function () {
|
||||
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
|
||||
},
|
||||
'click #configure-login-service-dialog-save-configuration': function () {
|
||||
if (loginButtonsSession.get('configureLoginServiceDialogVisible') &&
|
||||
! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) {
|
||||
// Prepare the configuration document for this login service
|
||||
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
|
||||
var configuration = {
|
||||
service: serviceName
|
||||
};
|
||||
|
||||
// Fetch the value of each input field
|
||||
_.each(configurationFields(), function(field) {
|
||||
configuration[field.property] = document.getElementById(
|
||||
'configure-login-service-dialog-' + field.property).value
|
||||
.replace(/^\s*|\s*$/g, ""); // trim;
|
||||
});
|
||||
|
||||
// Configure this login service
|
||||
Meteor.call("configureLoginService", configuration, function (error, result) {
|
||||
if (error)
|
||||
Meteor._debug("Error configuring login service " + serviceName, error);
|
||||
else
|
||||
loginButtonsSession.set('configureLoginServiceDialogVisible', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
// 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) {
|
||||
// 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)
|
||||
updateSaveDisabled();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 === '';
|
||||
});
|
||||
|
||||
loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty);
|
||||
};
|
||||
|
||||
// Returns the appropriate template for this login service. This
|
||||
// template should be defined in the service's package
|
||||
var configureLoginServiceDialogTemplateForService = function () {
|
||||
var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName');
|
||||
return Template['configureLoginServiceDialogFor' + capitalize(serviceName)];
|
||||
};
|
||||
|
||||
var configurationFields = function () {
|
||||
var template = configureLoginServiceDialogTemplateForService();
|
||||
return template.fields();
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.configurationFields = function () {
|
||||
return configurationFields();
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.visible = function () {
|
||||
return loginButtonsSession.get('configureLoginServiceDialogVisible');
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.configurationSteps = function () {
|
||||
// renders the appropriate template
|
||||
return configureLoginServiceDialogTemplateForService()();
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.saveDisabled = function () {
|
||||
return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled');
|
||||
};
|
||||
|
||||
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
|
||||
var capitalize = function(str){
|
||||
str = str == null ? '' : String(str);
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
@@ -1,503 +1,499 @@
|
||||
(function () {
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
// 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);
|
||||
Deps.flush();
|
||||
correctDropdownZIndexes();
|
||||
},
|
||||
'click .login-close-text': function () {
|
||||
loginButtonsSession.closeDropdown();
|
||||
}
|
||||
});
|
||||
// events shared between loginButtonsLoggedOutDropdown and
|
||||
// loginButtonsLoggedInDropdown
|
||||
Template._loginButtons.events({
|
||||
'click #login-name-link, click #login-sign-in-link': function () {
|
||||
loginButtonsSession.set('dropdownVisible', true);
|
||||
Deps.flush();
|
||||
correctDropdownZIndexes();
|
||||
},
|
||||
'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.displayName = function () {
|
||||
return Accounts._loginButtons.displayName();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.inChangePasswordFlow = function () {
|
||||
return loginButtonsSession.get('inChangePasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.inMessageOnlyFlow = function () {
|
||||
return loginButtonsSession.get('inMessageOnlyFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.dropdownVisible = function () {
|
||||
return loginButtonsSession.get('dropdownVisible');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdownActions.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.
|
||||
//
|
||||
// loginButtonsLoggedInDropdown template and related
|
||||
//
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.events({
|
||||
'click #login-buttons-open-change-password': function() {
|
||||
loginButtonsSession.resetMessages();
|
||||
loginButtonsSession.set('inChangePasswordFlow', true);
|
||||
}
|
||||
});
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.displayName = function () {
|
||||
return Accounts._loginButtons.displayName();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.inChangePasswordFlow = function () {
|
||||
return loginButtonsSession.get('inChangePasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.inMessageOnlyFlow = function () {
|
||||
return loginButtonsSession.get('inMessageOnlyFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdown.dropdownVisible = function () {
|
||||
return loginButtonsSession.get('dropdownVisible');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedInDropdownActions.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);
|
||||
};
|
||||
// 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
|
||||
//
|
||||
//
|
||||
// loginButtonsLoggedOutDropdown template and related
|
||||
//
|
||||
|
||||
Template._loginButtonsLoggedOutDropdown.events({
|
||||
'click #login-buttons-password': function () {
|
||||
loginOrSignup();
|
||||
},
|
||||
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 () {
|
||||
'keypress #forgot-password-email': function (event) {
|
||||
if (event.keyCode === 13)
|
||||
forgotPassword();
|
||||
},
|
||||
},
|
||||
|
||||
'click #signup-link': function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
'click #login-buttons-forgot-password': function () {
|
||||
forgotPassword();
|
||||
},
|
||||
|
||||
// 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
|
||||
Deps.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;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
|
||||
// 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
|
||||
Deps.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?
|
||||
|
||||
loginButtonsSession.set('inSignupFlow', false);
|
||||
loginButtonsSession.set('inForgotPasswordFlow', false);
|
||||
// force the ui to update so that we have the approprate fields to fill in
|
||||
Deps.flush();
|
||||
|
||||
if (document.getElementById('login-username'))
|
||||
document.getElementById('login-username').value = username;
|
||||
if (document.getElementById('login-email'))
|
||||
document.getElementById('login-email').value = email;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
if (document.getElementById('login-username-or-email'))
|
||||
document.getElementById('login-username-or-email').value = email || username;
|
||||
},
|
||||
'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();
|
||||
}
|
||||
});
|
||||
|
||||
// additional classes that can be helpful in styling the dropdown
|
||||
Template._loginButtonsLoggedOutDropdown.additionalClasses = function () {
|
||||
if (!Accounts.password) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutDropdown.dropdownVisible = function () {
|
||||
return loginButtonsSession.get('dropdownVisible');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutDropdown.hasPasswordService = function () {
|
||||
return Accounts._loginButtons.hasPasswordService();
|
||||
};
|
||||
|
||||
// return all login services, with password last
|
||||
Template._loginButtonsLoggedOutAllServices.services = function () {
|
||||
return Accounts._loginButtons.getLoginServices();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.isPasswordService = function () {
|
||||
return this.name === 'password';
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.hasOtherServices = function () {
|
||||
return Accounts._loginButtons.getLoginServices().length > 1;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.hasPasswordService = function () {
|
||||
return Accounts._loginButtons.hasPasswordService();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.fields = function () {
|
||||
var loginFields = [
|
||||
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
|
||||
visible: function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'username', fieldLabel: 'Username',
|
||||
visible: function () {
|
||||
return Accounts.ui._passwordSignupFields() === "USERNAME_ONLY";
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
|
||||
visible: function () {
|
||||
return Accounts.ui._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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
|
||||
visible: function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "EMAIL_ONLY"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
|
||||
visible: function () {
|
||||
return Accounts.ui._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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}}
|
||||
];
|
||||
|
||||
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inForgotPasswordFlow = function () {
|
||||
return loginButtonsSession.get('inForgotPasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inLoginFlow = function () {
|
||||
return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inSignupFlow = function () {
|
||||
return loginButtonsSession.get('inSignupFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.showCreateAccountLink = function () {
|
||||
return !Accounts._options.forbidClientAccountCreation;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.showForgotPasswordLink = function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
};
|
||||
|
||||
Template._loginButtonsFormField.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.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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}}
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// helpers
|
||||
//
|
||||
|
||||
var elementValueById = function(id) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element)
|
||||
return null;
|
||||
else
|
||||
return element.value;
|
||||
};
|
||||
|
||||
var trimmedElementValueById = function(id) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element)
|
||||
return null;
|
||||
else
|
||||
return element.value.replace(/^\s*|\s*$/g, ""); // trim;
|
||||
};
|
||||
|
||||
var loginOrSignup = function () {
|
||||
if (loginButtonsSession.get('inSignupFlow'))
|
||||
signup();
|
||||
else
|
||||
login();
|
||||
};
|
||||
|
||||
var login = function () {
|
||||
'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');
|
||||
|
||||
var loginSelector;
|
||||
if (username !== null) {
|
||||
if (!Accounts._loginButtons.validateUsername(username))
|
||||
return;
|
||||
else
|
||||
loginSelector = {username: username};
|
||||
} else if (email !== null) {
|
||||
if (!Accounts._loginButtons.validateEmail(email))
|
||||
return;
|
||||
else
|
||||
loginSelector = {email: email};
|
||||
} else if (usernameOrEmail !== null) {
|
||||
// XXX not sure how we should validate this. but this seems good enough (for now),
|
||||
// since an email must have at least 3 characters anyways
|
||||
if (!Accounts._loginButtons.validateUsername(usernameOrEmail))
|
||||
return;
|
||||
else
|
||||
loginSelector = usernameOrEmail;
|
||||
} else {
|
||||
throw new Error("Unexpected -- no element to use as a login user selector");
|
||||
}
|
||||
loginButtonsSession.set('inSignupFlow', true);
|
||||
loginButtonsSession.set('inForgotPasswordFlow', false);
|
||||
// force the ui to update so that we have the approprate fields to fill in
|
||||
Deps.flush();
|
||||
|
||||
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.closeDropdown();
|
||||
}
|
||||
});
|
||||
};
|
||||
// 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;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
|
||||
var signup = function () {
|
||||
// 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();
|
||||
|
||||
var options = {}; // to be passed to Accounts.createUser
|
||||
// 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
|
||||
Deps.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');
|
||||
if (username !== null) {
|
||||
if (!Accounts._loginButtons.validateUsername(username))
|
||||
return;
|
||||
else
|
||||
options.username = username;
|
||||
}
|
||||
var email = trimmedElementValueById('login-email')
|
||||
|| trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names?
|
||||
|
||||
var email = trimmedElementValueById('login-email');
|
||||
if (email !== null) {
|
||||
if (!Accounts._loginButtons.validateEmail(email))
|
||||
return;
|
||||
else
|
||||
options.email = email;
|
||||
}
|
||||
loginButtonsSession.set('inSignupFlow', false);
|
||||
loginButtonsSession.set('inForgotPasswordFlow', false);
|
||||
// force the ui to update so that we have the approprate fields to fill in
|
||||
Deps.flush();
|
||||
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var password = elementValueById('login-password');
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
if (document.getElementById('login-username'))
|
||||
document.getElementById('login-username').value = username;
|
||||
if (document.getElementById('login-email'))
|
||||
document.getElementById('login-email').value = email;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
if (document.getElementById('login-username-or-email'))
|
||||
document.getElementById('login-username-or-email').value = email || username;
|
||||
},
|
||||
'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();
|
||||
}
|
||||
});
|
||||
|
||||
// additional classes that can be helpful in styling the dropdown
|
||||
Template._loginButtonsLoggedOutDropdown.additionalClasses = function () {
|
||||
if (!Accounts.password) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutDropdown.dropdownVisible = function () {
|
||||
return loginButtonsSession.get('dropdownVisible');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutDropdown.hasPasswordService = function () {
|
||||
return Accounts._loginButtons.hasPasswordService();
|
||||
};
|
||||
|
||||
// return all login services, with password last
|
||||
Template._loginButtonsLoggedOutAllServices.services = function () {
|
||||
return Accounts._loginButtons.getLoginServices();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.isPasswordService = function () {
|
||||
return this.name === 'password';
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.hasOtherServices = function () {
|
||||
return Accounts._loginButtons.getLoginServices().length > 1;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutAllServices.hasPasswordService = function () {
|
||||
return Accounts._loginButtons.hasPasswordService();
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.fields = function () {
|
||||
var loginFields = [
|
||||
{fieldName: 'username-or-email', fieldLabel: 'Username or Email',
|
||||
visible: function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'username', fieldLabel: 'Username',
|
||||
visible: function () {
|
||||
return Accounts.ui._passwordSignupFields() === "USERNAME_ONLY";
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
|
||||
visible: function () {
|
||||
return Accounts.ui._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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email', inputType: 'email',
|
||||
visible: function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "EMAIL_ONLY"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}},
|
||||
{fieldName: 'email', fieldLabel: 'Email (optional)', inputType: 'email',
|
||||
visible: function () {
|
||||
return Accounts.ui._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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}}
|
||||
];
|
||||
|
||||
return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inForgotPasswordFlow = function () {
|
||||
return loginButtonsSession.get('inForgotPasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inLoginFlow = function () {
|
||||
return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.inSignupFlow = function () {
|
||||
return loginButtonsSession.get('inSignupFlow');
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.showCreateAccountLink = function () {
|
||||
return !Accounts._options.forbidClientAccountCreation;
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutPasswordService.showForgotPasswordLink = function () {
|
||||
return _.contains(
|
||||
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
};
|
||||
|
||||
Template._loginButtonsFormField.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.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"],
|
||||
Accounts.ui._passwordSignupFields());
|
||||
}}
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// helpers
|
||||
//
|
||||
|
||||
var elementValueById = function(id) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element)
|
||||
return null;
|
||||
else
|
||||
return element.value;
|
||||
};
|
||||
|
||||
var trimmedElementValueById = function(id) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element)
|
||||
return null;
|
||||
else
|
||||
return element.value.replace(/^\s*|\s*$/g, ""); // trim;
|
||||
};
|
||||
|
||||
var loginOrSignup = function () {
|
||||
if (loginButtonsSession.get('inSignupFlow'))
|
||||
signup();
|
||||
else
|
||||
login();
|
||||
};
|
||||
|
||||
var login = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
|
||||
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');
|
||||
|
||||
var loginSelector;
|
||||
if (username !== null) {
|
||||
if (!Accounts._loginButtons.validateUsername(username))
|
||||
return;
|
||||
else
|
||||
options.password = password;
|
||||
|
||||
if (!matchPasswordAgainIfPresent())
|
||||
loginSelector = {username: username};
|
||||
} else if (email !== null) {
|
||||
if (!Accounts._loginButtons.validateEmail(email))
|
||||
return;
|
||||
else
|
||||
loginSelector = {email: email};
|
||||
} else if (usernameOrEmail !== null) {
|
||||
// XXX not sure how we should validate this. but this seems good enough (for now),
|
||||
// since an email must have at least 3 characters anyways
|
||||
if (!Accounts._loginButtons.validateUsername(usernameOrEmail))
|
||||
return;
|
||||
else
|
||||
loginSelector = usernameOrEmail;
|
||||
} else {
|
||||
throw new Error("Unexpected -- no element to use as a login user selector");
|
||||
}
|
||||
|
||||
Accounts.createUser(options, function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.closeDropdown();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var forgotPassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
|
||||
var email = trimmedElementValueById("forgot-password-email");
|
||||
if (email.indexOf('@') !== -1) {
|
||||
Accounts.forgotPassword({email: email}, function (error) {
|
||||
if (error)
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
else
|
||||
loginButtonsSession.infoMessage("Email sent");
|
||||
});
|
||||
Meteor.loginWithPassword(loginSelector, password, function (error, result) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Invalid email");
|
||||
loginButtonsSession.closeDropdown();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var changePassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
var signup = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var oldPassword = elementValueById('login-old-password');
|
||||
var options = {}; // to be passed to Accounts.createUser
|
||||
|
||||
var username = trimmedElementValueById('login-username');
|
||||
if (username !== null) {
|
||||
if (!Accounts._loginButtons.validateUsername(username))
|
||||
return;
|
||||
else
|
||||
options.username = username;
|
||||
}
|
||||
|
||||
var email = trimmedElementValueById('login-email');
|
||||
if (email !== null) {
|
||||
if (!Accounts._loginButtons.validateEmail(email))
|
||||
return;
|
||||
else
|
||||
options.email = email;
|
||||
}
|
||||
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var password = elementValueById('login-password');
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
return;
|
||||
else
|
||||
options.password = password;
|
||||
|
||||
if (!matchPasswordAgainIfPresent())
|
||||
return;
|
||||
|
||||
Accounts.createUser(options, function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.closeDropdown();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var forgotPassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
|
||||
var email = trimmedElementValueById("forgot-password-email");
|
||||
if (email.indexOf('@') !== -1) {
|
||||
Accounts.forgotPassword({email: email}, function (error) {
|
||||
if (error)
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
else
|
||||
loginButtonsSession.infoMessage("Email sent");
|
||||
});
|
||||
} else {
|
||||
loginButtonsSession.errorMessage("Invalid email");
|
||||
}
|
||||
};
|
||||
|
||||
var changePassword = function () {
|
||||
loginButtonsSession.resetMessages();
|
||||
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var oldPassword = elementValueById('login-old-password');
|
||||
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var password = elementValueById('login-password');
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
return;
|
||||
|
||||
if (!matchPasswordAgainIfPresent())
|
||||
return;
|
||||
|
||||
Accounts.changePassword(oldPassword, password, function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('inChangePasswordFlow', false);
|
||||
loginButtonsSession.set('inMessageOnlyFlow', true);
|
||||
loginButtonsSession.infoMessage("Password changed");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var matchPasswordAgainIfPresent = function () {
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var 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');
|
||||
if (!Accounts._loginButtons.validatePassword(password))
|
||||
return;
|
||||
|
||||
if (!matchPasswordAgainIfPresent())
|
||||
return;
|
||||
|
||||
Accounts.changePassword(oldPassword, password, function (error) {
|
||||
if (error) {
|
||||
loginButtonsSession.errorMessage(error.reason || "Unknown error");
|
||||
} else {
|
||||
loginButtonsSession.set('inChangePasswordFlow', false);
|
||||
loginButtonsSession.set('inMessageOnlyFlow', true);
|
||||
loginButtonsSession.infoMessage("Password changed");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var matchPasswordAgainIfPresent = function () {
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var 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');
|
||||
if (password !== passwordAgain) {
|
||||
loginButtonsSession.errorMessage("Passwords don't match");
|
||||
return false;
|
||||
}
|
||||
if (password !== passwordAgain) {
|
||||
loginButtonsSession.errorMessage("Passwords don't match");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var correctDropdownZIndexes = function () {
|
||||
// IE <= 7 has a z-index bug that means we can't just give the
|
||||
// dropdown a z-index and expect it to stack above the rest of
|
||||
// the page even if nothing else has a z-index. The nature of
|
||||
// the bug is that all positioned elements are considered to
|
||||
// have z-index:0 (not auto) and therefore start new stacking
|
||||
// contexts, with ties broken by page order.
|
||||
//
|
||||
// The fix, then is to give z-index:1 to all ancestors
|
||||
// of the dropdown having z-index:0.
|
||||
for(var n = document.getElementById('login-dropdown-list').parentNode;
|
||||
n.nodeName !== 'BODY';
|
||||
n = n.parentNode)
|
||||
if (n.style.zIndex === 0)
|
||||
n.style.zIndex = 1;
|
||||
};
|
||||
|
||||
|
||||
}) ();
|
||||
var correctDropdownZIndexes = function () {
|
||||
// IE <= 7 has a z-index bug that means we can't just give the
|
||||
// dropdown a z-index and expect it to stack above the rest of
|
||||
// the page even if nothing else has a z-index. The nature of
|
||||
// the bug is that all positioned elements are considered to
|
||||
// have z-index:0 (not auto) and therefore start new stacking
|
||||
// contexts, with ties broken by page order.
|
||||
//
|
||||
// The fix, then is to give z-index:1 to all ancestors
|
||||
// of the dropdown having z-index:0.
|
||||
for(var n = document.getElementById('login-dropdown-list').parentNode;
|
||||
n.nodeName !== 'BODY';
|
||||
n = n.parentNode)
|
||||
if (n.style.zIndex === 0)
|
||||
n.style.zIndex = 1;
|
||||
};
|
||||
|
||||
@@ -1,102 +1,100 @@
|
||||
(function () {
|
||||
var VALID_KEYS = [
|
||||
'dropdownVisible',
|
||||
var VALID_KEYS = [
|
||||
'dropdownVisible',
|
||||
|
||||
// XXX consider replacing these with one key that has an enum for values.
|
||||
'inSignupFlow',
|
||||
'inForgotPasswordFlow',
|
||||
'inChangePasswordFlow',
|
||||
'inMessageOnlyFlow',
|
||||
// XXX consider replacing these with one key that has an enum for values.
|
||||
'inSignupFlow',
|
||||
'inForgotPasswordFlow',
|
||||
'inChangePasswordFlow',
|
||||
'inMessageOnlyFlow',
|
||||
|
||||
'errorMessage',
|
||||
'infoMessage',
|
||||
'errorMessage',
|
||||
'infoMessage',
|
||||
|
||||
// dialogs with messages (info and error)
|
||||
'resetPasswordToken',
|
||||
'enrollAccountToken',
|
||||
'justVerifiedEmail',
|
||||
// dialogs with messages (info and error)
|
||||
'resetPasswordToken',
|
||||
'enrollAccountToken',
|
||||
'justVerifiedEmail',
|
||||
|
||||
'configureLoginServiceDialogVisible',
|
||||
'configureLoginServiceDialogServiceName',
|
||||
'configureLoginServiceDialogSaveDisabled'
|
||||
];
|
||||
'configureLoginServiceDialogVisible',
|
||||
'configureLoginServiceDialogServiceName',
|
||||
'configureLoginServiceDialogSaveDisabled'
|
||||
];
|
||||
|
||||
var validateKey = function (key) {
|
||||
if (!_.contains(VALID_KEYS, key))
|
||||
throw new Error("Invalid key in loginButtonsSession: " + key);
|
||||
};
|
||||
var validateKey = function (key) {
|
||||
if (!_.contains(VALID_KEYS, key))
|
||||
throw new Error("Invalid key in loginButtonsSession: " + key);
|
||||
};
|
||||
|
||||
var KEY_PREFIX = "Meteor.loginButtons.";
|
||||
var KEY_PREFIX = "Meteor.loginButtons.";
|
||||
|
||||
// XXX we should have a better pattern for code private to a package like this one
|
||||
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().");
|
||||
// XXX we should have a better pattern for code private to a package like this one
|
||||
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().");
|
||||
|
||||
this._set(key, value);
|
||||
},
|
||||
this._set(key, value);
|
||||
},
|
||||
|
||||
_set: function(key, value) {
|
||||
Session.set(KEY_PREFIX + key, value);
|
||||
},
|
||||
_set: function(key, value) {
|
||||
Session.set(KEY_PREFIX + key, value);
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
validateKey(key);
|
||||
return Session.get(KEY_PREFIX + key);
|
||||
},
|
||||
get: function(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();
|
||||
},
|
||||
closeDropdown: function () {
|
||||
this.set('inSignupFlow', false);
|
||||
this.set('inForgotPasswordFlow', false);
|
||||
this.set('inChangePasswordFlow', false);
|
||||
this.set('inMessageOnlyFlow', false);
|
||||
this.set('dropdownVisible', false);
|
||||
this.resetMessages();
|
||||
},
|
||||
|
||||
infoMessage: function(message) {
|
||||
this._set("errorMessage", null);
|
||||
this._set("infoMessage", message);
|
||||
this.ensureMessageVisible();
|
||||
},
|
||||
infoMessage: function(message) {
|
||||
this._set("errorMessage", null);
|
||||
this._set("infoMessage", message);
|
||||
this.ensureMessageVisible();
|
||||
},
|
||||
|
||||
errorMessage: function(message) {
|
||||
this._set("errorMessage", message);
|
||||
this._set("infoMessage", null);
|
||||
this.ensureMessageVisible();
|
||||
},
|
||||
errorMessage: function(message) {
|
||||
this._set("errorMessage", message);
|
||||
this._set("infoMessage", null);
|
||||
this.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)
|
||||
isMessageDialogVisible: function () {
|
||||
return this.get('resetPasswordToken') ||
|
||||
this.get('enrollAccountToken') ||
|
||||
this.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.
|
||||
ensureMessageVisible: function () {
|
||||
if (!this.isMessageDialogVisible())
|
||||
this.set("dropdownVisible", true);
|
||||
},
|
||||
|
||||
resetMessages: function () {
|
||||
this._set("errorMessage", null);
|
||||
this._set("infoMessage", null);
|
||||
},
|
||||
resetMessages: function () {
|
||||
this._set("errorMessage", null);
|
||||
this._set("infoMessage", null);
|
||||
},
|
||||
|
||||
configureService: function (name) {
|
||||
this.set('configureLoginServiceDialogVisible', true);
|
||||
this.set('configureLoginServiceDialogServiceName', name);
|
||||
this.set('configureLoginServiceDialogSaveDisabled', true);
|
||||
}
|
||||
};
|
||||
}) ();
|
||||
configureService: function (name) {
|
||||
this.set('configureLoginServiceDialogVisible', true);
|
||||
this.set('configureLoginServiceDialogServiceName', name);
|
||||
this.set('configureLoginServiceDialogSaveDisabled', true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
(function () {
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.events({
|
||||
'click .login-button': function () {
|
||||
var serviceName = this.name;
|
||||
loginButtonsSession.resetMessages();
|
||||
var callback = function (err) {
|
||||
if (!err) {
|
||||
loginButtonsSession.closeDropdown();
|
||||
} else if (err instanceof Accounts.LoginCancelledError) {
|
||||
// do nothing
|
||||
} else if (err instanceof Accounts.ConfigError) {
|
||||
loginButtonsSession.configureService(serviceName);
|
||||
} else {
|
||||
loginButtonsSession.errorMessage(err.reason || "Unknown error");
|
||||
}
|
||||
};
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.events({
|
||||
'click .login-button': function () {
|
||||
var serviceName = this.name;
|
||||
loginButtonsSession.resetMessages();
|
||||
var callback = function (err) {
|
||||
if (!err) {
|
||||
loginButtonsSession.closeDropdown();
|
||||
} else if (err instanceof Accounts.LoginCancelledError) {
|
||||
// do nothing
|
||||
} else if (err instanceof Accounts.ConfigError) {
|
||||
loginButtonsSession.configureService(serviceName);
|
||||
} else {
|
||||
loginButtonsSession.errorMessage(err.reason || "Unknown error");
|
||||
}
|
||||
};
|
||||
|
||||
var loginWithService = Meteor["loginWith" + capitalize(serviceName)];
|
||||
var loginWithService = Meteor["loginWith" + capitalize(serviceName)];
|
||||
|
||||
var 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])
|
||||
options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];
|
||||
var 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])
|
||||
options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName];
|
||||
|
||||
loginWithService(options, callback);
|
||||
}
|
||||
});
|
||||
loginWithService(options, callback);
|
||||
}
|
||||
});
|
||||
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.configured = function () {
|
||||
return !!Accounts.loginServiceConfiguration.findOne({service: this.name});
|
||||
};
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.configured = function () {
|
||||
return !!Accounts.loginServiceConfiguration.findOne({service: this.name});
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () {
|
||||
if (this.name === 'github')
|
||||
// XXX we should allow service packages to set their capitalized name
|
||||
return 'GitHub';
|
||||
else
|
||||
return capitalize(this.name);
|
||||
};
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () {
|
||||
if (this.name === 'github')
|
||||
// XXX we should allow service packages to set their capitalized name
|
||||
return 'GitHub';
|
||||
else
|
||||
return capitalize(this.name);
|
||||
};
|
||||
|
||||
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
|
||||
var capitalize = function(str){
|
||||
str = str == null ? '' : String(str);
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
}) ();
|
||||
// XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js
|
||||
var capitalize = function(str){
|
||||
str = str == null ? '' : String(str);
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
// Browser support is IE 8+ and all modern browsers, with the caveat
|
||||
// that `-moz-box-sizing` in Firefox is considered to have some
|
||||
// buggy or non-compliant behavior. For example, min/max-width/height
|
||||
// may not interact correctly. Box-sizing is thus not a feature to
|
||||
// base a major layout system around.
|
||||
//
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=243412.
|
||||
// may not interact correctly. See
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=243412.
|
||||
.box-sizing-by-border () {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
@@ -39,7 +37,7 @@
|
||||
.display-inline-block () {
|
||||
display: inline-block;
|
||||
|
||||
// IE 7 hacks:
|
||||
// IE 7 hacks (disabled)
|
||||
//*display: inline;
|
||||
//*zoom: 1;
|
||||
}
|
||||
@@ -417,9 +415,11 @@
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
input[type=text], input[type=email], input[type=password] {
|
||||
padding: 4px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
#login-buttons, .accounts-dialog {
|
||||
input[type=text], input[type=email], input[type=password] {
|
||||
padding: 4px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
(function () {
|
||||
if (typeof Accounts === 'undefined')
|
||||
Accounts = {};
|
||||
if (typeof Accounts === 'undefined')
|
||||
Accounts = {};
|
||||
|
||||
// reads a reset password token from the url's hash fragment, if it's
|
||||
// there. if so prevent automatically logging in since it could be
|
||||
// confusing to be logged in as user A while resetting password for
|
||||
// user B
|
||||
//
|
||||
// reset password urls use hash fragments instead of url paths/query
|
||||
// strings so that the reset password token is not sent over the wire
|
||||
// on the http request
|
||||
var match;
|
||||
match = window.location.hash.match(/^\#\/reset-password\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._resetPasswordToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
// reads a reset password token from the url's hash fragment, if it's
|
||||
// there. if so prevent automatically logging in since it could be
|
||||
// confusing to be logged in as user A while resetting password for
|
||||
// user B
|
||||
//
|
||||
// reset password urls use hash fragments instead of url paths/query
|
||||
// strings so that the reset password token is not sent over the wire
|
||||
// on the http request
|
||||
var match;
|
||||
match = window.location.hash.match(/^\#\/reset-password\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._resetPasswordToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
|
||||
// reads a verify email token from the url's hash fragment, if
|
||||
// it's there. also don't automatically log the user is, as for
|
||||
// reset password links.
|
||||
//
|
||||
// XXX we don't need to use hash fragments in this case, and having
|
||||
// the token appear in the url's path would allow us to use a custom
|
||||
// middleware instead of verifying the email on pageload, which
|
||||
// would be faster but less DDP-ish (and more specifically, any
|
||||
// non-web DDP app, such as an iOS client, would do something more
|
||||
// in line with the hash fragment approach)
|
||||
match = window.location.hash.match(/^\#\/verify-email\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._verifyEmailToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
// reads a verify email token from the url's hash fragment, if
|
||||
// it's there. also don't automatically log the user is, as for
|
||||
// reset password links.
|
||||
//
|
||||
// XXX we don't need to use hash fragments in this case, and having
|
||||
// the token appear in the url's path would allow us to use a custom
|
||||
// middleware instead of verifying the email on pageload, which
|
||||
// would be faster but less DDP-ish (and more specifically, any
|
||||
// non-web DDP app, such as an iOS client, would do something more
|
||||
// in line with the hash fragment approach)
|
||||
match = window.location.hash.match(/^\#\/verify-email\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._verifyEmailToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
|
||||
// reads an account enrollment token from the url's hash fragment, if
|
||||
// it's there. also don't automatically log the user is, as for
|
||||
// reset password links.
|
||||
match = window.location.hash.match(/^\#\/enroll-account\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._enrollAccountToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
})();
|
||||
// reads an account enrollment token from the url's hash fragment, if
|
||||
// it's there. also don't automatically log the user is, as for
|
||||
// reset password links.
|
||||
match = window.location.hash.match(/^\#\/enroll-account\/(.*)$/);
|
||||
if (match) {
|
||||
Accounts._preventAutoLogin = true;
|
||||
Accounts._enrollAccountToken = match[1];
|
||||
window.location.hash = '';
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
(function () {
|
||||
// XXX support options.requestPermissions as we do for Facebook, Google, Github
|
||||
Meteor.loginWithWeibo = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
// XXX support options.requestPermissions as we do for Facebook, Google, Github
|
||||
Meteor.loginWithWeibo = function (options, callback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'});
|
||||
if (!config) {
|
||||
callback && callback(new Accounts.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = Random.id();
|
||||
// XXX need to support configuring access_type and scope
|
||||
var loginUrl =
|
||||
'https://api.weibo.com/oauth2/authorize' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + config.clientId +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) +
|
||||
'&state=' + state;
|
||||
var state = Random.id();
|
||||
// XXX need to support configuring access_type and scope
|
||||
var loginUrl =
|
||||
'https://api.weibo.com/oauth2/authorize' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + config.clientId +
|
||||
'&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) +
|
||||
'&state=' + state;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
}) ();
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback);
|
||||
};
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
(function () {
|
||||
Accounts.oauth.registerService('weibo', 2, function(query) {
|
||||
|
||||
Accounts.oauth.registerService('weibo', 2, function(query) {
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10));
|
||||
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10));
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
id: accessToken.uid,
|
||||
accessToken: accessToken.access_token,
|
||||
screenName: identity.screen_name
|
||||
},
|
||||
options: {profile: {name: identity.screen_name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var result = Meteor.http.post(
|
||||
"https://api.weibo.com/oauth2/access_token", {params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/weibo?close", {replaceLocalhost: true}),
|
||||
grant_type: 'authorization_code'
|
||||
}});
|
||||
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (typeof result.content === "string")
|
||||
result.content = JSON.parse(result.content);
|
||||
if (result.content.error) // if the http response was a json object with an error attribute
|
||||
throw result.content;
|
||||
return result.content;
|
||||
return {
|
||||
serviceData: {
|
||||
id: accessToken.uid,
|
||||
accessToken: accessToken.access_token,
|
||||
screenName: identity.screen_name
|
||||
},
|
||||
options: {profile: {name: identity.screen_name}}
|
||||
};
|
||||
});
|
||||
|
||||
var getIdentity = function (accessToken, userId) {
|
||||
var result = Meteor.http.get(
|
||||
"https://api.weibo.com/2/users/show.json",
|
||||
{params: {access_token: accessToken, uid: userId}});
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
})();
|
||||
var result = Meteor.http.post(
|
||||
"https://api.weibo.com/oauth2/access_token", {params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/weibo?close", {replaceLocalhost: true}),
|
||||
grant_type: 'authorization_code'
|
||||
}});
|
||||
|
||||
if (result.error) // if the http response was an error
|
||||
throw result.error;
|
||||
if (typeof result.content === "string")
|
||||
result.content = JSON.parse(result.content);
|
||||
if (result.content.error) // if the http response was a json object with an error attribute
|
||||
throw result.content;
|
||||
return result.content;
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken, userId) {
|
||||
var result = Meteor.http.get(
|
||||
"https://api.weibo.com/2/users/show.json",
|
||||
{params: {access_token: accessToken, uid: userId}});
|
||||
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
return result.data;
|
||||
};
|
||||
|
||||
@@ -1,71 +1,67 @@
|
||||
(function() {
|
||||
if (! window.applicationCache)
|
||||
return;
|
||||
|
||||
if (! window.applicationCache)
|
||||
var appCacheStatuses = [
|
||||
'uncached',
|
||||
'idle',
|
||||
'checking',
|
||||
'downloading',
|
||||
'updateready',
|
||||
'obsolete'
|
||||
];
|
||||
|
||||
var updatingAppcache = false;
|
||||
var reloadRetry = null;
|
||||
var appcacheUpdated = false;
|
||||
|
||||
Meteor._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.
|
||||
return [true];
|
||||
}
|
||||
updatingAppcache = true;
|
||||
}
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
var appCacheStatuses = [
|
||||
'uncached',
|
||||
'idle',
|
||||
'checking',
|
||||
'downloading',
|
||||
'updateready',
|
||||
'obsolete'
|
||||
];
|
||||
window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false);
|
||||
window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false);
|
||||
|
||||
var updatingAppcache = false;
|
||||
var reloadRetry = null;
|
||||
var appcacheUpdated = 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.
|
||||
|
||||
Meteor._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.
|
||||
return [true];
|
||||
}
|
||||
updatingAppcache = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
window.applicationCache.addEventListener('obsolete', (function() {
|
||||
if (reloadRetry) {
|
||||
cacheIsNowUpToDate();
|
||||
}
|
||||
else {
|
||||
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 {
|
||||
appcacheUpdated = true;
|
||||
Meteor._reload.reload();
|
||||
}
|
||||
}), false);
|
||||
|
||||
})();
|
||||
Meteor._reload.reload();
|
||||
}
|
||||
}), false);
|
||||
|
||||
@@ -1,185 +1,181 @@
|
||||
(function() {
|
||||
var app = __meteor_bootstrap__.app;
|
||||
var bundle = __meteor_bootstrap__.bundle;
|
||||
var crypto = Npm.require('crypto');
|
||||
var fs = Npm.require('fs');
|
||||
var path = Npm.require('path');
|
||||
|
||||
var app = __meteor_bootstrap__.app;
|
||||
var bundle = __meteor_bootstrap__.bundle;
|
||||
var crypto = __meteor_bootstrap__.require('crypto');
|
||||
var fs = __meteor_bootstrap__.require('fs');
|
||||
var path = __meteor_bootstrap__.require('path');
|
||||
var knownBrowsers = ['android', 'chrome', 'firefox', 'ie', 'mobileSafari', 'safari'];
|
||||
|
||||
var knownBrowsers = ['android', 'chrome', 'firefox', 'ie', 'mobileSafari', 'safari'];
|
||||
var browsersEnabledByDefault = ['android', 'chrome', 'ie', 'mobileSafari', 'safari'];
|
||||
|
||||
var browsersEnabledByDefault = ['android', 'chrome', 'ie', 'mobileSafari', 'safari'];
|
||||
var enabledBrowsers = {};
|
||||
_.each(browsersEnabledByDefault, function (browser) {
|
||||
enabledBrowsers[browser] = true;
|
||||
});
|
||||
|
||||
var enabledBrowsers = {};
|
||||
_.each(browsersEnabledByDefault, function (browser) {
|
||||
enabledBrowsers[browser] = true;
|
||||
Meteor.AppCache = {
|
||||
config: function(options) {
|
||||
_.each(options, function (value, option) {
|
||||
if (option === 'browsers') {
|
||||
enabledBrowsers = {};
|
||||
_.each(value, function (browser) {
|
||||
enabledBrowsers[browser] = true;
|
||||
});
|
||||
}
|
||||
else if (_.contains(knownBrowsers, option)) {
|
||||
enabledBrowsers[option] = value;
|
||||
}
|
||||
else if (option === 'onlineOnly') {
|
||||
_.each(value, function (urlPrefix) {
|
||||
Meteor._routePolicy.declare(urlPrefix, 'static-online');
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new Error('Invalid AppCache config option: ' + option);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var browserEnabled = function(request) {
|
||||
return enabledBrowsers[request.browser.name];
|
||||
};
|
||||
|
||||
__meteor_bootstrap__.htmlAttributeHooks.push(function (request) {
|
||||
if (browserEnabled(request))
|
||||
return 'manifest="/app.manifest"';
|
||||
else
|
||||
return null;
|
||||
});
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
if (req.url !== '/app.manifest') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 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
|
||||
// will continue to fetch the manifest as long as it's available,
|
||||
// even if we now are not including the manifest attribute in the
|
||||
// app HTML. (Firefox for example will continue to display "this
|
||||
// website is asking to store data on your computer for offline
|
||||
// use"). Returning a 404 gets the browser to really turn off the
|
||||
// app cache.
|
||||
|
||||
if (!browserEnabled(__meteor_bootstrap__.categorizeRequest(req))) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// After the browser has downloaded the app files from the server and
|
||||
// has populated the browser's application cache, the browser will
|
||||
// *only* connect to the server and reload the application if the
|
||||
// *contents* of the app manifest file has changed.
|
||||
//
|
||||
// So we have to ensure that if any static client resources change,
|
||||
// something changes in the manifest file. We compute a hash of
|
||||
// everything that gets delivered to the client during the initial
|
||||
// web page load, and include that hash as a comment in the app
|
||||
// manifest. That way if anything changes, the comment changes, and
|
||||
// the browser will reload resources.
|
||||
|
||||
var hash = crypto.createHash('sha1');
|
||||
hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8');
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' || resource.where === 'internal') {
|
||||
hash.update(resource.hash);
|
||||
}
|
||||
});
|
||||
var digest = hash.digest('hex');
|
||||
|
||||
var manifest = "CACHE MANIFEST\n\n";
|
||||
manifest += '# ' + digest + "\n\n";
|
||||
|
||||
manifest += "CACHE:" + "\n";
|
||||
manifest += "/" + "\n";
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' &&
|
||||
! Meteor._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 += "\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(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' &&
|
||||
! Meteor._routePolicy.classify(resource.url) &&
|
||||
!resource.cacheable) {
|
||||
manifest += resource.url + " " + resource.url +
|
||||
"?" + resource.hash + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.AppCache = {
|
||||
config: function(options) {
|
||||
_.each(options, function (value, option) {
|
||||
if (option === 'browsers') {
|
||||
enabledBrowsers = {};
|
||||
_.each(value, function (browser) {
|
||||
enabledBrowsers[browser] = true;
|
||||
});
|
||||
}
|
||||
else if (_.contains(knownBrowsers, option)) {
|
||||
enabledBrowsers[option] = value;
|
||||
}
|
||||
else if (option === 'onlineOnly') {
|
||||
_.each(value, function (urlPrefix) {
|
||||
Meteor._routePolicy.declare(urlPrefix, 'static-online');
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new Error('Invalid AppCache config option: ' + option);
|
||||
}
|
||||
});
|
||||
manifest += "\n";
|
||||
|
||||
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(
|
||||
Meteor._routePolicy.urlPrefixesFor('network'),
|
||||
Meteor._routePolicy.urlPrefixesFor('static-online')
|
||||
),
|
||||
function (urlPrefix) {
|
||||
manifest += urlPrefix + "\n";
|
||||
}
|
||||
};
|
||||
);
|
||||
manifest += "*" + "\n";
|
||||
|
||||
var browserEnabled = function(request) {
|
||||
return enabledBrowsers[request.browser.name];
|
||||
};
|
||||
// content length needs to be based on bytes
|
||||
var body = new Buffer(manifest);
|
||||
|
||||
__meteor_bootstrap__.htmlAttributeHooks.push(function (request) {
|
||||
if (browserEnabled(request))
|
||||
return 'manifest="/app.manifest"';
|
||||
else
|
||||
return null;
|
||||
res.setHeader('Content-Type', 'text/cache-manifest');
|
||||
res.setHeader('Content-Length', body.length);
|
||||
return res.end(body);
|
||||
});
|
||||
|
||||
var sizeCheck = function() {
|
||||
var totalSize = 0;
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client') {
|
||||
totalSize += resource.size;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
if (req.url !== '/app.manifest') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 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
|
||||
// will continue to fetch the manifest as long as it's available,
|
||||
// even if we now are not including the manifest attribute in the
|
||||
// app HTML. (Firefox for example will continue to display "this
|
||||
// website is asking to store data on your computer for offline
|
||||
// use"). Returning a 404 gets the browser to really turn off the
|
||||
// app cache.
|
||||
|
||||
if (!browserEnabled(__meteor_bootstrap__.categorizeRequest(req))) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// After the browser has downloaded the app files from the server and
|
||||
// has populated the browser's application cache, the browser will
|
||||
// *only* connect to the server and reload the application if the
|
||||
// *contents* of the app manifest file has changed.
|
||||
//
|
||||
// So we have to ensure that if any static client resources change,
|
||||
// something changes in the manifest file. We compute a hash of
|
||||
// everything that gets delivered to the client during the initial
|
||||
// web page load, and include that hash as a comment in the app
|
||||
// manifest. That way if anything changes, the comment changes, and
|
||||
// the browser will reload resources.
|
||||
|
||||
var hash = crypto.createHash('sha1');
|
||||
hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8');
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' || resource.where === 'internal') {
|
||||
hash.update(resource.hash);
|
||||
}
|
||||
});
|
||||
var digest = hash.digest('hex');
|
||||
|
||||
var manifest = "CACHE MANIFEST\n\n";
|
||||
manifest += '# ' + digest + "\n\n";
|
||||
|
||||
manifest += "CACHE:" + "\n";
|
||||
manifest += "/" + "\n";
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' &&
|
||||
! Meteor._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 += "\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(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client' &&
|
||||
! Meteor._routePolicy.classify(resource.url) &&
|
||||
!resource.cacheable) {
|
||||
manifest += resource.url + " " + resource.url +
|
||||
"?" + resource.hash + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
manifest += "\n";
|
||||
|
||||
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(
|
||||
Meteor._routePolicy.urlPrefixesFor('network'),
|
||||
Meteor._routePolicy.urlPrefixesFor('static-online')
|
||||
),
|
||||
function (urlPrefix) {
|
||||
manifest += urlPrefix + "\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"
|
||||
);
|
||||
manifest += "*" + "\n";
|
||||
}
|
||||
};
|
||||
|
||||
// content length needs to be based on bytes
|
||||
var body = new Buffer(manifest);
|
||||
|
||||
res.setHeader('Content-Type', 'text/cache-manifest');
|
||||
res.setHeader('Content-Length', body.length);
|
||||
return res.end(body);
|
||||
});
|
||||
|
||||
var sizeCheck = function() {
|
||||
var totalSize = 0;
|
||||
_.each(bundle.manifest, function (resource) {
|
||||
if (resource.where === 'client') {
|
||||
totalSize += resource.size;
|
||||
}
|
||||
});
|
||||
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"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
sizeCheck();
|
||||
|
||||
})();
|
||||
sizeCheck();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var path = require('path');
|
||||
|
||||
Package.describe({
|
||||
summary: "Front-end framework from Twitter"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('jquery');
|
||||
|
||||
var path = Npm.require('path');
|
||||
api.add_files(path.join('css', 'bootstrap.css'), 'client');
|
||||
api.add_files(path.join('css', 'bootstrap-responsive.css'), 'client');
|
||||
api.add_files(path.join('js', 'bootstrap.js'), 'client');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// bundle-time on the server, not in the client. (though I'd like to
|
||||
// support both..)
|
||||
|
||||
var path = require('path');
|
||||
var path = Npm.require('path');
|
||||
|
||||
Package.describe({
|
||||
summary: "Syntax highlighting of code, from Google"
|
||||
|
||||
1
packages/coffeescript/.npm/.gitignore
vendored
Normal file
1
packages/coffeescript/.npm/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
3
packages/coffeescript/.npm/README
Normal file
3
packages/coffeescript/.npm/README
Normal file
@@ -0,0 +1,3 @@
|
||||
This directory and its contents are automatically generated when you change this
|
||||
package's npm dependencies. Commit this directory to source control so that
|
||||
others run the same versions of sub-dependencies.
|
||||
8
packages/coffeescript/.npm/npm-shrinkwrap.json
generated
Normal file
8
packages/coffeescript/.npm/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"coffee-script": {
|
||||
"version": "1.5.0",
|
||||
"from": "coffee-script@1.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
Meteor.__COFFEESCRIPT_PRESENT = true
|
||||
|
||||
@__COFFEESCRIPT_TEST_GLOBAL = 123
|
||||
|
||||
Tinytest.add "coffeescript - compile", (test) -> test.isTrue true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user