mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' into linker.
Conflicts: meteor packages/absolute-url/.gitignore packages/accounts-base/package.js packages/accounts-oauth/package.js packages/audit-argument-checks/.gitignore packages/coffeescript/.gitignore packages/coffeescript/package.js packages/localstorage-polyfill/.gitignore packages/oauth1/package.js packages/oauth2/package.js packages/random/random.js scripts/generate-dev-bundle.sh tools/packages.js tools/run.js tools/server/server.js
This commit is contained in:
41
History.md
41
History.md
@@ -1,6 +1,33 @@
|
||||
|
||||
## vNEXT
|
||||
|
||||
* Separate OAuth flow logic from Accounts into separate packages. The
|
||||
`facebook`, `github`, `google`, `meetup`, `twitter`, and `weibo`
|
||||
packages can be used to perform an OAuth exchange without creating an
|
||||
account and logging in. #1024
|
||||
|
||||
* Make `Meteor.defer` work in an inactive tab in iOS. #1023
|
||||
|
||||
* Allow new `Random` instances to be constructed with specified seed. This
|
||||
can be used to create repeatable test cases for code that picks random
|
||||
values. #1033
|
||||
|
||||
* Fix CoffeeScript error reporting to include source file and line
|
||||
number again. #1052
|
||||
|
||||
* Fix Mongo queries which nested JavaScript RegExp objects inside `$or`. #1089
|
||||
|
||||
* Upgrade Underscore from 1.4.2 to 1.4.4. #776
|
||||
|
||||
* Upgrade http-proxy from 0.8.5 to 0.10.1. #513
|
||||
|
||||
* Upgrade Connect from 1.9.2 to 2.7.10.
|
||||
|
||||
Patches contributed by GitHub users awwx, johnston, and timhaines.
|
||||
|
||||
|
||||
## v0.6.3
|
||||
|
||||
* Add new `check` package for ensuring that a value matches a required
|
||||
type and structure. This is used to validate untrusted input from the
|
||||
client. See http://docs.meteor.com/#match for details.
|
||||
@@ -10,12 +37,15 @@
|
||||
|
||||
* With `autopublish` on, publish many useful fields on `Meteor.users`.
|
||||
|
||||
* Files in the 'client/compatibility/' subdirectory of a Meteor app do
|
||||
* Files in the `client/compatibility/` subdirectory of a Meteor app do
|
||||
not get wrapped in a new variable scope. This is useful for
|
||||
third-party libraries which expect `var` statements at the outermost
|
||||
level to be global.
|
||||
|
||||
* When using the `http` package on the server synchronously, errors
|
||||
* Add synthetic `tap` event for use on touch enabled devices. This is a
|
||||
replacement for `click` that fires immediately.
|
||||
|
||||
* When using the `http` package synchronously on the server, errors
|
||||
are thrown rather than passed in `result.error`
|
||||
|
||||
* The `manager` option to the `Meteor.Collection` constructor is now called
|
||||
@@ -50,6 +80,7 @@
|
||||
|
||||
Patches contributed by GitHub users awwx, jagill, spang, and timhaines.
|
||||
|
||||
|
||||
## v0.6.2.1
|
||||
|
||||
* When authenticating with GitHub, include a user agent string. This
|
||||
@@ -143,8 +174,8 @@ Patches contributed by GitHub users andreas-karlsson and awwx.
|
||||
* `{{#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
|
||||
* 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.
|
||||
|
||||
@@ -212,7 +243,7 @@ mquandalle, Primigenus, raix, reustle, and timhaines.
|
||||
* Publish functions may now return an array of cursors to publish. Currently,
|
||||
the cursors must all be from different collections. #716
|
||||
|
||||
* User documents have id's when onCreateUser and validateNewUser hooks run.
|
||||
* User documents have id's when `onCreateUser` and `validateNewUser` hooks run.
|
||||
|
||||
* Encode and store custom EJSON types in MongoDB.
|
||||
|
||||
|
||||
@@ -346,6 +346,12 @@ node-kexec: https://github.com/jprichardson/node-kexec
|
||||
Copyright (c) 2011-2012 JP Richardson
|
||||
|
||||
|
||||
----------
|
||||
setImmediate: https://github.com/NobleJS/setImmediate
|
||||
----------
|
||||
|
||||
Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola
|
||||
|
||||
|
||||
==============
|
||||
Apache License
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.2.1
|
||||
0.6.3.1
|
||||
|
||||
@@ -2213,6 +2213,12 @@ The user presses a keyboard key. `keypress` is most useful for
|
||||
catching typing in text fields, while `keydown` and `keyup` can be
|
||||
used for arrow keys or modifier keys.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "<code>tap</code>"}} Tap on an element. On touch-enabled
|
||||
devices, this is a replacement to `click` that fires immediately.
|
||||
These events are synthesized from `touchmove` and `touchend`.
|
||||
{{/dtdd}}
|
||||
|
||||
</dl>
|
||||
|
||||
Other DOM events are available as well, but for the events above,
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.2.1
|
||||
0.6.3
|
||||
|
||||
1
examples/other/defer-in-inactive-tab/.meteor/.gitignore
vendored
Normal file
1
examples/other/defer-in-inactive-tab/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
5
examples/other/defer-in-inactive-tab/.meteor/packages
Normal file
5
examples/other/defer-in-inactive-tab/.meteor/packages
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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.
|
||||
|
||||
13
examples/other/defer-in-inactive-tab/README.md
Normal file
13
examples/other/defer-in-inactive-tab/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Defer in Inactive Tab
|
||||
|
||||
Tests that `Meteor.defer` works in an inactive tab in iOS Safari.
|
||||
|
||||
(`setTimeout` and `setInterval` events aren't delivered to inactive
|
||||
tabs in iOS Safari until they become active again).
|
||||
|
||||
Sadly we have to run the test manually because scripts aren't allowed
|
||||
to open windows themselves except in response to user events.
|
||||
|
||||
This test will not run on Chrome for iOS because the storage event is
|
||||
not implemented in that browser. Also doesn't attempt to run on
|
||||
versions of IE that don't support `window.addEventListener`.
|
||||
52
examples/other/defer-in-inactive-tab/test.html
Normal file
52
examples/other/defer-in-inactive-tab/test.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<head>
|
||||
<title>defer in inactive tab</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> route}}
|
||||
</body>
|
||||
|
||||
<template name="route">
|
||||
{{#if isParent}}
|
||||
{{> parent}}
|
||||
{{else}}
|
||||
{{> child}}
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="parent">
|
||||
<h1>Test Defer in Inactive Tab</h1>
|
||||
|
||||
<p>
|
||||
Step one: open second tab:
|
||||
<button id="openTab">Open Tab</button>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Step two: run test:
|
||||
<button id="runTest">Run Test</button>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In a successful test the test status will immediately change to
|
||||
"test successful". (If you switch to the child tab yourself and
|
||||
that makes the test claim to be successful, that's actually an
|
||||
invalid test because you're letting the child tab become the
|
||||
active tab).
|
||||
</p>
|
||||
|
||||
<p style="padding: 1em; outline: 1px solid gray">
|
||||
Test status: <b>{{testStatus}}</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
After the test has run successfully you can close the child tab.
|
||||
</p>
|
||||
|
||||
</template>
|
||||
|
||||
<template name="child">
|
||||
<p>This is the child.</p>
|
||||
<p>Switch back to the first tab and run the test.</p>
|
||||
</template>
|
||||
57
examples/other/defer-in-inactive-tab/test.js
Normal file
57
examples/other/defer-in-inactive-tab/test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
if (Meteor.isClient) {
|
||||
|
||||
var isParent = (window.location.pathname === '/');
|
||||
var isChild = ! isParent;
|
||||
|
||||
Template.route.isParent = function () {
|
||||
return isParent;
|
||||
};
|
||||
|
||||
Template.parent.testStatus = function () {
|
||||
return Session.get('testStatus');
|
||||
};
|
||||
|
||||
Template.parent.events({
|
||||
'click #openTab': function () {
|
||||
window.open('/child');
|
||||
},
|
||||
|
||||
'click #runTest': function () {
|
||||
if (localStorage.getItem('ping') === '!' ||
|
||||
localStorage.getItem('pong') === '!') {
|
||||
Session.set('testStatus', 'Test already run. Close the second tab (if open), refresh this page, and run again.');
|
||||
}
|
||||
else {
|
||||
localStorage.setItem('ping', '!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isParent) {
|
||||
Session.set('testStatus', '');
|
||||
|
||||
Meteor.startup(function () {
|
||||
localStorage.setItem('ping', null);
|
||||
localStorage.setItem('pong', null);
|
||||
});
|
||||
window.addEventListener('storage', function (event) {
|
||||
if (event.key === 'pong' && event.newValue === '!') {
|
||||
Session.set('testStatus', 'test successful');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isChild) {
|
||||
window.addEventListener('storage', function (event) {
|
||||
if (event.key === 'ping' && event.newValue === '!') {
|
||||
// If we used setTimeout here in iOS Safari it wouldn't
|
||||
// work (unless we switched tabs) because setTimeout and
|
||||
// setInterval events don't fire in inactive tabs.
|
||||
Meteor.defer(function () {
|
||||
localStorage.setItem('pong', '!');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
0.6.2.1
|
||||
0.6.3
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.2.1
|
||||
0.6.3
|
||||
|
||||
@@ -162,12 +162,12 @@ if (Meteor.isClient) {
|
||||
'click #controlpane button': function (event) {
|
||||
if (this.key === "fakeConfig") {
|
||||
var service = this.value;
|
||||
if (! Accounts.loginServiceConfiguration.findOne({service: service}))
|
||||
Accounts.loginServiceConfiguration.insert(
|
||||
if (! ServiceConfiguration.configurations.findOne({service: service}))
|
||||
ServiceConfiguration.configurations.insert(
|
||||
{service: service, fake: true});
|
||||
} else if (this.key === "unconfig") {
|
||||
var service = this.value;
|
||||
Accounts.loginServiceConfiguration.remove({service: service});
|
||||
ServiceConfiguration.configurations.remove({service: service});
|
||||
} else if (this.key === "messages") {
|
||||
if (this.value === "error") {
|
||||
Accounts._loginButtonsSession.errorMessage('An error occurred! Gee golly gosh.');
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.2.1
|
||||
0.6.3
|
||||
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.3.4
|
||||
BUNDLE_VERSION=0.3.7
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -49,24 +49,9 @@ Meteor.users = new Meteor.Collection("users", {_preventAutopublish: true});
|
||||
// There is an allow call in accounts_server that restricts this
|
||||
// collection.
|
||||
|
||||
|
||||
// Table containing documents with configuration options for each
|
||||
// login service
|
||||
Accounts.loginServiceConfiguration = new Meteor.Collection(
|
||||
"meteor_accounts_loginServiceConfiguration", {_preventAutopublish: true});
|
||||
// Leave this collection open in insecure mode. In theory, someone could
|
||||
// hijack your oauth connect requests to a different endpoint or appId,
|
||||
// but you did ask for 'insecure'. The advantage is that it is much
|
||||
// easier to write a configuration wizard that works only in insecure
|
||||
// mode.
|
||||
|
||||
|
||||
// Thrown when trying to use a login service which is not configured
|
||||
Accounts.ConfigError = function(description) {
|
||||
this.message = description;
|
||||
};
|
||||
Accounts.ConfigError.prototype = new Error();
|
||||
Accounts.ConfigError.prototype.name = 'Accounts.ConfigError';
|
||||
// loginServiceConfiguration and ConfigError are maintained for backwards compatibility
|
||||
Accounts.loginServiceConfiguration = ServiceConfiguration.configurations;
|
||||
Accounts.ConfigError = ServiceConfiguration.ConfigError;
|
||||
|
||||
// Thrown when the user cancels the login process (eg, closes an oauth
|
||||
// popup, declines retina scan, etc)
|
||||
|
||||
@@ -351,7 +351,7 @@ Meteor.default_server.onAutopublish(function () {
|
||||
|
||||
// Publish all login service configuration fields other than secret.
|
||||
Meteor.publish("meteor.loginServiceConfiguration", function () {
|
||||
return Accounts.loginServiceConfiguration.find({}, {fields: {secret: 0}});
|
||||
return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}});
|
||||
}, {is_auto: true}); // not techincally autopublish, but stops the warning.
|
||||
|
||||
// Allow a one-time configuration for a login service. Modifications
|
||||
@@ -364,9 +364,9 @@ Meteor.methods({
|
||||
// instead of ours).
|
||||
if (!Accounts[options.service])
|
||||
throw new Meteor.Error(403, "Service unknown");
|
||||
if (Accounts.loginServiceConfiguration.findOne({service: options.service}))
|
||||
if (ServiceConfiguration.configurations.findOne({service: options.service}))
|
||||
throw new Meteor.Error(403, "Service " + options.service + " already configured");
|
||||
Accounts.loginServiceConfiguration.insert(options);
|
||||
ServiceConfiguration.configurations.insert(options);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Package.on_use(function (api) {
|
||||
api.use('deps', 'client');
|
||||
api.use('check', 'server');
|
||||
api.use('random', ['client', 'server']);
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
|
||||
// need this because of the Meteor.users collection but in the future
|
||||
// we'd probably want to abstract this away
|
||||
|
||||
@@ -1,28 +1,4 @@
|
||||
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);
|
||||
};
|
||||
Meteor.loginWithFacebook = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Facebook.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
var querystring = Npm.require('querystring');
|
||||
Accounts.oauth.registerService('facebook');
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
// publish all fields including access token, which can legitimately
|
||||
@@ -11,93 +11,3 @@ Accounts.addAutopublishFields({
|
||||
'services.facebook.id', 'services.facebook.username', 'services.facebook.gender'
|
||||
]
|
||||
});
|
||||
|
||||
Accounts.oauth.registerService('facebook', 2, function(query) {
|
||||
|
||||
var response = getTokenResponse(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
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'];
|
||||
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
// checks whether a string parses as JSON
|
||||
var isJSON = function (str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 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 responseContent;
|
||||
try {
|
||||
// Request an access token
|
||||
responseContent = 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
|
||||
}
|
||||
}).content;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Facebook. " + err.message);
|
||||
}
|
||||
|
||||
// If 'responseContent' parses as JSON, it is an error.
|
||||
// XXX which facebook error causes this behvaior?
|
||||
if (isJSON(responseContent)) {
|
||||
throw new Error("Failed to complete OAuth handshake with Facebook. " + responseContent);
|
||||
}
|
||||
|
||||
// Success! Extract the facebook access token and expiration
|
||||
// time from the response
|
||||
var parsedResponse = querystring.parse(responseContent);
|
||||
var fbAccessToken = parsedResponse.access_token;
|
||||
var fbExpires = parsedResponse.expires;
|
||||
|
||||
if (!fbAccessToken) {
|
||||
throw new Error("Failed to complete OAuth handshake with facebook " +
|
||||
"-- can't find access token in HTTP response. " + responseContent);
|
||||
}
|
||||
return {
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get("https://graph.facebook.com/me", {
|
||||
params: {access_token: accessToken}}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Facebook. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,13 +4,10 @@ Package.describe({
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth2-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('facebook', ['client', 'server']);
|
||||
|
||||
api.add_files(
|
||||
['facebook_login_button.css', 'facebook_configure.html', 'facebook_configure.js'],
|
||||
'client');
|
||||
api.add_files('facebook_login_button.css', 'client');
|
||||
|
||||
api.add_files('facebook_common.js', ['client', 'server']);
|
||||
api.add_files('facebook_server.js', 'server');
|
||||
|
||||
@@ -1,26 +1,4 @@
|
||||
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 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;
|
||||
|
||||
Accounts.oauth.initiateLogin(state, loginUrl, callback, {width: 900, height: 450});
|
||||
};
|
||||
Meteor.loginWithGithub = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Github.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
Accounts.oauth.registerService('github');
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
// not sure whether the github api can be used from the browser,
|
||||
// thus not sure if we should be sending access tokens; but we do it
|
||||
@@ -5,67 +7,3 @@ Accounts.addAutopublishFields({
|
||||
forLoggedInUser: ['services.github'],
|
||||
forOtherUsers: ['services.github.username']
|
||||
});
|
||||
|
||||
Accounts.oauth.registerService('github', 2, function(query) {
|
||||
|
||||
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}}
|
||||
};
|
||||
});
|
||||
|
||||
// http://developer.github.com/v3/#user-agent-required
|
||||
var userAgent = "Meteor";
|
||||
if (Meteor.release)
|
||||
userAgent += "/" + Meteor.release;
|
||||
|
||||
var getAccessToken = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'github'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var response;
|
||||
try {
|
||||
response = Meteor.http.post(
|
||||
"https://github.com/login/oauth/access_token", {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"User-Agent": userAgent
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/github?close"),
|
||||
state: query.state
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Github. " + err.message);
|
||||
}
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error);
|
||||
} else {
|
||||
return response.data.access_token;
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get(
|
||||
"https://api.github.com/user", {
|
||||
headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required
|
||||
params: {access_token: accessToken}
|
||||
}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from GitHub. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,13 +4,10 @@ Package.describe({
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth2-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('github', ['client', 'server']);
|
||||
|
||||
api.add_files(
|
||||
['github_login_button.css', 'github_configure.html', 'github_configure.js'],
|
||||
'client');
|
||||
api.add_files('github_login_button.css', 'client');
|
||||
|
||||
api.add_files('github_common.js', ['client', 'server']);
|
||||
api.add_files('github_server.js', 'server');
|
||||
|
||||
@@ -1,39 +1,4 @@
|
||||
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 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.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';
|
||||
|
||||
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);
|
||||
};
|
||||
Meteor.loginWithGoogle = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Google.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
|
||||
var whitelisted = ['id', 'email', 'verified_email', 'name', 'given_name',
|
||||
'family_name', 'picture', 'locale', 'timezone', 'gender'];
|
||||
Accounts.oauth.registerService('google');
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
forLoggedInUser: _.map(
|
||||
@@ -8,82 +6,12 @@ Accounts.addAutopublishFields({
|
||||
// transmitted over ssl or on
|
||||
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
|
||||
// refresh token probably shouldn't be sent down.
|
||||
whitelisted.concat(['accessToken', 'expiresAt']), // don't publish refresh token
|
||||
Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token
|
||||
function (subfield) { return 'services.google.' + subfield; }),
|
||||
|
||||
forOtherUsers: _.map(
|
||||
// even with autopublish, no legitimate web app should be
|
||||
// publishing all users' emails
|
||||
_.without(whitelisted, 'email', 'verified_email'),
|
||||
_.without(Google.whitelistedFields, 'email', 'verified_email'),
|
||||
function (subfield) { return 'services.google.' + subfield; })
|
||||
});
|
||||
|
||||
Accounts.oauth.registerService('google', 2, function(query) {
|
||||
|
||||
var response = getTokens(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
|
||||
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 response;
|
||||
try {
|
||||
response = 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'
|
||||
}});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Google. " + err.message);
|
||||
}
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with Google. " + response.data.error);
|
||||
} else {
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token,
|
||||
expiresIn: response.data.expires_in
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
{params: {access_token: accessToken}}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Google. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,13 +5,10 @@ Package.describe({
|
||||
Package.on_use(function(api) {
|
||||
api.use(['underscore', 'random']);
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth2-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('google', ['client', 'server']);
|
||||
|
||||
api.add_files(
|
||||
['google_login_button.css', 'google_configure.html', 'google_configure.js'],
|
||||
'client');
|
||||
api.add_files('google_login_button.css', 'client');
|
||||
|
||||
api.add_files('google_common.js', ['client', 'server']);
|
||||
api.add_files('google_server.js', 'server');
|
||||
|
||||
@@ -1,33 +1,4 @@
|
||||
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 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;
|
||||
|
||||
// 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});
|
||||
};
|
||||
Meteor.loginWithMeetup = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Meetup.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
Accounts.oauth.registerService('meetup');
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
// publish all fields including access token, which can legitimately
|
||||
// be used from the client (if transmitted over ssl or on
|
||||
@@ -7,54 +9,3 @@ Accounts.addAutopublishFields({
|
||||
});
|
||||
|
||||
|
||||
Accounts.oauth.registerService('meetup', 2, function(query) {
|
||||
|
||||
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 response;
|
||||
try {
|
||||
response = 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
|
||||
}});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Meetup. " + err.message);
|
||||
}
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error);
|
||||
} else {
|
||||
return response.data.access_token;
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
var response = Meteor.http.get(
|
||||
"https://secure.meetup.com/2/members",
|
||||
{params: {member_id: 'self', access_token: accessToken}});
|
||||
return response.data.results && response.data.results[0];
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Meetup: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,13 +4,10 @@ Package.describe({
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth2-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('meetup', ['client', 'server']);
|
||||
|
||||
api.add_files(
|
||||
['meetup_login_button.css', 'meetup_configure.html', 'meetup_configure.js'],
|
||||
'client');
|
||||
api.add_files('meetup_login_button.css', 'client');
|
||||
|
||||
api.add_files('meetup_common.js', ['client', 'server']);
|
||||
api.add_files('meetup_server.js', 'server');
|
||||
|
||||
27
packages/accounts-oauth/oauth_client.js
Normal file
27
packages/accounts-oauth/oauth_client.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Send an OAuth login method to the server. If the user authorized
|
||||
// access in the popup this should log the user in, otherwise
|
||||
// nothing should happen.
|
||||
Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{oauth: {credentialToken: credentialToken}}],
|
||||
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);
|
||||
}
|
||||
}});
|
||||
};
|
||||
|
||||
Accounts.oauth.credentialRequestCompleteHandler = function(callback) {
|
||||
return function (credentialTokenOrError) {
|
||||
if(credentialTokenOrError && credentialTokenOrError instanceof Error) {
|
||||
callback(credentialTokenOrError);
|
||||
} else {
|
||||
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
51
packages/accounts-oauth/oauth_server.js
Normal file
51
packages/accounts-oauth/oauth_server.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Helper for registering OAuth based accounts packages.
|
||||
// Adds an index to the user collection.
|
||||
Accounts.oauth.registerService = function (name) {
|
||||
// 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});
|
||||
|
||||
};
|
||||
|
||||
// For test cleanup only. (Mongo has a limit as to how many indexes it can have
|
||||
// per collection.)
|
||||
Accounts.oauth._unregisterService = function (name) {
|
||||
var index = {};
|
||||
index['services.' + name + '.id'] = 1;
|
||||
Meteor.users._dropIndex(index);
|
||||
};
|
||||
|
||||
|
||||
// Listen to calls to `login` with an oauth option set. This is where
|
||||
// users actually get logged in to meteor via oauth.
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
|
||||
check(options.oauth, {credentialToken: String});
|
||||
|
||||
if (!Oauth.hasCredential(options.oauth.credentialToken)) {
|
||||
// OAuth credentialToken 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');
|
||||
}
|
||||
var result = Oauth.retrieveCredential(options.oauth.credentialToken);
|
||||
if (result instanceof Error)
|
||||
// We tried to login, but there was a fatal error. Report it back
|
||||
// to the user.
|
||||
throw result;
|
||||
else
|
||||
return Accounts.updateOrCreateUserFromExternalService(result.serviceName, result.serviceData, result.options);
|
||||
});
|
||||
|
||||
2
packages/accounts-oauth/oauth_tests.js
Normal file
2
packages/accounts-oauth/oauth_tests.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// XXX Add a test to ensure that successful logins call Accounts.updateOrCreateUserFromExternalService
|
||||
// XXX Add a test to ensure that a missing or failed loginResult is handled correctly
|
||||
@@ -10,8 +10,14 @@ Package.on_use(function (api) {
|
||||
api.use('webapp', 'server');
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('routepolicy', 'server');
|
||||
api.use('oauth', 'server');
|
||||
|
||||
api.add_files('oauth_common.js', ['client', 'server']);
|
||||
api.add_files('oauth_client.js', 'client');
|
||||
api.add_files('oauth_server.js', 'server');
|
||||
});
|
||||
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.add_files("oauth_tests.js", 'server');
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
Accounts.oauth1 = {};
|
||||
@@ -1,140 +0,0 @@
|
||||
|
||||
Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
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() {
|
||||
this.accessToken = twitterfooAccessToken;
|
||||
this.accessTokenSecret = twitterfooAccessTokenSecret;
|
||||
};
|
||||
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// register a fake login service
|
||||
Accounts.oauth.registerService(serviceName, 1, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: twitterfooId,
|
||||
screenName: twitterfooName,
|
||||
accessToken: twitterfooAccessToken,
|
||||
accessTokenSecret: twitterfooAccessTokenSecret
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// simulate logging in using twitterfoo
|
||||
Accounts.oauth1._requestTokens[state] = twitterfooAccessToken;
|
||||
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {
|
||||
state: state,
|
||||
oauth_token: twitterfooAccessToken
|
||||
}
|
||||
};
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".screenName"] = twitterfooName;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.notEqual(user, undefined);
|
||||
test.equal(user.services[serviceName].accessToken,
|
||||
twitterfooAccessToken);
|
||||
test.equal(user.services[serviceName].accessTokenSecret,
|
||||
twitterfooAccessTokenSecret);
|
||||
|
||||
// and that that user has a login token
|
||||
test.equal(user.services.resume.loginTokens.length, 1);
|
||||
var token = user.services.resume.loginTokens[0].token;
|
||||
test.notEqual(token, undefined);
|
||||
|
||||
// and that the login result for that user is prepared
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].id, user._id);
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].token, token);
|
||||
} finally {
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
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();
|
||||
|
||||
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(serviceName, 1, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: twitterfailId,
|
||||
screenName: twitterfailName,
|
||||
accessToken: twitterfailAccessToken,
|
||||
accessTokenSecret: twitterfailAccessTokenSecret
|
||||
},
|
||||
options: {
|
||||
profile: {invalid: true}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// a way to fail new users. duplicated from passwords_tests, but
|
||||
// shouldn't hurt.
|
||||
Accounts.validateNewUser(function (user) {
|
||||
return !(user.profile && user.profile.invalid);
|
||||
});
|
||||
|
||||
// simulate logging in with failure
|
||||
Meteor._suppress_log(1);
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {
|
||||
state: state,
|
||||
oauth_token: twitterfailAccessToken
|
||||
}
|
||||
};
|
||||
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
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
|
||||
test.equal(Accounts.oauth._loginResultForState[state].error, 403);
|
||||
|
||||
// verify error is handed back to login method.
|
||||
test.throws(function () {
|
||||
Meteor.apply('login', [{oauth: {version: 1, state: state}}]);
|
||||
});
|
||||
} finally {
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Accounts.oauth2 = {};
|
||||
@@ -1,98 +0,0 @@
|
||||
Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
var http = Npm.require('http');
|
||||
var foobookId = Random.id();
|
||||
var state = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// 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/" + serviceName + "?close",
|
||||
query: {state: state}};
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var selector = {};
|
||||
selector["services." + serviceName + ".id"] = foobookId;
|
||||
var user = Meteor.users.findOne(selector);
|
||||
test.notEqual(user, undefined);
|
||||
test.equal(user.services[serviceName].id, foobookId);
|
||||
|
||||
// and that that user has a login token
|
||||
test.equal(user.services.resume.loginTokens.length, 1);
|
||||
var token = user.services.resume.loginTokens[0].token;
|
||||
test.notEqual(token, undefined);
|
||||
|
||||
// and that the login result for that user is prepared
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].id, user._id);
|
||||
test.equal(
|
||||
Accounts.oauth._loginResultForState[state].token, token);
|
||||
} finally {
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
var http = Npm.require('http');
|
||||
var state = Random.id();
|
||||
var failbookId = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
Accounts.loginServiceConfiguration.insert({service: serviceName});
|
||||
Accounts[serviceName] = {};
|
||||
|
||||
try {
|
||||
// register a failing login service
|
||||
Accounts.oauth.registerService(serviceName, 2, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: failbookId
|
||||
},
|
||||
options: {
|
||||
profile: {invalid: true}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// a way to fail new users. duplicated from passwords_tests, but
|
||||
// shouldn't hurt.
|
||||
Accounts.validateNewUser(function (user) {
|
||||
return !(user.profile && user.profile.invalid);
|
||||
});
|
||||
|
||||
// simulate logging in with failure
|
||||
Meteor._suppress_log(1);
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {state: state}};
|
||||
Accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
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
|
||||
test.equal(Accounts.oauth._loginResultForState[state].error, 403);
|
||||
|
||||
// verify error is handed back to login method.
|
||||
test.throws(function () {
|
||||
Meteor.apply('login', [{oauth: {version: 2, state: state}}]);
|
||||
});
|
||||
} finally {
|
||||
Accounts.oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ Package.describe({
|
||||
Package.on_use(function(api) {
|
||||
api.use('underscore', ['server']);
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth1-helper', ['client', 'server']);
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('twitter', ['client', 'server']);
|
||||
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
|
||||
api.add_files(
|
||||
['twitter_login_button.css', 'twitter_configure.html', 'twitter_configure.js'],
|
||||
'client');
|
||||
api.add_files('twitter_login_button.css', 'client');
|
||||
|
||||
api.add_files('twitter_common.js', ['client', 'server']);
|
||||
api.add_files('twitter_server.js', 'server');
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
// 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 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 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);
|
||||
};
|
||||
Meteor.loginWithTwitter = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Twitter.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,10 +1,3 @@
|
||||
if (!Accounts.twitter) {
|
||||
Accounts.twitter = {};
|
||||
}
|
||||
|
||||
Accounts.twitter._urls = {
|
||||
requestToken: "https://api.twitter.com/oauth/request_token",
|
||||
authorize: "https://api.twitter.com/oauth/authorize",
|
||||
accessToken: "https://api.twitter.com/oauth/access_token",
|
||||
authenticate: "https://api.twitter.com/oauth/authenticate"
|
||||
};
|
||||
|
||||
@@ -1,36 +1,11 @@
|
||||
// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials
|
||||
var whitelisted = ['profile_image_url', 'profile_image_url_https', 'lang'];
|
||||
Accounts.oauth.registerService('twitter');
|
||||
|
||||
var autopublishedFields = _.map(
|
||||
// don't send access token. https://dev.twitter.com/discussions/5025
|
||||
whitelisted.concat(['id', 'screenName']),
|
||||
Twitter.whitelistedFields.concat(['id', 'screenName']),
|
||||
function (subfield) { return 'services.twitter.' + subfield; });
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
forLoggedInUser: autopublishedFields,
|
||||
forOtherUsers: autopublishedFields
|
||||
});
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// include helpful fields from twitter
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {
|
||||
profile: {
|
||||
name: identity.name
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({
|
||||
loginButtonsSession.closeDropdown();
|
||||
} else if (err instanceof Accounts.LoginCancelledError) {
|
||||
// do nothing
|
||||
} else if (err instanceof Accounts.ConfigError) {
|
||||
} else if (err instanceof ServiceConfiguration.ConfigError) {
|
||||
loginButtonsSession.configureService(serviceName);
|
||||
} else {
|
||||
loginButtonsSession.errorMessage(err.reason || "Unknown error");
|
||||
@@ -30,7 +30,7 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({
|
||||
});
|
||||
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.configured = function () {
|
||||
return !!Accounts.loginServiceConfiguration.findOne({service: this.name});
|
||||
return !!ServiceConfiguration.configurations.findOne({service: this.name});
|
||||
};
|
||||
|
||||
Template._loginButtonsLoggedOutSingleLoginButton.capitalizedName = function () {
|
||||
|
||||
@@ -4,13 +4,10 @@ Package.describe({
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth2-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
api.use('accounts-oauth', ['client', 'server']);
|
||||
api.use('weibo', ['client', 'server']);
|
||||
|
||||
api.add_files(
|
||||
['weibo_login_button.css', 'weibo_configure.html', 'weibo_configure.js'],
|
||||
'client');
|
||||
api.add_files('weibo_login_button.css', 'client');
|
||||
|
||||
api.add_files('weibo_common.js', ['client', 'server']);
|
||||
api.add_files('weibo_server.js', 'server');
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
// 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 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);
|
||||
};
|
||||
Meteor.loginWithWeibo = function(options, callback) {
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Weibo.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
Accounts.oauth.registerService('weibo');
|
||||
|
||||
Accounts.addAutopublishFields({
|
||||
// publish all fields including access token, which can legitimately
|
||||
// be used from the client (if transmitted over ssl or on localhost)
|
||||
@@ -5,72 +7,3 @@ Accounts.addAutopublishFields({
|
||||
forOtherUsers: ['services.weibo.screenName']
|
||||
});
|
||||
|
||||
Accounts.oauth.registerService('weibo', 2, function(query) {
|
||||
|
||||
var response = getTokenResponse(query);
|
||||
var uid = parseInt(response.uid, 10);
|
||||
|
||||
// different parts of weibo's api seem to expect numbers, or strings
|
||||
// for uid. let's make sure they're both the same.
|
||||
if (response.uid !== uid + "")
|
||||
throw new Error("Expected 'uid' to parse to an integer: " + JSON.stringify(response));
|
||||
|
||||
var identity = getIdentity(response.access_token, uid);
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
// We used to store this as a string, so keep it this way rather than
|
||||
// add complexity to Account.updateOrCreateUserFromExternalService or
|
||||
// force a database migration
|
||||
id: uid + "",
|
||||
accessToken: response.access_token,
|
||||
screenName: identity.screen_name,
|
||||
expiresAt: (+new Date) + (1000 * response.expires_in)
|
||||
},
|
||||
options: {profile: {name: identity.screen_name}}
|
||||
};
|
||||
});
|
||||
|
||||
// return an object containining:
|
||||
// - uid
|
||||
// - access_token
|
||||
// - expires_in: lifetime of this token in seconds (5 years(!) right now)
|
||||
var getTokenResponse = function (query) {
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: 'weibo'});
|
||||
if (!config)
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
|
||||
var response;
|
||||
try {
|
||||
response = 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'
|
||||
}});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Weibo. " + err.message);
|
||||
}
|
||||
|
||||
// result.headers["content-type"] is 'text/plain;charset=UTF-8', so
|
||||
// the http package doesn't automatically populate result.data
|
||||
response.data = JSON.parse(response.content);
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with Weibo. " + response.data.error);
|
||||
} else {
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken, userId) {
|
||||
try {
|
||||
return Meteor.http.get(
|
||||
"https://api.weibo.com/2/users/show.json",
|
||||
{params: {access_token: accessToken, uid: userId}}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Weibo. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,8 +113,7 @@ var checkSubtree = function (value, pattern) {
|
||||
return;
|
||||
|
||||
// Basic atomic types.
|
||||
// XXX do we have to worry about if value is boxed (eg String)? will that
|
||||
// happen?
|
||||
// Do not match boxed objects (e.g. String, Boolean)
|
||||
for (var i = 0; i < typeofChecks.length; ++i) {
|
||||
if (pattern === typeofChecks[i][0]) {
|
||||
if (typeof value === typeofChecks[i][1])
|
||||
|
||||
@@ -75,6 +75,9 @@ Tinytest.add("check - check", function (test) {
|
||||
});
|
||||
});
|
||||
fails(true, Match.OneOf(String, Number, undefined, null, [Boolean]));
|
||||
fails(new String("foo"), String);
|
||||
fails(new Boolean(true), Boolean);
|
||||
fails(new Number(123), Number);
|
||||
|
||||
matches([1, 2, 3], [Number]);
|
||||
matches([], [Number]);
|
||||
|
||||
@@ -14,7 +14,11 @@ var handler = function (compileStep) {
|
||||
var output = coffee.compile(source, options);
|
||||
} catch (e) {
|
||||
// XXX better error handling, once the Plugin interface support it
|
||||
throw new Error(e.message);
|
||||
throw new Error(
|
||||
compileStep.inputPath + ':' +
|
||||
(e.location ? (e.location.first_line + ': ') : ' ') +
|
||||
e.message
|
||||
);
|
||||
}
|
||||
|
||||
compileStep.addJavaScript({
|
||||
|
||||
@@ -213,8 +213,8 @@ EJSON.parse = function (item) {
|
||||
};
|
||||
|
||||
EJSON.isBinary = function (obj) {
|
||||
return (typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
|
||||
(obj && obj.$Uint8ArrayPolyfill);
|
||||
return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
|
||||
(obj && obj.$Uint8ArrayPolyfill));
|
||||
};
|
||||
|
||||
EJSON.equals = function (a, b, options) {
|
||||
@@ -298,14 +298,18 @@ EJSON.clone = function (v) {
|
||||
return new Date(v.getTime());
|
||||
if (EJSON.isBinary(v)) {
|
||||
ret = EJSON.newBinary(v.length);
|
||||
for (i = 0; i < v.length; i++) {
|
||||
for (var i = 0; i < v.length; i++) {
|
||||
ret[i] = v[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
// Clone arrays (and turn 'arguments' into an array).
|
||||
if (_.isArray(v) || _.isArguments(v)) {
|
||||
return _.map(v, EJSON.clone);
|
||||
// For some reason, _.map doesn't work in this context on Opera (weird test
|
||||
// failures).
|
||||
ret = [];
|
||||
for (i = 0; i < v.length; i++)
|
||||
ret[i] = EJSON.clone(v[i]);
|
||||
return ret;
|
||||
}
|
||||
// handle general user-defined typed Objects if they have a clone method
|
||||
if (typeof v.clone === 'function') {
|
||||
|
||||
33
packages/facebook/facebook_client.js
Normal file
33
packages/facebook/facebook_client.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Request Facebook credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
Facebook.requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
|
||||
if (!config) {
|
||||
credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialToken = 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=' + credentialToken;
|
||||
|
||||
Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback);
|
||||
};
|
||||
3
packages/facebook/facebook_common.js
Normal file
3
packages/facebook/facebook_common.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if (typeof Facebook === 'undefined') {
|
||||
Facebook = {};
|
||||
}
|
||||
96
packages/facebook/facebook_server.js
Normal file
96
packages/facebook/facebook_server.js
Normal file
@@ -0,0 +1,96 @@
|
||||
var querystring = Npm.require('querystring');
|
||||
|
||||
|
||||
Oauth.registerService('facebook', 2, null, function(query) {
|
||||
|
||||
var response = getTokenResponse(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
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'];
|
||||
|
||||
var fields = _.pick(identity, whitelisted);
|
||||
_.extend(serviceData, fields);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
// checks whether a string parses as JSON
|
||||
var isJSON = function (str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
var getTokenResponse = function (query) {
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError("Service not configured");
|
||||
|
||||
var responseContent;
|
||||
try {
|
||||
// Request an access token
|
||||
responseContent = 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
|
||||
}
|
||||
}).content;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Facebook. " + err.message);
|
||||
}
|
||||
|
||||
// If 'responseContent' parses as JSON, it is an error.
|
||||
// XXX which facebook error causes this behvaior?
|
||||
if (isJSON(responseContent)) {
|
||||
throw new Error("Failed to complete OAuth handshake with Facebook. " + responseContent);
|
||||
}
|
||||
|
||||
// Success! Extract the facebook access token and expiration
|
||||
// time from the response
|
||||
var parsedResponse = querystring.parse(responseContent);
|
||||
var fbAccessToken = parsedResponse.access_token;
|
||||
var fbExpires = parsedResponse.expires;
|
||||
|
||||
if (!fbAccessToken) {
|
||||
throw new Error("Failed to complete OAuth handshake with facebook " +
|
||||
"-- can't find access token in HTTP response. " + responseContent);
|
||||
}
|
||||
return {
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get("https://graph.facebook.com/me", {
|
||||
params: {access_token: accessToken}}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Facebook. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
Facebook.retrieveCredential = function(credentialToken) {
|
||||
return Oauth.retrieveCredential(credentialToken);
|
||||
};
|
||||
20
packages/facebook/package.js
Normal file
20
packages/facebook/package.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Package.describe({
|
||||
summary: "Facebook OAuth flow",
|
||||
// internal for now. Should be external when it has a richer API to do
|
||||
// actual API things with the service, not just handle the OAuth flow.
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
|
||||
api.add_files(
|
||||
['facebook_configure.html', 'facebook_configure.js'],
|
||||
'client');
|
||||
|
||||
api.add_files('facebook_common.js', ['client', 'server']);
|
||||
api.add_files('facebook_server.js', 'server');
|
||||
api.add_files('facebook_client.js', 'client');
|
||||
});
|
||||
32
packages/github/github_client.js
Normal file
32
packages/github/github_client.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Request Github credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
Github.requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'github'});
|
||||
if (!config) {
|
||||
credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var credentialToken = Random.id();
|
||||
|
||||
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=' + credentialToken;
|
||||
|
||||
Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback,
|
||||
{width: 900, height: 450});
|
||||
};
|
||||
3
packages/github/github_common.js
Normal file
3
packages/github/github_common.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if (typeof Github === 'undefined') {
|
||||
Github = {};
|
||||
}
|
||||
67
packages/github/github_server.js
Normal file
67
packages/github/github_server.js
Normal file
@@ -0,0 +1,67 @@
|
||||
Oauth.registerService('github', 2, null, function(query) {
|
||||
|
||||
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}}
|
||||
};
|
||||
});
|
||||
|
||||
// http://developer.github.com/v3/#user-agent-required
|
||||
var userAgent = "Meteor";
|
||||
if (Meteor.release)
|
||||
userAgent += "/" + Meteor.release;
|
||||
|
||||
var getAccessToken = function (query) {
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'github'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError("Service not configured");
|
||||
|
||||
var response;
|
||||
try {
|
||||
response = Meteor.http.post(
|
||||
"https://github.com/login/oauth/access_token", {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"User-Agent": userAgent
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.secret,
|
||||
redirect_uri: Meteor.absoluteUrl("_oauth/github?close"),
|
||||
state: query.state
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Github. " + err.message);
|
||||
}
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error);
|
||||
} else {
|
||||
return response.data.access_token;
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get(
|
||||
"https://api.github.com/user", {
|
||||
headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required
|
||||
params: {access_token: accessToken}
|
||||
}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from GitHub. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
Github.retrieveCredential = function(credentialToken) {
|
||||
return Oauth.retrieveCredential(credentialToken);
|
||||
};
|
||||
20
packages/github/package.js
Normal file
20
packages/github/package.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Package.describe({
|
||||
summary: "Github OAuth flow",
|
||||
// internal for now. Should be external when it has a richer API to do
|
||||
// actual API things with the service, not just handle the OAuth flow.
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
|
||||
api.add_files(
|
||||
['github_configure.html', 'github_configure.js'],
|
||||
'client');
|
||||
|
||||
api.add_files('github_common.js', ['client', 'server']);
|
||||
api.add_files('github_server.js', 'server');
|
||||
api.add_files('github_client.js', 'client');
|
||||
});
|
||||
44
packages/google/google_client.js
Normal file
44
packages/google/google_client.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Request Google credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
Google.requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = {};
|
||||
} else if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'google'});
|
||||
if (!config) {
|
||||
credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialToken = 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.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';
|
||||
|
||||
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=' + credentialToken +
|
||||
'&access_type=' + accessType;
|
||||
|
||||
Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback);
|
||||
};
|
||||
3
packages/google/google_common.js
Normal file
3
packages/google/google_common.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if (typeof Google === 'undefined') {
|
||||
Google = {};
|
||||
}
|
||||
78
packages/google/google_server.js
Normal file
78
packages/google/google_server.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
|
||||
Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name',
|
||||
'family_name', 'picture', 'locale', 'timezone', 'gender'];
|
||||
|
||||
|
||||
Oauth.registerService('google', 2, null, function(query) {
|
||||
|
||||
var response = getTokens(query);
|
||||
var accessToken = response.accessToken;
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
var serviceData = {
|
||||
accessToken: accessToken,
|
||||
expiresAt: (+new Date) + (1000 * response.expiresIn)
|
||||
};
|
||||
|
||||
var fields = _.pick(identity, Google.whitelistedFields);
|
||||
_.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 = ServiceConfiguration.configurations.findOne({service: 'google'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError("Service not configured");
|
||||
|
||||
var response;
|
||||
try {
|
||||
response = 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'
|
||||
}});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Google. " + err.message);
|
||||
}
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with Google. " + response.data.error);
|
||||
} else {
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token,
|
||||
expiresIn: response.data.expires_in
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
return Meteor.http.get(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo",
|
||||
{params: {access_token: accessToken}}).data;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Google. " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
Google.retrieveCredential = function(credentialToken) {
|
||||
return Oauth.retrieveCredential(credentialToken);
|
||||
};
|
||||
20
packages/google/package.js
Normal file
20
packages/google/package.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Package.describe({
|
||||
summary: "Google OAuth flow",
|
||||
// internal for now. Should be external when it has a richer API to do
|
||||
// actual API things with the service, not just handle the OAuth flow.
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
|
||||
api.add_files(
|
||||
['google_configure.html', 'google_configure.js'],
|
||||
'client');
|
||||
|
||||
api.add_files('google_common.js', ['client', 'server']);
|
||||
api.add_files('google_server.js', 'server');
|
||||
api.add_files('google_client.js', 'client');
|
||||
});
|
||||
38
packages/meetup/meetup_client.js
Normal file
38
packages/meetup/meetup_client.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// Request Meetup credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
Meetup.requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
|
||||
if (!config) {
|
||||
credentialRequestCompleteCallback && credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError("Service not configured"));
|
||||
return;
|
||||
}
|
||||
var credentialToken = Random.id();
|
||||
|
||||
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=' + credentialToken;
|
||||
|
||||
// meetup box gets taller when permissions requested.
|
||||
var height = 620;
|
||||
if (_.without(scope, 'basic').length)
|
||||
height += 130;
|
||||
|
||||
Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback,
|
||||
{width: 900, height: height});
|
||||
};
|
||||
3
packages/meetup/meetup_common.js
Normal file
3
packages/meetup/meetup_common.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if (typeof Meetup === 'undefined') {
|
||||
Meetup = {};
|
||||
}
|
||||
56
packages/meetup/meetup_server.js
Normal file
56
packages/meetup/meetup_server.js
Normal file
@@ -0,0 +1,56 @@
|
||||
Oauth.registerService('meetup', 2, null, function(query) {
|
||||
|
||||
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 = ServiceConfiguration.configurations.findOne({service: 'meetup'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError("Service not configured");
|
||||
|
||||
var response;
|
||||
try {
|
||||
response = 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
|
||||
}});
|
||||
} catch (err) {
|
||||
throw new Error("Failed to complete OAuth handshake with Meetup. " + err.message);
|
||||
}
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete OAuth handshake with Meetup. " + response.data.error);
|
||||
} else {
|
||||
return response.data.access_token;
|
||||
}
|
||||
};
|
||||
|
||||
var getIdentity = function (accessToken) {
|
||||
try {
|
||||
var response = Meteor.http.get(
|
||||
"https://secure.meetup.com/2/members",
|
||||
{params: {member_id: 'self', access_token: accessToken}});
|
||||
return response.data.results && response.data.results[0];
|
||||
} catch (err) {
|
||||
throw new Error("Failed to fetch identity from Meetup: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Meetup.retrieveCredential = function(credentialToken) {
|
||||
return Oauth.retrieveCredential(credentialToken);
|
||||
};
|
||||
20
packages/meetup/package.js
Normal file
20
packages/meetup/package.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Package.describe({
|
||||
summary: "Meetup OAuth flow",
|
||||
// internal for now. Should be external when it has a richer API to do
|
||||
// actual API things with the service, not just handle the OAuth flow.
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
api.use('templating', 'client');
|
||||
|
||||
api.add_files(
|
||||
['meetup_configure.html', 'meetup_configure.js'],
|
||||
'client');
|
||||
|
||||
api.add_files('meetup_common.js', ['client', 'server']);
|
||||
api.add_files('meetup_server.js', 'server');
|
||||
api.add_files('meetup_client.js', 'client');
|
||||
});
|
||||
@@ -16,6 +16,7 @@ Package.on_use(function (api, where) {
|
||||
api.add_files('client_environment.js', 'client');
|
||||
api.add_files('server_environment.js', 'server');
|
||||
api.add_files('helpers.js', ['client', 'server']);
|
||||
api.add_files('setimmediate.js', ['client', 'server']);
|
||||
api.add_files('timers.js', ['client', 'server']);
|
||||
api.add_files('errors.js', ['client', 'server']);
|
||||
api.add_files('fiber_helpers.js', 'server');
|
||||
@@ -45,4 +46,6 @@ Package.on_test(function (api) {
|
||||
api.add_files('fiber_helpers_test.js', ['server']);
|
||||
|
||||
api.add_files('url_tests.js', ['client', 'server']);
|
||||
|
||||
api.add_files('timers_tests.js', ['client', 'server']);
|
||||
});
|
||||
|
||||
141
packages/meteor/setimmediate.js
Normal file
141
packages/meteor/setimmediate.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// Chooses one of three setImmediate implementations:
|
||||
//
|
||||
// * Native setImmediate (IE 10, Node 0.9+)
|
||||
//
|
||||
// * postMessage (many browsers)
|
||||
//
|
||||
// * setTimeout (fallback)
|
||||
//
|
||||
// The postMessage implementation is based on
|
||||
// https://github.com/NobleJS/setImmediate/tree/1.0.1
|
||||
//
|
||||
// Don't use `nextTick` for Node since it runs its callbacks before
|
||||
// I/O, which is stricter than we're looking for.
|
||||
//
|
||||
// Not installed as a polyfill, as our public API is `Meteor.defer`.
|
||||
// Since we're not trying to be a polyfill, we have some
|
||||
// simplifications:
|
||||
//
|
||||
// If one invocation of a setImmediate callback pauses itself by a
|
||||
// call to alert/prompt/showModelDialog, the NobleJS polyfill
|
||||
// implementation ensured that no setImmedate callback would run until
|
||||
// the first invocation completed. While correct per the spec, what it
|
||||
// would mean for us in practice is that any reactive updates relying
|
||||
// on Meteor.defer would be hung in the main window until the modal
|
||||
// dialog was dismissed. Thus we only ensure that a setImmediate
|
||||
// function is called in a later event loop.
|
||||
//
|
||||
// We don't need to support using a string to be eval'ed for the
|
||||
// callback, arguments to the function, or clearImmediate.
|
||||
|
||||
"use strict";
|
||||
|
||||
var global = this;
|
||||
|
||||
|
||||
// IE 10, Node >= 9.1
|
||||
|
||||
function useSetImmediate() {
|
||||
if (! global.setImmediate)
|
||||
return null;
|
||||
else {
|
||||
var setImmediate = function (fn) {
|
||||
global.setImmediate(fn);
|
||||
};
|
||||
setImmediate.implementation = 'setImmediate';
|
||||
return setImmediate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Android 2.3.6, Chrome 26, Firefox 20, IE 8-9, iOS 5.1.1 Safari
|
||||
|
||||
function usePostMessage() {
|
||||
// The test against `importScripts` prevents this implementation
|
||||
// from being installed inside a web worker, where
|
||||
// `global.postMessage` means something completely different and
|
||||
// can't be used for this purpose.
|
||||
|
||||
if (!global.postMessage || global.importScripts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid synchronous post message implementations.
|
||||
|
||||
var postMessageIsAsynchronous = true;
|
||||
var oldOnMessage = global.onmessage;
|
||||
global.onmessage = function () {
|
||||
postMessageIsAsynchronous = false;
|
||||
};
|
||||
global.postMessage("", "*");
|
||||
global.onmessage = oldOnMessage;
|
||||
|
||||
if (! postMessageIsAsynchronous)
|
||||
return null;
|
||||
|
||||
var funcIndex = 0;
|
||||
var funcs = {};
|
||||
|
||||
// Installs an event handler on `global` for the `message` event: see
|
||||
// * https://developer.mozilla.org/en/DOM/window.postMessage
|
||||
// * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
|
||||
|
||||
// XXX use Random.id() here?
|
||||
var MESSAGE_PREFIX = "Meteor._setImmediate." + Math.random() + '.';
|
||||
|
||||
function isStringAndStartsWith(string, putativeStart) {
|
||||
return (typeof string === "string" &&
|
||||
string.substring(0, putativeStart.length) === putativeStart);
|
||||
}
|
||||
|
||||
function onGlobalMessage(event) {
|
||||
// This will catch all incoming messages (even from other
|
||||
// windows!), so we need to try reasonably hard to avoid letting
|
||||
// anyone else trick us into firing off. We test the origin is
|
||||
// still this window, and that a (randomly generated)
|
||||
// unpredictable identifying prefix is present.
|
||||
if (event.source === global &&
|
||||
isStringAndStartsWith(event.data, MESSAGE_PREFIX)) {
|
||||
var index = event.data.substring(MESSAGE_PREFIX.length);
|
||||
try {
|
||||
if (funcs[index])
|
||||
funcs[index]();
|
||||
}
|
||||
finally {
|
||||
delete funcs[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (global.addEventListener) {
|
||||
global.addEventListener("message", onGlobalMessage, false);
|
||||
} else {
|
||||
global.attachEvent("onmessage", onGlobalMessage);
|
||||
}
|
||||
|
||||
var setImmediate = function (fn) {
|
||||
// Make `global` post a message to itself with the handle and
|
||||
// identifying prefix, thus asynchronously invoking our
|
||||
// onGlobalMessage listener above.
|
||||
++funcIndex;
|
||||
funcs[funcIndex] = fn;
|
||||
global.postMessage(MESSAGE_PREFIX + funcIndex, "*");
|
||||
};
|
||||
setImmediate.implementation = 'postMessage';
|
||||
return setImmediate;
|
||||
}
|
||||
|
||||
|
||||
function useTimeout() {
|
||||
var setImmediate = function (fn) {
|
||||
global.setTimeout(fn, 0);
|
||||
};
|
||||
setImmediate.implementation = 'setTimeout';
|
||||
return setImmediate;
|
||||
}
|
||||
|
||||
|
||||
Meteor._setImmediate =
|
||||
useSetImmediate() ||
|
||||
usePostMessage() ||
|
||||
useTimeout();
|
||||
@@ -1,36 +1,31 @@
|
||||
var withoutInvocation = function (f) {
|
||||
if (Meteor._CurrentInvocation) {
|
||||
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
|
||||
throw new Error("Can't set timers inside simulations");
|
||||
return function () { Meteor._CurrentInvocation.withValue(null, f); };
|
||||
}
|
||||
else
|
||||
return f;
|
||||
};
|
||||
|
||||
var bindAndCatch = function (context, f) {
|
||||
return Meteor.bindEnvironment(withoutInvocation(f), function (e) {
|
||||
// XXX report nicely (or, should we catch it at all?)
|
||||
Meteor._debug("Exception from " + context + ":", e);
|
||||
});
|
||||
};
|
||||
|
||||
_.extend(Meteor, {
|
||||
// Meteor.setTimeout and Meteor.setInterval callbacks scheduled
|
||||
// inside a server method are not part of the method invocation and
|
||||
// should clear out the CurrentInvocation environment variable.
|
||||
|
||||
setTimeout: function (f, duration) {
|
||||
if (Meteor._CurrentInvocation) {
|
||||
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
|
||||
throw new Error("Can't set timers inside simulations");
|
||||
|
||||
var f_with_ci = f;
|
||||
f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); };
|
||||
}
|
||||
|
||||
return setTimeout(Meteor.bindEnvironment(f, function (e) {
|
||||
// XXX report nicely (or, should we catch it at all?)
|
||||
Meteor._debug("Exception from setTimeout callback:", e.stack);
|
||||
}), duration);
|
||||
return setTimeout(bindAndCatch("setTimeout callback", f), duration);
|
||||
},
|
||||
|
||||
setInterval: function (f, duration) {
|
||||
if (Meteor._CurrentInvocation) {
|
||||
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
|
||||
throw new Error("Can't set timers inside simulations");
|
||||
|
||||
var f_with_ci = f;
|
||||
f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); };
|
||||
}
|
||||
|
||||
return setInterval(Meteor.bindEnvironment(f, function (e) {
|
||||
// XXX report nicely (or, should we catch it at all?)
|
||||
Meteor._debug("Exception from setInterval callback:", e);
|
||||
}), duration);
|
||||
return setInterval(bindAndCatch("setInterval callback", f), duration);
|
||||
},
|
||||
|
||||
clearInterval: function(x) {
|
||||
@@ -41,16 +36,11 @@ _.extend(Meteor, {
|
||||
return clearTimeout(x);
|
||||
},
|
||||
|
||||
// won't be necessary once we clobber the global setTimeout
|
||||
//
|
||||
// XXX consider making this guarantee ordering of defer'd callbacks, like
|
||||
// Deps.afterFlush or Node's nextTick (in practice). Then tests can do:
|
||||
// callSomethingThatDefersSomeWork();
|
||||
// Meteor.defer(expect(somethingThatValidatesThatTheWorkHappened));
|
||||
defer: function (f) {
|
||||
// Older Firefox will pass an argument to the setTimeout callback
|
||||
// function, indicating the "actual lateness." It's non-standard,
|
||||
// so for defer, standardize on not having it.
|
||||
Meteor.setTimeout(function () {f();}, 0);
|
||||
Meteor._setImmediate(bindAndCatch("defer callback", f));
|
||||
}
|
||||
});
|
||||
|
||||
21
packages/meteor/timers_tests.js
Normal file
21
packages/meteor/timers_tests.js
Normal file
@@ -0,0 +1,21 @@
|
||||
Tinytest.addAsync('timers - defer', function (test, onComplete) {
|
||||
var x = 'a';
|
||||
Meteor.defer(function () {
|
||||
test.equal(x, 'b');
|
||||
onComplete();
|
||||
});
|
||||
x = 'b';
|
||||
});
|
||||
|
||||
Tinytest.addAsync('timers - nested defer', function (test, onComplete) {
|
||||
var x = 'a';
|
||||
Meteor.defer(function () {
|
||||
test.equal(x, 'b');
|
||||
Meteor.defer(function () {
|
||||
test.equal(x, 'c');
|
||||
onComplete();
|
||||
});
|
||||
x = 'c';
|
||||
});
|
||||
x = 'b';
|
||||
});
|
||||
7
packages/mongo-livedata/.npm/npm-shrinkwrap.json
generated
7
packages/mongo-livedata/.npm/npm-shrinkwrap.json
generated
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mongodb": {
|
||||
"version": "1.2.13",
|
||||
"version": "1.3.6",
|
||||
"dependencies": {
|
||||
"bson": {
|
||||
"version": "0.1.8"
|
||||
"version": "0.1.9"
|
||||
},
|
||||
"kerberos": {
|
||||
"version": "0.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,8 +257,6 @@ Meteor.Collection._rewriteSelector = function (selector) {
|
||||
var ret = {};
|
||||
_.each(selector, function (value, key) {
|
||||
if (value instanceof RegExp) {
|
||||
// XXX should also do this translation at lower levels (eg if the outer
|
||||
// level is $and/$or/$nor, or if there's an $elemMatch)
|
||||
ret[key] = {$regex: value.source};
|
||||
var regexOptions = '';
|
||||
// JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options
|
||||
@@ -270,6 +268,12 @@ Meteor.Collection._rewriteSelector = function (selector) {
|
||||
if (regexOptions)
|
||||
ret[key].$options = regexOptions;
|
||||
}
|
||||
else if (_.contains(['$or','$and','$nor'], key)) {
|
||||
// Translate lower levels of $and/$or/$nor
|
||||
ret[key] = _.map(value, function (v) {
|
||||
return Meteor.Collection._rewriteSelector(v);
|
||||
});
|
||||
}
|
||||
else
|
||||
ret[key] = value;
|
||||
});
|
||||
|
||||
@@ -823,6 +823,47 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) {
|
||||
{x: {$regex: '^o+B'}});
|
||||
test.equal(Meteor.Collection._rewriteSelector('foo'),
|
||||
{_id: 'foo'});
|
||||
|
||||
test.equal(
|
||||
Meteor.Collection._rewriteSelector(
|
||||
{'$or': [
|
||||
{x: /^o/},
|
||||
{y: /^p/},
|
||||
{z: 'q'}
|
||||
]}
|
||||
),
|
||||
{'$or': [
|
||||
{x: {$regex: '^o'}},
|
||||
{y: {$regex: '^p'}},
|
||||
{z: 'q'}
|
||||
]}
|
||||
);
|
||||
|
||||
test.equal(
|
||||
Meteor.Collection._rewriteSelector(
|
||||
{'$or': [
|
||||
{'$and': [
|
||||
{x: /^a/i},
|
||||
{y: /^b/}
|
||||
]},
|
||||
{'$nor': [
|
||||
{s: /^c/},
|
||||
{t: /^d/i}
|
||||
]}
|
||||
]}
|
||||
),
|
||||
{'$or': [
|
||||
{'$and': [
|
||||
{x: {$regex: '^a', $options: 'i'}},
|
||||
{y: {$regex: '^b'}}
|
||||
]},
|
||||
{'$nor': [
|
||||
{s: {$regex: '^c'}},
|
||||
{t: {$regex: '^d', $options: 'i'}}
|
||||
]}
|
||||
]}
|
||||
);
|
||||
|
||||
var oid = new Meteor.Collection.ObjectID();
|
||||
test.equal(Meteor.Collection._rewriteSelector(oid),
|
||||
{_id: oid});
|
||||
|
||||
@@ -12,7 +12,7 @@ Package.describe({
|
||||
internal: true
|
||||
});
|
||||
|
||||
Npm.depends({mongodb: "1.2.13"});
|
||||
Npm.depends({mongodb: "1.3.6"});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Open a popup window pointing to a OAuth handshake page
|
||||
//
|
||||
// @param state {String} The OAuth state generated by the client
|
||||
// @param credentialToken {String} The OAuth credentialToken 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
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken 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) {
|
||||
Oauth.initiateLogin = function(credentialToken, url, credentialRequestCompleteCallback, dimensions) {
|
||||
// default dimensions that worked well for facebook and google
|
||||
var popup = openCenteredPopup(
|
||||
url,
|
||||
@@ -15,33 +15,26 @@ Accounts.oauth.initiateLogin = function(state, url, callback, dimensions) {
|
||||
(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) {
|
||||
try {
|
||||
// Fix for #328 - added a second test criteria (popup.closed === undefined)
|
||||
// to humour this Android quirk:
|
||||
// http://code.google.com/p/android/issues/detail?id=21061
|
||||
var popupClosed = popup.closed || popup.closed === undefined;
|
||||
} catch (e) {
|
||||
// For some unknown reason, IE9 (and others?) sometimes (when
|
||||
// the popup closes too quickly?) throws "SCRIPT16386: No such
|
||||
// interface supported" when trying to read 'popup.closed'. Try
|
||||
// again in 100ms.
|
||||
return;
|
||||
}
|
||||
|
||||
if (popupClosed) {
|
||||
clearInterval(checkPopupOpen);
|
||||
tryLoginAfterPopupClosed(state, callback);
|
||||
credentialRequestCompleteCallback(credentialToken);
|
||||
}
|
||||
}, 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'
|
||||
1
packages/oauth/oauth_common.js
Normal file
1
packages/oauth/oauth_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Oauth = {};
|
||||
@@ -2,7 +2,8 @@ var Fiber = Npm.require('fibers');
|
||||
|
||||
Meteor._routePolicy.declare('/_oauth/', 'network');
|
||||
|
||||
Accounts.oauth._services = {};
|
||||
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
|
||||
@@ -11,6 +12,7 @@ Accounts.oauth._services = {};
|
||||
//
|
||||
// @param name {String} e.g. "google", "facebook"
|
||||
// @param version {Number} OAuth version (1 or 2)
|
||||
// @param urls For OAuth1 only, specify the service's urls
|
||||
// @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
|
||||
@@ -18,70 +20,41 @@ Accounts.oauth._services = {};
|
||||
// - {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])
|
||||
Oauth.registerService = function (name, version, urls, handleOauthRequest) {
|
||||
if (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.oauth._services[name] = {
|
||||
Oauth._services[name] = {
|
||||
serviceName: name,
|
||||
version: version,
|
||||
urls: urls,
|
||||
handleOauthRequest: handleOauthRequest
|
||||
};
|
||||
};
|
||||
|
||||
// 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);
|
||||
// For test cleanup only.
|
||||
Oauth._unregisterService = function (name) {
|
||||
delete Oauth._services[name];
|
||||
};
|
||||
|
||||
|
||||
// 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`
|
||||
// method is called. Maps credentialToken --> return value of `login`
|
||||
//
|
||||
// XXX we should periodically clear old entries
|
||||
Accounts.oauth._loginResultForState = {};
|
||||
Oauth._loginResultForCredentialToken = {};
|
||||
|
||||
// Listen to calls to `login` with an oauth option set. This is where
|
||||
// users actually get logged in to meteor via oauth.
|
||||
Accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
Oauth.hasCredential = function(credentialToken) {
|
||||
return _.has(Oauth._loginResultForCredentialToken, credentialToken);
|
||||
}
|
||||
|
||||
check(options.oauth, {state: String});
|
||||
|
||||
if (!_.has(Accounts.oauth._loginResultForState, options.oauth.state)) {
|
||||
// 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');
|
||||
}
|
||||
var result = Accounts.oauth._loginResultForState[options.oauth.state];
|
||||
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;
|
||||
});
|
||||
Oauth.retrieveCredential = function(credentialToken) {
|
||||
result = Oauth._loginResultForCredentialToken[credentialToken];
|
||||
delete Oauth._loginResultForCredentialToken[credentialToken];
|
||||
return result;
|
||||
}
|
||||
|
||||
// Listen to incoming OAuth http requests
|
||||
__meteor_bootstrap__.app
|
||||
@@ -90,11 +63,12 @@ __meteor_bootstrap__.app
|
||||
// calls and nothing else is wrapping this in a fiber
|
||||
// automatically
|
||||
Fiber(function () {
|
||||
Accounts.oauth._middleware(req, res, next);
|
||||
Oauth._middleware(req, res, next);
|
||||
}).run();
|
||||
});
|
||||
|
||||
Accounts.oauth._middleware = function (req, res, next) {
|
||||
|
||||
Oauth._middleware = function (req, res, next) {
|
||||
// Make sure to catch any exceptions because otherwise we'd crash
|
||||
// the runner
|
||||
try {
|
||||
@@ -105,7 +79,7 @@ Accounts.oauth._middleware = function (req, res, next) {
|
||||
return;
|
||||
}
|
||||
|
||||
var service = Accounts.oauth._services[serviceName];
|
||||
var service = Oauth._services[serviceName];
|
||||
|
||||
// Skip everything if there's no service set by the oauth middleware
|
||||
if (!service)
|
||||
@@ -115,9 +89,9 @@ Accounts.oauth._middleware = function (req, res, next) {
|
||||
ensureConfigured(serviceName);
|
||||
|
||||
if (service.version === 1)
|
||||
Accounts.oauth1._handleRequest(service, req.query, res);
|
||||
Oauth1._handleRequest(service, req.query, res);
|
||||
else if (service.version === 2)
|
||||
Accounts.oauth2._handleRequest(service, req.query, res);
|
||||
Oauth2._handleRequest(service, req.query, res);
|
||||
else
|
||||
throw new Error("Unexpected OAuth version " + service.version);
|
||||
} catch (err) {
|
||||
@@ -129,12 +103,12 @@ Accounts.oauth._middleware = function (req, res, next) {
|
||||
// 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;
|
||||
Oauth._loginResultForCredentialToken[req.query.state] = 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
|
||||
// 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.
|
||||
@@ -167,12 +141,12 @@ var oauthServiceName = function (req) {
|
||||
|
||||
// Make sure we're configured
|
||||
var ensureConfigured = function(serviceName) {
|
||||
if (!Accounts.loginServiceConfiguration.findOne({service: serviceName})) {
|
||||
throw new Accounts.ConfigError("Service not configured");
|
||||
if (!ServiceConfiguration.configurations.findOne({service: serviceName})) {
|
||||
throw new ServiceConfiguration.ConfigError("Service not configured");
|
||||
};
|
||||
};
|
||||
|
||||
Accounts.oauth._renderOauthResults = function(res, query) {
|
||||
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
|
||||
@@ -192,3 +166,4 @@ var closePopup = function(res) {
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
};
|
||||
|
||||
12
packages/oauth/package.js
Normal file
12
packages/oauth/package.js
Normal file
@@ -0,0 +1,12 @@
|
||||
Package.describe({
|
||||
summary: "Common code for OAuth-based services",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('routepolicy', 'server');
|
||||
|
||||
api.add_files('oauth_common.js', ['client', 'server']);
|
||||
api.add_files('oauth_client.js', 'client');
|
||||
api.add_files('oauth_server.js', 'server');
|
||||
});
|
||||
1
packages/oauth1/oauth1_common.js
Normal file
1
packages/oauth1/oauth1_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Oauth1 = {};
|
||||
@@ -1,15 +1,15 @@
|
||||
// A place to store request tokens pending verification
|
||||
Accounts.oauth1._requestTokens = {};
|
||||
Oauth1._requestTokens = {};
|
||||
|
||||
// connect middleware
|
||||
Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
Oauth1._handleRequest = function (service, query, res) {
|
||||
|
||||
var config = Accounts.loginServiceConfiguration.findOne({service: service.serviceName});
|
||||
var config = ServiceConfiguration.configurations.findOne({service: service.serviceName});
|
||||
if (!config) {
|
||||
throw new Accounts.ConfigError("Service " + service.serviceName + " not configured");
|
||||
throw new ServiceConfiguration.ConfigError("Service " + service.serviceName + " not configured");
|
||||
}
|
||||
|
||||
var urls = Accounts[service.serviceName]._urls;
|
||||
var urls = service.urls;
|
||||
var oauthBinding = new OAuth1Binding(
|
||||
config.consumerKey, config.secret, urls);
|
||||
|
||||
@@ -20,7 +20,7 @@ Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
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;
|
||||
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;
|
||||
@@ -33,8 +33,8 @@ Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
// 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];
|
||||
var requestToken = Oauth1._requestTokens[query.state];
|
||||
delete Oauth1._requestTokens[query.state];
|
||||
|
||||
// Verify user authorized access and the oauth_token matches
|
||||
// the requestToken from previous step
|
||||
@@ -49,14 +49,16 @@ Accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
// 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);
|
||||
// Add the login result to the result map
|
||||
Oauth._loginResultForCredentialToken[query.state] = {
|
||||
serviceName: service.serviceName,
|
||||
serviceData: oauthResult.serviceData,
|
||||
options: oauthResult.options
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
Oauth._renderOauthResults(res, query);
|
||||
};
|
||||
73
packages/oauth1/oauth1_tests.js
Normal file
73
packages/oauth1/oauth1_tests.js
Normal file
@@ -0,0 +1,73 @@
|
||||
Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) {
|
||||
var http = Npm.require('http');
|
||||
var twitterfooId = Random.id();
|
||||
var twitterfooName = 'nickname' + Random.id();
|
||||
var twitterfooAccessToken = Random.id();
|
||||
var twitterfooAccessTokenSecret = Random.id();
|
||||
var twitterOption1 = Random.id();
|
||||
var credentialToken = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
var urls = {
|
||||
requestToken: "https://example.com/oauth/request_token",
|
||||
authorize: "https://example.com/oauth/authorize",
|
||||
accessToken: "https://example.com/oauth/access_token",
|
||||
authenticate: "https://example.com/oauth/authenticate"
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.prepareRequestToken = function() {};
|
||||
OAuth1Binding.prototype.prepareAccessToken = function() {
|
||||
this.accessToken = twitterfooAccessToken;
|
||||
this.accessTokenSecret = twitterfooAccessTokenSecret;
|
||||
};
|
||||
|
||||
ServiceConfiguration.configurations.insert({service: serviceName});
|
||||
|
||||
try {
|
||||
// register a fake login service
|
||||
Oauth.registerService(serviceName, 1, urls, function (query) {
|
||||
return {
|
||||
serviceData: {
|
||||
id: twitterfooId,
|
||||
screenName: twitterfooName,
|
||||
accessToken: twitterfooAccessToken,
|
||||
accessTokenSecret: twitterfooAccessTokenSecret
|
||||
},
|
||||
options: {
|
||||
option1: twitterOption1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// simulate logging in using twitterfoo
|
||||
Oauth1._requestTokens[credentialToken] = twitterfooAccessToken;
|
||||
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {
|
||||
state: credentialToken,
|
||||
oauth_token: twitterfooAccessToken
|
||||
}
|
||||
};
|
||||
Oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// Test that right data is placed on the loginResult map
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceName, serviceName);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceData.id, twitterfooId);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceData.screenName, twitterfooName);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceData.accessToken, twitterfooAccessToken);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceData.accessTokenSecret, twitterfooAccessTokenSecret);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].options.option1, twitterOption1);
|
||||
|
||||
} finally {
|
||||
Oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('accounts-oauth-helper', ['client', 'server']);
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
api.use('oauth', 'client');
|
||||
|
||||
api.add_files('oauth1_binding.js', 'server');
|
||||
api.add_files('oauth1_common.js', ['client', 'server']);
|
||||
@@ -15,7 +15,7 @@ Package.on_use(function (api) {
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('random');
|
||||
api.use('accounts-base');
|
||||
api.use('accounts-oauth1-helper', 'server');
|
||||
api.use('service-configuration', 'server');
|
||||
api.use('oauth1', 'server');
|
||||
api.add_files("oauth1_tests.js", 'server');
|
||||
});
|
||||
1
packages/oauth2/oauth2_common.js
Normal file
1
packages/oauth2/oauth2_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Oauth2 = {};
|
||||
@@ -1,5 +1,5 @@
|
||||
// connect middleware
|
||||
Accounts.oauth2._handleRequest = function (service, query, res) {
|
||||
Oauth2._handleRequest = function (service, query, res) {
|
||||
// check if user authorized access
|
||||
if (!query.error) {
|
||||
// Prepare the login results before returning. This way the
|
||||
@@ -8,13 +8,15 @@ Accounts.oauth2._handleRequest = function (service, query, res) {
|
||||
// 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);
|
||||
// Add the login result to the result map
|
||||
Oauth._loginResultForCredentialToken[query.state] = {
|
||||
serviceName: service.serviceName,
|
||||
serviceData: oauthResult.serviceData,
|
||||
options: oauthResult.options
|
||||
};
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Accounts.oauth._renderOauthResults(res, query);
|
||||
Oauth._renderOauthResults(res, query);
|
||||
};
|
||||
36
packages/oauth2/oauth2_tests.js
Normal file
36
packages/oauth2/oauth2_tests.js
Normal file
@@ -0,0 +1,36 @@
|
||||
Tinytest.add("oauth2 - loginResultForCredentialToken is stored", function (test) {
|
||||
var http = Npm.require('http');
|
||||
var foobookId = Random.id();
|
||||
var foobookOption1 = Random.id();
|
||||
var credentialToken = Random.id();
|
||||
var serviceName = Random.id();
|
||||
|
||||
ServiceConfiguration.configurations.insert({service: serviceName});
|
||||
|
||||
try {
|
||||
// register a fake login service
|
||||
Oauth.registerService(serviceName, 2, null, function (query) {
|
||||
return {
|
||||
serviceData: {id: foobookId},
|
||||
options: {option1: foobookOption1}
|
||||
};
|
||||
});
|
||||
|
||||
// simulate logging in using foobook
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/" + serviceName + "?close",
|
||||
query: {state: credentialToken}};
|
||||
Oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// Test that the login result for that user is prepared
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceName, serviceName);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].serviceData.id, foobookId);
|
||||
test.equal(
|
||||
Oauth._loginResultForCredentialToken[credentialToken].options.option1, foobookOption1);
|
||||
|
||||
} finally {
|
||||
Oauth._unregisterService(serviceName);
|
||||
}
|
||||
});
|
||||
@@ -4,17 +4,15 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('accounts-oauth-helper', ['client', 'server']);
|
||||
api.use('accounts-base', ['client', 'server']);
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
api.use('oauth', 'client');
|
||||
|
||||
api.add_files('oauth2_common.js', ['client', 'server']);
|
||||
api.add_files('oauth2_server.js', 'server');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('random');
|
||||
api.use('accounts-base');
|
||||
api.use('accounts-oauth2-helper', 'server');
|
||||
api.use('service-configuration', 'server');
|
||||
api.use('oauth2', 'server');
|
||||
api.add_files("oauth2_tests.js", 'server');
|
||||
});
|
||||
@@ -1,3 +1,9 @@
|
||||
// This file is used to set up aliases and methods to preserve backwards
|
||||
// compatibility on some deprecated methods. Care should be taken when
|
||||
// adding aliases and methods that the target will not be undefined, as
|
||||
// the past package is loaded early. In some cases, it may be best to
|
||||
// define the alias in the package it refers to.
|
||||
|
||||
// Old under_score version of camelCase public API names.
|
||||
Meteor.is_client = Meteor.isClient;
|
||||
Meteor.is_server = Meteor.isServer;
|
||||
|
||||
@@ -7,3 +7,8 @@ Package.on_use(function (api, where) {
|
||||
where = where || ['client', 'server'];
|
||||
api.add_files('random.js', where);
|
||||
});
|
||||
|
||||
Package.on_test(function(api) {
|
||||
api.use('random');
|
||||
api.add_files('random_tests.js', ['client', 'server']);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// @export Random
|
||||
Random = {};
|
||||
|
||||
// see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript
|
||||
// for a full discussion and Alea implementation.
|
||||
Random._Alea = function () {
|
||||
var Alea = function () {
|
||||
function Mash() {
|
||||
var n = 0xefc8249d;
|
||||
|
||||
@@ -76,6 +73,53 @@ Random._Alea = function () {
|
||||
} (Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
|
||||
var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
var create = function (/* arguments */) {
|
||||
|
||||
var random = Alea.apply(null, arguments);
|
||||
|
||||
var self = {};
|
||||
|
||||
var bind = function (fn) {
|
||||
return _.bind(fn, self);
|
||||
};
|
||||
|
||||
return _.extend(self, {
|
||||
_Alea: Alea,
|
||||
|
||||
create: create,
|
||||
|
||||
fraction: random,
|
||||
|
||||
choice: bind(function (arrayOrString) {
|
||||
var index = Math.floor(this.fraction() * arrayOrString.length);
|
||||
if (typeof arrayOrString === "string")
|
||||
return arrayOrString.substr(index, 1);
|
||||
else
|
||||
return arrayOrString[index];
|
||||
}),
|
||||
|
||||
id: bind(function() {
|
||||
var digits = [];
|
||||
// Length of 17 preserves around 96 bits of entropy, which is the
|
||||
// amount of state in our PRNG
|
||||
for (var i = 0; i < 17; i++) {
|
||||
digits[i] = this.choice(UNMISTAKABLE_CHARS);
|
||||
}
|
||||
return digits.join("");
|
||||
}),
|
||||
|
||||
hexString: bind(function (digits) {
|
||||
var hexDigits = [];
|
||||
for (var i = 0; i < digits; ++i) {
|
||||
hexDigits.push(this.choice("0123456789abcdef"));
|
||||
}
|
||||
return hexDigits.join('');
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// instantiate RNG. Heuristically collect entropy from various sources
|
||||
|
||||
// client sources
|
||||
@@ -105,33 +149,7 @@ var pid = (typeof process !== 'undefined' && process.pid) || 1;
|
||||
// XXX On the server, use the crypto module (OpenSSL) instead of this PRNG.
|
||||
// (Make Random.fraction be generated from Random.hexString instead of the
|
||||
// other way around, and generate Random.hexString from crypto.randomBytes.)
|
||||
Random.fraction = new Random._Alea([
|
||||
new Date(), height, width, agent, pid, Math.random()]);
|
||||
|
||||
Random.choice = function (arrayOrString) {
|
||||
var index = Math.floor(Random.fraction() * arrayOrString.length);
|
||||
if (typeof arrayOrString === "string")
|
||||
return arrayOrString.substr(index, 1);
|
||||
else
|
||||
return arrayOrString[index];
|
||||
};
|
||||
|
||||
var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
Random.id = function() {
|
||||
var digits = [];
|
||||
// Length of 17 preserves around 96 bits of entropy, which is the
|
||||
// amount of state in our PRNG
|
||||
for (var i = 0; i < 17; i++) {
|
||||
digits[i] = Random.choice(UNMISTAKABLE_CHARS);
|
||||
}
|
||||
return digits.join("");
|
||||
};
|
||||
|
||||
var HEX_DIGITS = "0123456789abcdef";
|
||||
Random.hexString = function (digits) {
|
||||
var hexDigits = [];
|
||||
for (var i = 0; i < digits; ++i) {
|
||||
hexDigits.push(Random.choice("0123456789abcdef"));
|
||||
}
|
||||
return hexDigits.join('');
|
||||
};
|
||||
// @export Random
|
||||
Random = create([
|
||||
new Date(), height, width, agent, pid, Math.random()
|
||||
]);
|
||||
|
||||
15
packages/random/random_tests.js
Normal file
15
packages/random/random_tests.js
Normal file
@@ -0,0 +1,15 @@
|
||||
Tinytest.add('random', function (test) {
|
||||
// Deterministic with a specified seed, which should generate the
|
||||
// same sequence in all environments.
|
||||
//
|
||||
// For repeatable unit test failures using deterministic random
|
||||
// number sequences it's fine if a new Meteor release changes the
|
||||
// algorithm being used and it starts generating a different
|
||||
// sequence for a seed, as long as the sequence is consistent for
|
||||
// a particular release.
|
||||
var random = Random.create(0);
|
||||
test.equal(random.id(), "cp9hWvhg8GSvuZ9os");
|
||||
test.equal(random.id(), "3f3k6Xo7rrHCifQhR");
|
||||
test.equal(random.id(), "shxDnjWWmnKPEoLhM");
|
||||
test.equal(random.id(), "6QTjB8C5SEqhmz4ni");
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user