mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' into accounts-ui-css
This commit is contained in:
43
History.md
43
History.md
@@ -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
|
||||
|
||||
|
||||
1
examples/unfinished/accounts-ui-viewer/.meteor/.gitignore
vendored
Normal file
1
examples/unfinished/accounts-ui-viewer/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
15
examples/unfinished/accounts-ui-viewer/.meteor/packages
Normal file
15
examples/unfinished/accounts-ui-viewer/.meteor/packages
Normal 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
|
||||
112
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html
Normal file
112
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html
Normal 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>
|
||||
215
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js
Normal file
215
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
110
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less
Normal file
110
examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less
Normal 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
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
accessToken: accessToken,
|
||||
email: identity.email
|
||||
},
|
||||
extra: {profile: {name: identity.name}}
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
email: identity.email,
|
||||
username: identity.login
|
||||
},
|
||||
extra: {profile: {name: identity.name}}
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
accessToken: accessToken,
|
||||
email: identity.email
|
||||
},
|
||||
extra: {profile: {name: identity.name}}
|
||||
options: {profile: {name: identity.name}}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
accessToken: twitterfailAccessToken,
|
||||
accessTokenSecret: twitterfailAccessTokenSecret
|
||||
},
|
||||
extra: {
|
||||
options: {
|
||||
profile: {invalid: true}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
serviceData: {
|
||||
id: failbookId
|
||||
},
|
||||
extra: {
|
||||
options: {
|
||||
profile: {invalid: true}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
accessToken: oauthBinding.accessToken,
|
||||
accessTokenSecret: oauthBinding.accessTokenSecret
|
||||
},
|
||||
extra: {
|
||||
options: {
|
||||
profile: {
|
||||
name: identity.name
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
accessToken: accessToken.access_token,
|
||||
screenName: identity.screen_name
|
||||
},
|
||||
extra: {profile: {name: identity.screen_name}}
|
||||
options: {profile: {name: identity.screen_name}}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user