Merge branch 'devel' into accounts-ui-css

This commit is contained in:
David Greenspan
2012-10-10 22:20:27 -07:00
29 changed files with 607 additions and 110 deletions

View File

@@ -1,6 +1,49 @@
## vNEXT
* This release introduces Meteor Accounts, a full-featured auth system that supports
- fine-grained user-based control over database reads and writes
- federated login with Facebook, GitHub, Google, Twitter, and Weibo
- secure password login
- email validation and password recovery
- an optional set of UI widgets implementing standard login/signup/password
change/logout flows
When you upgrade to Meteor 0.5.0, existing apps will lose the ability to write
to the database from the client. To restore this, either:
- configure each of your collections with
[`collection.allow`](http://docs.meteor.com/#allow) and
[`collection.deny`](http://docs.meteor.com/#deny) calls to specify which
users can perform which write operations, or
- add the `insecure` smart package (which is included in new apps by default)
to restore the old behavior where anyone can write to any collection which
has not been configured with `allow` or `deny`
For more information on Meteor Accounts, see http://docs.meteor.com/#accounts
* Arrays and objects can now be stored in the `Session`; mutating the value you
retrieve with `Session.get` does not affect the value in the session.
* On the client, `Meteor.apply` takes a new `wait` option, which ensures that no
further method calls are sent to the server until this method is finished; it
is used for login and logout methods in order to keep the user ID
well-defined. You can also specifiy an `onReconnect` handler which is run when
re-establishing a connection; Meteor Accounts uses this to log back in on
reconnect.
* Meteor now provides a compatible replacement for the DOM `localStorage`
facility that works in IE7, in the `localstorage-polyfill` smart package.
* `Meteor.Collection` now takes its optional `manager` argument (used to
associate a collection with a server you've connected to with
`Meteor.connect`) as a named option. (The old call syntax continues to work
for now.)
* Fix a bug where trying to immediately resubscribe to a record set after
unsubscribing could fail silently.
* Better error handling for failed Mongo writes from inside methods; previously,
errors here could cause clients to stop processing data from the server.
## v0.4.2

View File

@@ -0,0 +1 @@
local

View File

@@ -0,0 +1,15 @@
# 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.
autopublish
insecure
preserve-inputs
accounts-ui
less
accounts-google
accounts-github
accounts-password
underscore
accounts-facebook

View File

@@ -0,0 +1,112 @@
<head>
<title>accounts-ui-viewer</title>
</head>
<body>
{{> page}}
</body>
<template name="radio">
<span class="radio"><input id="{{key}}:{{value}}" {{maybeChecked}} type="radio" name="{{key}}" value="{{value}}" />{{! no whitespace}}<label for="{{key}}:{{value}}">{{label}}</label></span>
</template>
<template name="button">
<button>{{label}}</button>
</template>
<template name="page">
<div id="controlpane">
<div class="group">
<h3>Dropdown opens:</h3>
{{radio "openLeft" "false" "Right"}}
{{radio "openLeft" "true" "Left"}}
</div>
<div class="group">
<h3>Positioning:</h3>
{{radio "positioning" "relative" "Relative"}}
{{radio "positioning" "absolute" "Absolute"}}
{{radio "positioning" "floatRight" "Float:right"}}
{{radio "positioning" "inline" "Inline"}}
</div>
<div class="group">
<h3>How many third-party services?</h3>
{{radio "numServices" "0" "0"}}
{{radio "numServices" "1" "1"}}
{{radio "numServices" "2" "2"}}
{{radio "numServices" "3" "3"}}
</div>
<div class="group">
<h3>Has password accounts?</h3>
{{radio "hasPasswords" "false" "No"}}
{{radio "hasPasswords" "true" "Yes"}}
</div>
<div class="group">
<h3>Password sign-up fields:</h3>
{{radio "signupFields" "EMAIL_ONLY" "Email"}}
{{radio "signupFields" "USERNAME_ONLY" "Username"}}
{{radio "signupFields" "USERNAME_AND_EMAIL" "Username & Email"}}
{{radio "signupFields" "USERNAME_AND_OPTIONAL_EMAIL" "Username & Optional Email"}}
</div>
<div class="group">
<h3>Fake-Configure:</h3>
{{button "fakeConfig" "facebook" "Facebook"}}
{{button "fakeConfig" "github" "GitHub"}}
{{button "fakeConfig" "google" "Google"}}
</div>
<div class="group">
<h3>Show Configure Dialog:</h3>
{{button "showConfig" "facebook" "Facebook"}}
{{button "showConfig" "github" "GitHub"}}
{{button "showConfig" "google" "Google"}}
</div>
<div class="group">
<h3>Unconfigure:</h3>
{{button "unconfig" "facebook" "Facebook"}}
{{button "unconfig" "github" "GitHub"}}
{{button "unconfig" "google" "Google"}}
</div>
<div class="group">
<h3>Messages:</h3>
{{button "messages" "error" "Error"}}
{{button "messages" "info" "Info"}}
{{button "messages" "clear" "Clear"}}
</div>
<div class="group">
<h3>Signing in/out</h3>
{{button "sign" "in" "Fake sign-in"}}
{{button "sign" "out" "Sign out"}}
</div>
<div class="group">
<h3>Logged-out Views</h3>
{{button "lov" "signIn" "Sign In"}}
{{button "lov" "createAccount" "Create Account"}}
{{button "lov" "forgotPassword" "Forgot Password"}}
</div>
<div class="group">
<h3>Logged-in Views</h3>
{{button "liv" "accountButtons" "Account Buttons"}}
{{button "liv" "changePassword" "Change Password"}}
{{button "liv" "messageOnly" "Message Only"}}
</div>
<div class="group">
<h3>Other Modals</h3>
{{button "modals" "resetPassword" "Reset Password"}}
{{button "modals" "enrollAccount" "Enroll Account"}}
{{button "modals" "justVerifiedEmail" "Verified Email"}}
</div>
</div>
<div id="previewpane" class="{{settingsClass}}">
{{#with settings}}
<div id="preview-wrapper" class="{{outerClass}}">
{{#if match "positioning:inline"}}
Here is a place to sign in, yay!
{{/if}}
{{> loginButtons}}
{{#if match "positioning:inline"}}
Isn't that great?
{{/if}}
</div>
{{/with}}
<div id="pos-indicator"></div>
</div>
</template>

View File

@@ -0,0 +1,215 @@
Meteor.users.allow({update: function () { return true; }});
if (Meteor.isClient) {
Accounts.STASH = _.extend({}, Accounts);
var handleSetting = function (key, value) {
if (key === "numServices") {
_.each(['facebook', 'github', 'google'],
function (serv, i) {
if (i < value)
Accounts[serv] = Accounts.STASH[serv];
else
Accounts[serv] = null;
});
} else if (key === "hasPasswords") {
Accounts.password = value && Accounts.STASH.password || null;
var user = Meteor.user();
if (user) {
if (! value) {
// make sure we have no username if "app" has no passwords
Meteor.users.update(Meteor.userId(),
{ $unset: { username: 1 }});
} else {
// make sure we have a username
Meteor.users.update(Meteor.userId(),
{ $set: { username: Meteor.uuid() }});
}
}
} else if (key === "signupFields") {
Accounts.ui._options.passwordSignupFields = value;
}
};
if (! Session.get('settings'))
Session.set('settings', {
openLeft: false,
positioning: "relative",
numServices: 3,
hasPasswords: true,
signupFields: 'EMAIL_ONLY'
});
else
_.each(Session.get('settings'), function (v,k) {
handleSetting(k, v);
});
Template.page.settings = function () {
return Session.get('settings');
};
Template.page.settingsClass = function () {
var settings = Session.get('settings');
var classes = [];
if (settings.positioning)
classes.push('positioning-' + settings.positioning.toLowerCase());
return classes.join(' ');
};
Template.page.outerClass = function () {
var settings = Session.get('settings');
var classes = [];
if (settings.openLeft)
classes.push('login-buttons-dropdown-hangs-left');
return classes.join(' ');
};
var keyValueFromId = function (id) {
var match;
if (id && (match = /^(.*?):(.*)$/.exec(id))) {
var key = match[1];
var value = castValue(match[2]);
return [key, value];
}
return null;
};
var castValue = function (value) {
if (value === "false")
value = false;
else if (value === "true")
value = true;
else if (/^[0-9]+$/.test(value))
value = Number(value);
return value;
};
Template.radio.maybeChecked = function () {
var curValue = Session.get('settings')[this.key];
if (castValue(this.value) === curValue)
return 'checked="checked"';
return '';
};
Template.page.radio = function (key, value, label) {
return new Handlebars.SafeString(
Template.radio({key: key, value: value, label: label}));
};
Template.page.button = function (key, value, label) {
return new Handlebars.SafeString(
Template.button({key: key, value: value, label: label}));
};
Template.page.match = function (kv) {
kv = keyValueFromId(kv);
if (! kv)
return false;
return Session.get('settings')[kv[0]] === kv[1];
};
var fakeLogin = function () {
Accounts.createUser(
{username: Meteor.uuid(),
password: "password",
profile: { name: "Joe Schmoe" }},
function () {
var user = Meteor.user();
if (! user)
return;
// delete our username if we are in a mode
// where there aren't usernames/emails/passwords
// (only third-party auth) so that there is no
// "Change Password" button when signed in
if (! Session.get('settings').hasPasswords)
Meteor.users.update(Meteor.userId(),
{ $unset: { username: 1 }});
});
};
var exitFlows = function () {
Accounts._loginButtonsSession.set('inSignupFlow', false);
Accounts._loginButtonsSession.set('inForgotPasswordFlow', false);
Accounts._loginButtonsSession.set('inChangePasswordFlow', false);
Accounts._loginButtonsSession.set('inMessageOnlyFlow', false);
};
Template.page.events({
'change #controlpane input[type=radio]': function (event) {
var input = event.currentTarget;
var keyValue;
if (input && input.id && (keyValue = keyValueFromId(input.id))) {
var key = keyValue[0];
var value = keyValue[1];
if (value === "false")
value = false;
else if (value === "true")
value = true;
var settings = Session.get('settings');
settings[key] = value;
Session.set('settings', settings);
handleSetting(key, value);
}
},
'click #controlpane button': function (event) {
if (this.key === "fakeConfig") {
var service = this.value;
if (! Accounts.loginServiceConfiguration.findOne({service: service}))
Accounts.loginServiceConfiguration.insert(
{service: service, fake: true});
} else if (this.key === "unconfig") {
var service = this.value;
Accounts.loginServiceConfiguration.remove({service: service});
} else if (this.key === "messages") {
if (this.value === "error") {
Accounts._loginButtonsSession.set('errorMessage', 'An error occurred! Gee golly gosh.');
} else if (this.value === "info") {
Accounts._loginButtonsSession.set('infoMessage', 'Here is some information that is crucial.');
} else if (this.value === "clear") {
Accounts._loginButtonsSession.resetMessages();
}
} else if (this.key === "sign") {
if (this.value === 'in') {
// create a random new user
Accounts._loginButtonsSession.closeDropdown();
fakeLogin();
} else if (this.value === 'out') {
Meteor.logout();
}
} else if (this.key === "showConfig") {
Accounts._loginButtonsSession.configureService(this.value);
} else if (this.key === "lov") {
exitFlows();
Accounts._loginButtonsSession.set("dropdownVisible", true);
if (Meteor.userId())
Meteor.logout();
if (this.value === "createAccount")
Accounts._loginButtonsSession.set("inSignupFlow", true);
else if (this.value === "forgotPassword")
Accounts._loginButtonsSession.set("inForgotPasswordFlow", true);
} else if (this.key === "liv") {
exitFlows();
Accounts._loginButtonsSession.set("dropdownVisible", true);
if (! Meteor.userId())
fakeLogin();
if (this.value === "changePassword")
Accounts._loginButtonsSession.set("inChangePasswordFlow", true);
else if (this.value === "messageOnly")
Accounts._loginButtonsSession.set("inMessageOnlyFlow", true);
} else if (this.key === "modals") {
var value = this.value;
_.each([
'resetPasswordToken',
'enrollAccountToken',
'justVerifiedEmail'], function (k) {
Accounts._loginButtonsSession.set(
k, k.indexOf(value) >= 0 ? 'foo' : null);
});
}
}
});
}

View File

@@ -0,0 +1,110 @@
* { padding: 0; margin: 0; }
html, body { height: 100%; }
#controlpane {
position: absolute;
left: 0;
width: 299px;
top: 0;
bottom: 0;
background: #eee;
border-right: 1px solid #999;
overflow: auto;
h3 {
border-top: 1px solid #999;
font-size: 85%;
margin-bottom: 5px;
}
.group {
margin: 10px;
}
input[type=radio] {
margin-left: 5px;
vertical-align: middle;
}
label {
padding-left: 3px;
}
}
#previewpane {
position: absolute;
left: 300px;
right: 0;
top: 0;
bottom: 0;
#preview-wrapper {
margin: 20px;
}
}
.radio {
white-space: nowrap;
}
.positioning-floatright {
#login-buttons {
float: right;
margin-right: 180px;
}
#pos-indicator {
display: block;
top: 0;
right: 0;
width: 200px;
height: 20px;
}
}
.positioning-relative {
#login-buttons {
position: relative;
left: 120px;
top: 20px;
}
#pos-indicator {
display: block;
top: 0;
left: 0;
width: 140px;
height: 40px;
}
}
.positioning-absolute {
#login-buttons {
position: absolute;
left: 140px;
top: 40px;
}
#pos-indicator {
display: block;
top: 0;
left: 0;
width: 140px;
height: 40px;
}
}
#pos-indicator {
position: absolute;
background: #eec;
display: none;
}
a { color: blue; }
button { padding: 4px;
margin-bottom: 4px; // for when buttons wrap
}

View File

@@ -17,9 +17,13 @@
var userId = Meteor.userId();
if (!userId)
return null;
if (Meteor.userLoaded())
return Meteor.users.findOne(userId);
// Not yet loaded: return a minimal object.
if (Meteor.userLoaded()) {
var user = Meteor.users.findOne(userId);
if (user) return user;
}
// Either the subscription isn't done yet, or for some reason this user has
// no published fields (and thus is considered to not exist in
// minimongo). Return a minimal object.
return {_id: userId};
};

View File

@@ -115,21 +115,13 @@
};
// XXX see comment on Accounts.createUser in passwords_server about adding a
// third "server options" argument.
var defaultCreateUserHook = function (options, extra, user) {
// This hook gets 'extra' directly from the createUser method, so make sure
// we don't allow users to set any fields at creation time that they won't
// later be able to set according to the default Meteor.users.allow. Set
// your own onCreateUser if you want users to be able to specify other
// fields at creation time.
if (_.any(extra, function(value, key) {return key != 'profile';})) {
console.log(JSON.stringify(extra));
throw new Meteor.Error(400, "Disallowed fields in extra");
}
return _.extend(user, extra);
// second "server options" argument.
var defaultCreateUserHook = function (options, user) {
if (options.profile)
user.profile = options.profile;
return user;
};
Accounts.insertUserDoc = function (options, extra, user) {
Accounts.insertUserDoc = function (options, user) {
// add created at timestamp (and protect passed in user object from
// modification)
user = _.extend({createdAt: +(new Date)}, user);
@@ -137,15 +129,15 @@
var fullUser;
if (onCreateUserHook) {
fullUser = onCreateUserHook(options, extra, user);
fullUser = onCreateUserHook(options, user);
// This is *not* part of the API. We need this because we can't isolate
// the global server environment between tests, meaning we can't test
// both having a create user hook set and not having one set.
if (fullUser === 'TEST DEFAULT HOOK')
fullUser = defaultCreateUserHook(options, extra, user);
fullUser = defaultCreateUserHook(options, user);
} else {
fullUser = defaultCreateUserHook(options, extra, user);
fullUser = defaultCreateUserHook(options, user);
}
_.each(validateNewUserHooks, function (hook) {
@@ -199,13 +191,13 @@
// @param serviceData {Object} Data to store in the user's record
// under services[serviceName]. Must include an "id" field
// which is a unique identifier for the user in the service.
// @param extra {Object, optional} Any additional fields to place on the user
// object
// @param options {Object, optional} Other options to pass to insertUserDoc
// (eg, profile)
// @returns {Object} Object with token and id keys, like the result
// of the "login" method.
Accounts.updateOrCreateUserFromExternalService = function(
serviceName, serviceData, extra) {
extra = extra || {};
serviceName, serviceData, options) {
options = _.clone(options || {});
if (serviceName === "password" || serviceName === "resume")
throw new Error(
@@ -221,26 +213,28 @@
var user = Meteor.users.findOne(selector);
if (user) {
// don't overwrite existing fields
// XXX subobjects (aka 'profile', 'services')?
var newKeys = _.difference(_.keys(extra), _.keys(user));
var newAttrs = _.pick(extra, newKeys);
// We *don't* process options (eg, profile) for update, but we do replace
// the serviceData (eg, so that we keep an unexpired access token and
// don't cache old email addresses in serviceData.email).
// XXX provide an onUpdateUser hook which would let apps update
// the profile too
var stampedToken = Accounts._generateStampedLoginToken();
var result = {token: stampedToken.token};
var setAttrs = {};
setAttrs["services." + serviceName] = serviceData;
// XXX Maybe we should re-use the selector above and notice if the update
// touches nothing?
Meteor.users.update(
user._id,
{$set: newAttrs, $push: {'services.resume.loginTokens': stampedToken}});
result.id = user._id;
return result;
{$set: setAttrs,
$push: {'services.resume.loginTokens': stampedToken}});
return {token: stampedToken.token, id: user._id};
} else {
// Create a new user.
var servicesClause = {};
servicesClause[serviceName] = serviceData;
var insertOptions = {services: servicesClause, generateLoginToken: true};
// Build a user doc; clone to make sure sure mutating
// insertOptions.services doesn't affect user.services or vice versa.
user = {services: JSON.parse(JSON.stringify(servicesClause))};
return Accounts.insertUserDoc(insertOptions, extra, user);
// Create a new user with the service data. Pass other options through to
// insertUserDoc.
user = {services: {}};
user.services[serviceName] = serviceData;
options.generateLoginToken = true;
return Accounts.insertUserDoc(options, user);
}
};

View File

@@ -6,20 +6,24 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService', function (test)
// create an account with facebook
var uid1 = Accounts.updateOrCreateUserFromExternalService(
'facebook', {id: facebookId}, {profile: {foo: 1}}).id;
test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1);
test.equal(Meteor.users.findOne({"services.facebook.id": facebookId}).profile.foo, 1);
'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id;
var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users, 1);
test.equal(users[0].profile.foo, 1);
test.equal(users[0].services.facebook.monkey, 42);
// create again with the same id, see that we get the same user. profile
// doesn't get overwritten in this implementation (though we should do
// something better with merging later).
// create again with the same id, see that we get the same user.
// it should update services.facebook but not profile.
var uid2 = Accounts.updateOrCreateUserFromExternalService(
'facebook', {id: facebookId}, {profile: {foo: 1000, bar: 2}}).id;
'facebook', {id: facebookId, llama: 50},
{profile: {foo: 1000, bar: 2}}).id;
test.equal(uid1, uid2);
test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1);
test.equal(Meteor.users.findOne(uid1).profile.foo, 1);
test.equal(Meteor.users.findOne(uid1).profile.bar, undefined);
users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
test.length(users, 1);
test.equal(users[0].profile.foo, 1);
test.equal(users[0].profile.bar, undefined);
test.equal(users[0].services.facebook.llama, 50);
test.equal(users[0].services.facebook.monkey, undefined);
// cleanup
Meteor.users.remove(uid1);
@@ -48,7 +52,6 @@ Tinytest.add('accounts - insertUserDoc username', function (test) {
// user does not already exist. create a user object with fields set.
var result = Accounts.insertUserDoc(
userIn,
{profile: {name: 'Foo Bar'}},
userIn
);
@@ -61,7 +64,6 @@ Tinytest.add('accounts - insertUserDoc username', function (test) {
// run the hook again. now the user exists, so it throws an error.
test.throws(function () {
Accounts.insertUserDoc(
userIn,
{profile: {name: 'Foo Bar'}},
userIn
);
@@ -83,7 +85,6 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
// user does not already exist. create a user object with fields set.
var result = Accounts.insertUserDoc(
userIn,
{profile: {name: 'Foo Bar'}},
userIn
);
@@ -97,7 +98,6 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
// run the hook again. now the user exists, so it throws an error.
test.throws(function () {
Accounts.insertUserDoc(
userIn,
{profile: {name: 'Foo Bar'}},
userIn
);
@@ -106,20 +106,20 @@ Tinytest.add('accounts - insertUserDoc email', function (test) {
// now with only one of them.
test.throws(function () {
Accounts.insertUserDoc(
{}, {}, {emails: [{address: email1}]}
{}, {emails: [{address: email1}]}
);
});
test.throws(function () {
Accounts.insertUserDoc(
{}, {}, {emails: [{address: email2}]}
{}, {emails: [{address: email2}]}
);
});
// a third email works.
var result3 = Accounts.insertUserDoc(
{}, {}, {emails: [{address: email3}]}
{}, {emails: [{address: email3}]}
);
var user3 = Meteor.users.findOne(result3.id);
test.equal(typeof user3.createdAt, 'number');

View File

@@ -11,7 +11,7 @@
accessToken: accessToken,
email: identity.email
},
extra: {profile: {name: identity.name}}
options: {profile: {name: identity.name}}
};
});

View File

@@ -11,7 +11,7 @@
email: identity.email,
username: identity.login
},
extra: {profile: {name: identity.name}}
options: {profile: {name: identity.name}}
};
});

View File

@@ -11,7 +11,7 @@
accessToken: accessToken,
email: identity.email
},
extra: {profile: {name: identity.name}}
options: {profile: {name: identity.name}}
};
});

View File

@@ -14,7 +14,7 @@
// - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider
// - (For OAuth2 only) query {Object} parameters passed in query string
// - return value is:
// - {serviceData, (optional extra)} where serviceData should end
// - {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) {

View File

@@ -55,7 +55,7 @@
// Get or create user doc and login token for reconnect.
Accounts.oauth._loginResultForState[query.state] =
Accounts.updateOrCreateUserFromExternalService(
service.serviceName, oauthResult.serviceData, oauthResult.extra);
service.serviceName, oauthResult.serviceData, oauthResult.options);
}
}

View File

@@ -92,7 +92,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) {
accessToken: twitterfailAccessToken,
accessTokenSecret: twitterfailAccessTokenSecret
},
extra: {
options: {
profile: {invalid: true}
}
};

View File

@@ -14,7 +14,7 @@
// Get or create user doc and login token for reconnect.
Accounts.oauth._loginResultForState[query.state] =
Accounts.updateOrCreateUserFromExternalService(
service.serviceName, oauthResult.serviceData, oauthResult.extra);
service.serviceName, oauthResult.serviceData, oauthResult.options);
}
// Either close the window, redirect, or render nothing

View File

@@ -55,7 +55,7 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
serviceData: {
id: failbookId
},
extra: {
options: {
profile: {invalid: true}
}
};

View File

@@ -17,9 +17,9 @@
function (test, expect) {
email1 = Meteor.uuid() + "-intercept@example.com";
Accounts.createUser({email: email1, password: 'foobar'},
expect(function (error) {
test.equal(error, undefined);
}));
expect(function (error) {
test.equal(error, undefined);
}));
},
function (test, expect) {
Accounts.forgotPassword({email: email1}, expect(function (error) {

View File

@@ -1,12 +1,7 @@
(function () {
Accounts.createUser = function (options, extra, callback) {
Accounts.createUser = function (options, callback) {
options = _.clone(options); // we'll be modifying options
if (typeof extra === "function") {
callback = extra;
extra = {};
}
if (!options.password)
throw new Error("Must set options.password");
var verifier = Meteor._srp.generateVerifier(options.password);
@@ -14,7 +9,7 @@
delete options.password;
options.srp = verifier;
Meteor.apply('createUser', [options, extra], {wait: true},
Meteor.apply('createUser', [options], {wait: true},
function (error, result) {
if (error || !result) {
error = error || new Error("No result");

View File

@@ -355,8 +355,7 @@
//
// returns an object with id: userId, and (if options.generateLoginToken is
// set) token: loginToken.
var createUser = function (options, extra) {
extra = extra || {};
var createUser = function (options) {
var username = options.username;
var email = options.email;
if (!username && !email)
@@ -379,19 +378,19 @@
if (email)
user.emails = [{address: email, verified: false}];
return Accounts.insertUserDoc(options, extra, user);
return Accounts.insertUserDoc(options, user);
};
// method for create user. Requests come from the client.
Meteor.methods({
createUser: function (options, extra) {
createUser: function (options) {
options = _.clone(options);
options.generateLoginToken = true;
if (Accounts._options.forbidClientAccountCreation)
throw new Meteor.Error(403, "Signups forbidden");
// Create user. result contains id and token.
var result = createUser(options, extra);
var result = createUser(options);
// safety belt. createUser is supposed to throw on error. send 500 error
// instead of sending a verification email with empty userid.
if (!result.id)
@@ -420,20 +419,16 @@
// which is always empty when called from the createUser method? eg, "admin:
// true", which we want to prevent the client from setting, but which a custom
// method calling Accounts.createUser could set?
Accounts.createUser = function (options, extra, callback) {
Accounts.createUser = function (options, callback) {
options = _.clone(options);
options.generateLoginToken = false;
if (typeof extra === "function") {
callback = extra;
extra = {};
}
// XXX allow an optional callback?
if (callback) {
throw new Error("Meteor.createUser with callback not supported on the server yet.");
}
var userId = createUser(options, extra).id;
var userId = createUser(options).id;
return userId;
};

View File

@@ -199,9 +199,9 @@ if (Meteor.isClient) (function () {
logoutStep,
// test Accounts.validateNewUser
function(test, expect) {
Accounts.createUser({username: username3, password: password3},
// should fail the new user validators
{profile: {invalid: true}},
Accounts.createUser({username: username3, password: password3,
// should fail the new user validators
profile: {invalid: true}},
expect(function (error) {
test.equal(error.error, 403);
test.equal(
@@ -211,10 +211,10 @@ if (Meteor.isClient) (function () {
},
logoutStep,
function(test, expect) {
Accounts.createUser({username: username3, password: password3},
Accounts.createUser({username: username3, password: password3,
// should fail the new user validator with a special
// exception
{profile: {invalidAndThrowException: true}},
profile: {invalidAndThrowException: true}},
expect(function (error) {
test.equal(
error.reason,
@@ -224,14 +224,13 @@ if (Meteor.isClient) (function () {
// test Accounts.onCreateUser
function(test, expect) {
Accounts.createUser(
{username: username3, password: password3},
{testOnCreateUserHook: true},
{username: username3, password: password3,
testOnCreateUserHook: true},
loggedInAs(username3, test, expect));
},
function(test, expect) {
test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
},
// test Meteor.user(). This test properly belongs in
// accounts-base/accounts_tests.js, but this is where the tests that
// actually log in are.
@@ -243,6 +242,14 @@ if (Meteor.isClient) (function () {
test.equal(err, undefined);
}));
},
function(test, expect) {
Meteor.call('clearUsernameAndProfile');
Meteor.default_connection.onQuiesce(expect(function() {
test.isTrue(Meteor.userId());
var user = Meteor.user();
test.equal(user, {_id: Meteor.userId()});
}));
},
logoutStep,
function(test, expect) {
var clientUser = Meteor.user();
@@ -275,14 +282,14 @@ if (Meteor.isServer) (function () {
var email = Meteor.uuid() + '@example.com';
test.throws(function () {
// should fail the new user validators
Accounts.createUser({email: email}, {profile: {invalid: true}});
Accounts.createUser({email: email, profile: {invalid: true}});
});
// disable sending emails
var oldEmailSend = Email.send;
Email.send = function() {};
var userId = Accounts.createUser({email: email},
{testOnCreateUserHook: true});
var userId = Accounts.createUser({email: email,
testOnCreateUserHook: true});
Email.send = oldEmailSend;
test.isTrue(userId);
@@ -296,7 +303,7 @@ if (Meteor.isServer) (function () {
function (test) {
var username = Meteor.uuid();
var userId = Accounts.createUser({username: username}, {});
var userId = Accounts.createUser({username: username});
var user = Meteor.users.findOne(userId);
// no services yet.

View File

@@ -4,9 +4,9 @@ Accounts.validateNewUser(function (user) {
return !(user.profile && user.profile.invalid);
});
Accounts.onCreateUser(function (options, extra, user) {
if (extra.testOnCreateUserHook) {
user.profile = (user.profile || {});
Accounts.onCreateUser(function (options, user) {
if (options.testOnCreateUserHook) {
user.profile = user.profile || {};
user.profile.touchedByOnCreateUser = true;
return user;
} else {
@@ -35,5 +35,11 @@ Accounts.config({
// This test properly belongs in accounts-base/accounts_tests.js, but
// this is where the tests that actually log in are.
Meteor.methods({
testMeteorUser: function () { return Meteor.user(); }
testMeteorUser: function () { return Meteor.user(); },
clearUsernameAndProfile: function () {
if (!this.userId)
throw new Error("Not logged in!");
Meteor.users.update(this.userId,
{$unset: {profile: 1, username: 1}});
}
});

View File

@@ -10,7 +10,7 @@
accessToken: oauthBinding.accessToken,
accessTokenSecret: oauthBinding.accessTokenSecret
},
extra: {
options: {
profile: {
name: identity.name
}

View File

@@ -11,7 +11,7 @@
accessToken: accessToken.access_token,
screenName: identity.screen_name
},
extra: {profile: {name: identity.screen_name}}
options: {profile: {name: identity.screen_name}}
};
});

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Cross browser API for Persistant Storage, PubSub and Request."
summary: "API for Persistant Storage, PubSub and Request"
});
Package.on_use(function (api) {

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Automatically publish all data in the database to every client"
summary: "Automatically publish the entire database to all clients"
});
Package.on_use(function (api, where) {

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Require this application always use transport layer encryption"
summary: "Require this application to use secure transport (HTTPS)"
});
Package.on_use(function (api) {

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Automatically preserve all form fields that have a unique id"
summary: "Automatically preserve all form fields with a unique id"
});
Package.on_use(function (api, where) {

View File

@@ -1,5 +1,5 @@
Package.describe({
summary: "Collection of small helper functions (map, each, bind, ...)"
summary: "Collection of small helper functions: _.map, _.each, ..."
});
Package.on_use(function (api, where) {