mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Add case-insensitive accounts-password utility functions
Since we have added additional constraints to the database around case sensitivity, we now want to discourage people from working with the Accounts collection directly and provide an API for changing certain fields in a correct way. Methods that do database checks before and after the operation: Accounts.setUsername Accounts.addEmail Accounts.removeEmail Methods that make sure to use a case-insensitive query to retrieve the user Accounts.findUserByUsername Accounts.findUserByEmail PR #5024
This commit is contained in:
committed by
Sashko Stubailo
parent
c237c37618
commit
dac019fda8
@@ -9,10 +9,52 @@ DocsData = {
|
||||
"scope": "global",
|
||||
"summary": "The namespace for all server-side accounts-related methods."
|
||||
},
|
||||
"Accounts.addEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 847,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.addEmail",
|
||||
"memberof": "Accounts",
|
||||
"name": "addEmail",
|
||||
"options": [],
|
||||
"params": [
|
||||
{
|
||||
"description": "<p>The ID of the user to update.</p>",
|
||||
"name": "userId",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "<p>A new email address for the user.</p>",
|
||||
"name": "newEmail",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "<p>Optional - whether the new email address should\nbe marked as verified. Defaults to false.</p>",
|
||||
"name": "verified",
|
||||
"optional": true,
|
||||
"type": {
|
||||
"names": [
|
||||
"Boolean"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "static",
|
||||
"summary": "Add an email address for a user. Use this instead of directly\nupdating the database. The operation will fail if there is a different user\nwith an email only differing in case. If the specified user has an existing\nemail only differing in case however, we replace it."
|
||||
},
|
||||
"Accounts.changePassword": {
|
||||
"filepath": "accounts-password/password_client.js",
|
||||
"kind": "function",
|
||||
"lineno": 150,
|
||||
"lineno": 148,
|
||||
"locus": "Client",
|
||||
"longname": "Accounts.changePassword",
|
||||
"memberof": "Accounts",
|
||||
@@ -131,10 +173,76 @@ DocsData = {
|
||||
"scope": "static",
|
||||
"summary": "Options to customize emails sent from the Accounts system."
|
||||
},
|
||||
"Accounts.findUserByEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 138,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.findUserByEmail",
|
||||
"memberof": "Accounts",
|
||||
"name": "findUserByEmail",
|
||||
"options": [],
|
||||
"params": [
|
||||
{
|
||||
"description": "<p>The email address to look for</p>",
|
||||
"name": "email",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": [
|
||||
{
|
||||
"description": "<p>A user if found, else null</p>",
|
||||
"type": {
|
||||
"names": [
|
||||
"Object"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "static",
|
||||
"summary": "Finds the user with the specified email.\nFirst tries to match email case sensitively; if that fails, it\ntries case insensitively; but if more than one user matches the case\ninsensitive search, it returns null."
|
||||
},
|
||||
"Accounts.findUserByUsername": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 123,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.findUserByUsername",
|
||||
"memberof": "Accounts",
|
||||
"name": "findUserByUsername",
|
||||
"options": [],
|
||||
"params": [
|
||||
{
|
||||
"description": "<p>The username to look for</p>",
|
||||
"name": "username",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns": [
|
||||
{
|
||||
"description": "<p>A user if found, else null</p>",
|
||||
"type": {
|
||||
"names": [
|
||||
"Object"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "static",
|
||||
"summary": "Finds the user with the specified username.\nFirst tries to match username case sensitively; if that fails, it\ntries case insensitively; but if more than one user matches the case\ninsensitive search, it returns null."
|
||||
},
|
||||
"Accounts.forgotPassword": {
|
||||
"filepath": "accounts-password/password_client.js",
|
||||
"kind": "function",
|
||||
"lineno": 212,
|
||||
"lineno": 210,
|
||||
"locus": "Client",
|
||||
"longname": "Accounts.forgotPassword",
|
||||
"memberof": "Accounts",
|
||||
@@ -173,10 +281,42 @@ DocsData = {
|
||||
"scope": "static",
|
||||
"summary": "Request a forgot password email."
|
||||
},
|
||||
"Accounts.removeEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 930,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.removeEmail",
|
||||
"memberof": "Accounts",
|
||||
"name": "removeEmail",
|
||||
"options": [],
|
||||
"params": [
|
||||
{
|
||||
"description": "<p>The ID of the user to update.</p>",
|
||||
"name": "userId",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "<p>The email address to remove.</p>",
|
||||
"name": "email",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "static",
|
||||
"summary": "Remove an email address for a user. Use this instead of updating\nthe database directly."
|
||||
},
|
||||
"Accounts.resetPassword": {
|
||||
"filepath": "accounts-password/password_client.js",
|
||||
"kind": "function",
|
||||
"lineno": 232,
|
||||
"lineno": 230,
|
||||
"locus": "Client",
|
||||
"longname": "Accounts.resetPassword",
|
||||
"memberof": "Accounts",
|
||||
@@ -218,7 +358,7 @@ DocsData = {
|
||||
"Accounts.sendEnrollmentEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 509,
|
||||
"lineno": 584,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.sendEnrollmentEmail",
|
||||
"memberof": "Accounts",
|
||||
@@ -251,7 +391,7 @@ DocsData = {
|
||||
"Accounts.sendResetPasswordEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 444,
|
||||
"lineno": 519,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.sendResetPasswordEmail",
|
||||
"memberof": "Accounts",
|
||||
@@ -284,7 +424,7 @@ DocsData = {
|
||||
"Accounts.sendVerificationEmail": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 648,
|
||||
"lineno": 723,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.sendVerificationEmail",
|
||||
"memberof": "Accounts",
|
||||
@@ -317,7 +457,7 @@ DocsData = {
|
||||
"Accounts.setPassword": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 396,
|
||||
"lineno": 471,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.setPassword",
|
||||
"memberof": "Accounts",
|
||||
@@ -365,6 +505,38 @@ DocsData = {
|
||||
"scope": "static",
|
||||
"summary": "Forcibly change the password for a user."
|
||||
},
|
||||
"Accounts.setUsername": {
|
||||
"filepath": "accounts-password/password_server.js",
|
||||
"kind": "function",
|
||||
"lineno": 372,
|
||||
"locus": "Server",
|
||||
"longname": "Accounts.setUsername",
|
||||
"memberof": "Accounts",
|
||||
"name": "setUsername",
|
||||
"options": [],
|
||||
"params": [
|
||||
{
|
||||
"description": "<p>The ID of the user to update.</p>",
|
||||
"name": "userId",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "<p>A new username for the user.</p>",
|
||||
"name": "newUsername",
|
||||
"type": {
|
||||
"names": [
|
||||
"String"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "static",
|
||||
"summary": "Change a user's username. Use this instead of updating the\ndatabase directly. The operation will fail if there is an existing user\nwith a username only differing in case."
|
||||
},
|
||||
"Accounts.ui": {
|
||||
"filepath": "accounts-ui-unstyled/accounts_ui.js",
|
||||
"kind": "namespace",
|
||||
@@ -437,7 +609,7 @@ DocsData = {
|
||||
"Accounts.verifyEmail": {
|
||||
"filepath": "accounts-password/password_client.js",
|
||||
"kind": "function",
|
||||
"lineno": 259,
|
||||
"lineno": 257,
|
||||
"locus": "Client",
|
||||
"longname": "Accounts.verifyEmail",
|
||||
"memberof": "Accounts",
|
||||
|
||||
@@ -31,9 +31,10 @@ id.
|
||||
|
||||
On the client, you must pass `password` and at least one of `username` or
|
||||
`email` — enough information for the user to be able to log in again
|
||||
later. If there are existing users with a username or email only differing in case, `createUser` will fail. On the server, you do not need to specify `password`, but the user will
|
||||
not be able to log in until it has a password (eg, set with
|
||||
[`Accounts.setPassword`](#accounts_setpassword)).
|
||||
later. If there are existing users with a username or email only differing in
|
||||
case, `createUser` will fail. On the server, you do not need to specify
|
||||
`password`, but the user will not be able to log in until it has a password (eg,
|
||||
set with [`Accounts.setPassword`](#accounts_setpassword)).
|
||||
|
||||
To create an account without a password on the server and still let the
|
||||
user pick their own password, call `createUser` with the `email` option
|
||||
@@ -47,6 +48,35 @@ override this behavior, use [`Accounts.onCreateUser`](#accounts_oncreateuser).
|
||||
This function is only used for creating users with passwords. The external
|
||||
service login flows do not use this function.
|
||||
|
||||
### Managing usernames and emails
|
||||
|
||||
Instead of modifying documents in the [`Meteor.users`](#meteor_users) collection
|
||||
directly, use these convenience functions which correctly check for case
|
||||
insensitive duplicates before updates.
|
||||
|
||||
{{> autoApiBox "Accounts.setUsername"}}
|
||||
|
||||
{{> autoApiBox "Accounts.addEmail"}}
|
||||
|
||||
By default, an email address is added with `{ verified: false }`. Use
|
||||
[`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an
|
||||
email with a link the user can use verify their email address.
|
||||
|
||||
{{> autoApiBox "Accounts.removeEmail"}}
|
||||
|
||||
{{> autoApiBox "Accounts.verifyEmail"}}
|
||||
|
||||
This function accepts tokens passed into the callback registered with
|
||||
[`Accounts.onEmailVerificationLink`](#Accounts-onEmailVerificationLink).
|
||||
|
||||
{{> autoApiBox "Accounts.findUserByUsername"}}
|
||||
|
||||
{{> autoApiBox "Accounts.findUserByEmail"}}
|
||||
|
||||
### Managing passwords
|
||||
|
||||
Use the below functions to initiate password changes or resets from the server
|
||||
or the client.
|
||||
|
||||
{{> autoApiBox "Accounts.changePassword"}}
|
||||
|
||||
@@ -70,10 +100,9 @@ This function accepts tokens passed into the callbacks registered with
|
||||
|
||||
{{> autoApiBox "Accounts.setPassword"}}
|
||||
|
||||
{{> autoApiBox "Accounts.verifyEmail"}}
|
||||
|
||||
This function accepts tokens passed into the callback registered with
|
||||
[`Accounts.onEmailVerificationLink`](#Accounts-onEmailVerificationLink).
|
||||
|
||||
<h3 id="sending-emails"><span>Sending emails</span></h3>
|
||||
|
||||
{{> autoApiBox "Accounts.sendResetPasswordEmail"}}
|
||||
|
||||
|
||||
@@ -138,11 +138,20 @@ var toc = [
|
||||
|
||||
{name: "Passwords", id: "accounts_passwords"}, [
|
||||
"Accounts.createUser",
|
||||
{type: "spacer"},
|
||||
|
||||
{name: "Accounts.setUsername", id: "Accounts-setUsername"},
|
||||
{name: "Accounts.addEmail", id: "Accounts-addEmail"},
|
||||
{name: "Accounts.removeEmail", id: "Accounts-removeEmail"},
|
||||
{name: "Accounts.verifyEmail", id: "accounts_verifyemail"},
|
||||
{name: "Accounts.findUserByUsername", id: "Accounts-findUserByUsername"},
|
||||
{name: "Accounts.findUserByEmail", id: "Accounts-findUserByEmail"},
|
||||
{type: "spacer"},
|
||||
|
||||
"Accounts.changePassword",
|
||||
"Accounts.forgotPassword",
|
||||
"Accounts.resetPassword",
|
||||
"Accounts.setPassword",
|
||||
"Accounts.verifyEmail",
|
||||
{type: "spacer"},
|
||||
|
||||
"Accounts.sendResetPasswordEmail",
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
[
|
||||
"Accounts",
|
||||
"Accounts",
|
||||
"Accounts.addEmail",
|
||||
"Accounts.changePassword",
|
||||
"Accounts.createUser",
|
||||
"Accounts.emailTemplates",
|
||||
"Accounts.findUserByEmail",
|
||||
"Accounts.findUserByUsername",
|
||||
"Accounts.forgotPassword",
|
||||
"Accounts.removeEmail",
|
||||
"Accounts.resetPassword",
|
||||
"Accounts.sendEnrollmentEmail",
|
||||
"Accounts.sendResetPasswordEmail",
|
||||
"Accounts.sendVerificationEmail",
|
||||
"Accounts.setPassword",
|
||||
"Accounts.setUsername",
|
||||
"Accounts.ui",
|
||||
"Accounts.ui.config",
|
||||
"Accounts.verifyEmail",
|
||||
|
||||
@@ -130,8 +130,6 @@ Accounts.createUser = function (options, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Change password. Must be logged in.
|
||||
//
|
||||
// @param oldPassword {String|null} By default servers no longer allow
|
||||
|
||||
@@ -77,13 +77,7 @@ var checkPassword = Accounts._checkPassword;
|
||||
/// LOGIN
|
||||
///
|
||||
|
||||
// Attempts to find a user from a user query.
|
||||
// First tries to match username or email case sensitively; if that fails, it
|
||||
// tries case insensitively; but if more than one user matches the case
|
||||
// insensitive search, it returns null
|
||||
// @param query {Object} with one of `id`, `username`, or `email`.
|
||||
// @returns A user if found, else null
|
||||
var findUserFromQuery = function (query) {
|
||||
Accounts._findUserByQuery = function (query) {
|
||||
var user = null;
|
||||
|
||||
if (query.id) {
|
||||
@@ -110,8 +104,6 @@ var findUserFromQuery = function (query) {
|
||||
// No match if multiple candidates are found
|
||||
if (candidateUsers.length === 1) {
|
||||
user = candidateUsers[0];
|
||||
} else {
|
||||
console.error('Found multiple users with ' + fieldName + ' = ' + fieldValue + ' only differing in case. Requiring case sensitive login.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +111,35 @@ var findUserFromQuery = function (query) {
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Finds the user with the specified username.
|
||||
* First tries to match username case sensitively; if that fails, it
|
||||
* tries case insensitively; but if more than one user matches the case
|
||||
* insensitive search, it returns null.
|
||||
* @locus Server
|
||||
* @param {String} username The username to look for
|
||||
* @returns {Object} A user if found, else null
|
||||
*/
|
||||
Accounts.findUserByUsername = function (username) {
|
||||
return Accounts._findUserByQuery({
|
||||
username: username
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Finds the user with the specified email.
|
||||
* First tries to match email case sensitively; if that fails, it
|
||||
* tries case insensitively; but if more than one user matches the case
|
||||
* insensitive search, it returns null.
|
||||
* @locus Server
|
||||
* @param {String} email The email address to look for
|
||||
* @returns {Object} A user if found, else null
|
||||
*/
|
||||
Accounts.findUserByEmail = function (email) {
|
||||
return Accounts._findUserByQuery({
|
||||
email: email
|
||||
});
|
||||
};
|
||||
|
||||
// Generates a MongoDB selector that can be used to perform a fast case
|
||||
// insensitive lookup for the given fieldName and string. Since MongoDB does
|
||||
@@ -164,6 +185,26 @@ var generateCasePermutationsForString = function (string) {
|
||||
return permutations;
|
||||
}
|
||||
|
||||
var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) {
|
||||
// Some tests need the ability to add users with the same case insensitive
|
||||
// value, hence the _skipCaseInsensitiveChecksForTest check
|
||||
var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue);
|
||||
|
||||
if (fieldValue && !skipCheck) {
|
||||
var matchedUsers = Meteor.users.find(
|
||||
selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch();
|
||||
|
||||
if (matchedUsers.length > 0 &&
|
||||
// If we don't have a userId yet, any match we find is a duplicate
|
||||
(!ownUserId ||
|
||||
// Otherwise, check to see if there are multiple matches or a match
|
||||
// that is not us
|
||||
(matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) {
|
||||
throw new Meteor.Error(403, displayName + " already exists.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// XXX maybe this belongs in the check package
|
||||
var NonEmptyString = Match.Where(function (x) {
|
||||
check(x, String);
|
||||
@@ -210,7 +251,7 @@ Accounts.registerLoginHandler("password", function (options) {
|
||||
});
|
||||
|
||||
|
||||
var user = findUserFromQuery(options.user);
|
||||
var user = Accounts._findUserByQuery(options.user);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
@@ -276,7 +317,7 @@ Accounts.registerLoginHandler("password", function (options) {
|
||||
password: passwordValidator
|
||||
});
|
||||
|
||||
var user = findUserFromQuery(options.user);
|
||||
var user = Accounts._findUserByQuery(options.user);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
@@ -320,6 +361,40 @@ Accounts.registerLoginHandler("password", function (options) {
|
||||
/// CHANGING
|
||||
///
|
||||
|
||||
/**
|
||||
* @summary Change a user's username. Use this instead of updating the
|
||||
* database directly. The operation will fail if there is an existing user
|
||||
* with a username only differing in case.
|
||||
* @locus Server
|
||||
* @param {String} userId The ID of the user to update.
|
||||
* @param {String} newUsername A new username for the user.
|
||||
*/
|
||||
Accounts.setUsername = function (userId, newUsername) {
|
||||
check(userId, NonEmptyString);
|
||||
check(newUsername, NonEmptyString);
|
||||
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
var oldUsername = user.username;
|
||||
|
||||
// Perform a case insensitive check fro duplicates before update
|
||||
checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id);
|
||||
|
||||
Meteor.users.update({_id: user._id}, {$set: {username: newUsername}});
|
||||
|
||||
// Perform another check after update, in case a matching user has been
|
||||
// inserted in the meantime
|
||||
try {
|
||||
checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id);
|
||||
} catch (ex) {
|
||||
// Undo update if the check fails
|
||||
Meteor.users.update({_id: user._id}, {$set: {username: oldUsername}});
|
||||
throw ex;
|
||||
}
|
||||
};
|
||||
|
||||
// Let the user change their own password if they know the old
|
||||
// password. `oldPassword` and `newPassword` should be objects with keys
|
||||
// `digest` and `algorithm` (representing the SHA256 of the password).
|
||||
@@ -758,7 +833,111 @@ Meteor.methods({verifyEmail: function (token) {
|
||||
);
|
||||
}});
|
||||
|
||||
/**
|
||||
* @summary Add an email address for a user. Use this instead of directly
|
||||
* updating the database. The operation will fail if there is a different user
|
||||
* with an email only differing in case. If the specified user has an existing
|
||||
* email only differing in case however, we replace it.
|
||||
* @locus Server
|
||||
* @param {String} userId The ID of the user to update.
|
||||
* @param {String} newEmail A new email address for the user.
|
||||
* @param {Boolean} [verified] Optional - whether the new email address should
|
||||
* be marked as verified. Defaults to false.
|
||||
*/
|
||||
Accounts.addEmail = function (userId, newEmail, verified) {
|
||||
check(userId, NonEmptyString);
|
||||
check(newEmail, NonEmptyString);
|
||||
check(verified, Match.Optional(Boolean));
|
||||
|
||||
if (_.isUndefined(verified)) {
|
||||
verified = false;
|
||||
}
|
||||
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
// Allow users to change their own email to a version with a different case
|
||||
|
||||
// We don't have to call checkForCaseInsensitiveDuplicates to do a case
|
||||
// insensitive check across all emails in the database here because: (1) if
|
||||
// there is no case-insensitive duplicate between this user and other users,
|
||||
// then we are OK and (2) if this would create a conflict with other users
|
||||
// then there would already be a case-insensitive duplicate and we can't fix
|
||||
// that in this code anyway.
|
||||
var caseInsensitiveRegExp =
|
||||
new RegExp('^' + Meteor._escapeRegExp(newEmail) + '$', 'i');
|
||||
|
||||
var didUpdateOwnEmail = _.any(user.emails, function(email, index) {
|
||||
if (caseInsensitiveRegExp.test(email.address)) {
|
||||
Meteor.users.update({
|
||||
_id: user._id,
|
||||
'emails.address': email.address
|
||||
}, {$set: {
|
||||
'emails.$.address': newEmail,
|
||||
'emails.$.verified': verified
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// In the other updates below, we have to do another call to
|
||||
// checkForCaseInsensitiveDuplicates to make sure that no conflicting values
|
||||
// were added to the database in the meantime. We don't have to do this for
|
||||
// the case where the user is updating their email address to one that is the
|
||||
// same as before, but only different because of capitalization. Read the
|
||||
// big comment above to understand why.
|
||||
|
||||
if (didUpdateOwnEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform a case insensitive check for duplicates before update
|
||||
checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id);
|
||||
|
||||
Meteor.users.update({
|
||||
_id: user._id
|
||||
}, {
|
||||
$addToSet: {
|
||||
emails: {
|
||||
address: newEmail,
|
||||
verified: verified
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Perform another check after update, in case a matching user has been
|
||||
// inserted in the meantime
|
||||
try {
|
||||
checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id);
|
||||
} catch (ex) {
|
||||
// Undo update if the check fails
|
||||
Meteor.users.update({_id: user._id},
|
||||
{$pull: {emails: {address: newEmail}}});
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Remove an email address for a user. Use this instead of updating
|
||||
* the database directly.
|
||||
* @locus Server
|
||||
* @param {String} userId The ID of the user to update.
|
||||
* @param {String} email The email address to remove.
|
||||
*/
|
||||
Accounts.removeEmail = function (userId, email) {
|
||||
check(userId, NonEmptyString);
|
||||
check(email, NonEmptyString);
|
||||
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
Meteor.users.update({_id: user._id},
|
||||
{$pull: {emails: {address: email}}});
|
||||
}
|
||||
|
||||
///
|
||||
/// CREATING USERS
|
||||
@@ -794,34 +973,16 @@ var createUser = function (options) {
|
||||
if (email)
|
||||
user.emails = [{address: email, verified: false}];
|
||||
|
||||
// Check if there is no other user with a username or email only differing
|
||||
// in case.
|
||||
var performCaseInsensitiveCheck = function () {
|
||||
// Some tests need the ability to add users with the same case insensitive
|
||||
// username or email, hence the _skipCaseInsensitiveChecksForTest check
|
||||
|
||||
if (username &&
|
||||
!_.has(Accounts._skipCaseInsensitiveChecksForTest, username) &&
|
||||
Meteor.users.find(selectorForFastCaseInsensitiveLookup(
|
||||
"username", username)).count() > 1) {
|
||||
throw new Meteor.Error(403, "Username already exists.");
|
||||
}
|
||||
|
||||
if (email &&
|
||||
!_.has(Accounts._skipCaseInsensitiveChecksForTest, email) &&
|
||||
Meteor.users.find(selectorForFastCaseInsensitiveLookup(
|
||||
"emails.address", email)).count() > 1) {
|
||||
throw new Meteor.Error(403, "Email already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a case insensitive check before insert
|
||||
performCaseInsensitiveCheck();
|
||||
checkForCaseInsensitiveDuplicates('username', 'Username', username);
|
||||
checkForCaseInsensitiveDuplicates('emails.address', 'Email', email);
|
||||
|
||||
var userId = Accounts.insertUserDoc(options, user);
|
||||
// Perform another check after insert, in case a matching user has been
|
||||
// inserted in the meantime
|
||||
try {
|
||||
performCaseInsensitiveCheck();
|
||||
checkForCaseInsensitiveDuplicates('username', 'Username', username, userId);
|
||||
checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId);
|
||||
} catch (ex) {
|
||||
// Remove inserted user if the check fails
|
||||
Meteor.users.remove(userId);
|
||||
|
||||
@@ -2,9 +2,6 @@ Accounts._noConnectionCloseDelayForTest = true;
|
||||
Accounts.removeDefaultRateLimit();
|
||||
if (Meteor.isServer) {
|
||||
Meteor.methods({
|
||||
getUserId: function () {
|
||||
return this.userId;
|
||||
},
|
||||
getResetToken: function () {
|
||||
var token = Meteor.users.findOne(this.userId).services.password.reset;
|
||||
return token;
|
||||
@@ -48,14 +45,29 @@ if (Meteor.isClient) (function () {
|
||||
};
|
||||
var logoutStep = function (test, expect) {
|
||||
Meteor.logout(expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
test.equal(Meteor.user(), null);
|
||||
}));
|
||||
};
|
||||
var loggedInAs = function (someUsername, test, expect) {
|
||||
return expect(function (error) {
|
||||
test.equal(error, undefined);
|
||||
test.equal(Meteor.user().username, someUsername);
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
test.equal(Meteor.userId() && Meteor.user().username, someUsername);
|
||||
});
|
||||
};
|
||||
var loggedInUserHasEmail = function (someEmail, test, expect) {
|
||||
return expect(function (error) {
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
var user = Meteor.user();
|
||||
test.isTrue(user && _.some(user.emails, function(email) {
|
||||
return email.address === someEmail;
|
||||
}));
|
||||
});
|
||||
};
|
||||
var expectError = function (expectedError, test, expect) {
|
||||
@@ -74,17 +86,23 @@ if (Meteor.isClient) (function () {
|
||||
};
|
||||
var invalidateLoginsStep = function (test, expect) {
|
||||
Meteor.call("testInvalidateLogins", 'fail', expect(function (error) {
|
||||
test.isFalse(error);
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
}));
|
||||
};
|
||||
var hideActualLoginErrorStep = function (test, expect) {
|
||||
Meteor.call("testInvalidateLogins", 'hide', expect(function (error) {
|
||||
test.isFalse(error);
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
}));
|
||||
};
|
||||
var validateLoginsStep = function (test, expect) {
|
||||
Meteor.call("testInvalidateLogins", false, expect(function (error) {
|
||||
test.isFalse(error);
|
||||
if (error) {
|
||||
test.fail(error.message);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -206,7 +224,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive username with non-ASCII characters", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive username " +
|
||||
"with non-ASCII characters", [
|
||||
function (test, expect) {
|
||||
// Hack because Tinytest does not clean the database between tests/runs
|
||||
this.randomSuffix = Random.id(10);
|
||||
@@ -226,7 +245,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive username should escape regex special characters", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive username " +
|
||||
"should escape regex special characters", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// We shouldn't be able to log in with a regex expression for the username
|
||||
@@ -238,7 +258,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive username should require a match of the full string", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive username " +
|
||||
"should require a match of the full string", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// We shouldn't be able to log in with a partial match for the username
|
||||
@@ -250,21 +271,22 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive username when there are multiple matches", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive username when " +
|
||||
"there are multiple matches", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
this.otherUserName = 'Adalovelace' + this.randomSuffix;
|
||||
addSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
|
||||
this.otherUsername = 'Adalovelace' + this.randomSuffix;
|
||||
addSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
|
||||
},
|
||||
// Create another user with a username that only differs in case
|
||||
function (test, expect) {
|
||||
Accounts.createUser(
|
||||
{ username: this.otherUserName, password: this.password },
|
||||
loggedInAs(this.otherUserName, test, expect));
|
||||
{ username: this.otherUsername, password: this.password },
|
||||
loggedInAs(this.otherUsername, test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
removeSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
|
||||
removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
|
||||
},
|
||||
// We shouldn't be able to log in with the username in lower case
|
||||
function (test, expect) {
|
||||
@@ -282,7 +304,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - creating users with the same case insensitive username", [
|
||||
testAsyncMulti("passwords - creating users with the same case insensitive " +
|
||||
"username", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// Attempting to create another user with a username that only differs in
|
||||
@@ -318,7 +341,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive email should escape regex special characters", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive email should " +
|
||||
"escape regex special characters", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// We shouldn't be able to log in with a regex expression for the email
|
||||
@@ -330,7 +354,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive email should require a match of the full string", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive email should " +
|
||||
"require a match of the full string", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// We shouldn't be able to log in with a partial match for the email
|
||||
@@ -342,24 +367,25 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - logging in with case insensitive email when there are multiple matches", [
|
||||
testAsyncMulti("passwords - logging in with case insensitive email when " +
|
||||
"there are multiple matches", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
function (test, expect) {
|
||||
this.otherUserName = 'AdaLovelace' + Random.id(10);
|
||||
this.otherUsername = 'AdaLovelace' + Random.id(10);
|
||||
this.otherEmail = "ADA-intercept@lovelace.com" + this.randomSuffix;
|
||||
addSkipCaseInsensitiveChecksForTest(this.otherEmail, test, expect);
|
||||
},
|
||||
// Create another user with an email that only differs in case
|
||||
function (test, expect) {
|
||||
Accounts.createUser(
|
||||
{ username: this.otherUserName,
|
||||
{ username: this.otherUsername,
|
||||
email: this.otherEmail,
|
||||
password: this.password },
|
||||
loggedInAs(this.otherUserName, test, expect));
|
||||
loggedInAs(this.otherUsername, test, expect));
|
||||
},
|
||||
function (test, expect) {
|
||||
removeSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
|
||||
removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
|
||||
},
|
||||
logoutStep,
|
||||
// We shouldn't be able to log in with the email in lower case
|
||||
@@ -378,7 +404,8 @@ if (Meteor.isClient) (function () {
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti("passwords - creating users with the same case insensitive email", [
|
||||
testAsyncMulti("passwords - creating users with the same case insensitive " +
|
||||
"email", [
|
||||
createUserStep,
|
||||
logoutStep,
|
||||
// Attempting to create another user with an email that only differs in
|
||||
@@ -1229,7 +1256,8 @@ if (Meteor.isServer) (function () {
|
||||
});
|
||||
});
|
||||
|
||||
// XXX would be nice to test Accounts.config({forbidClientAccountCreation: true})
|
||||
// XXX would be nice to test
|
||||
// Accounts.config({forbidClientAccountCreation: true})
|
||||
|
||||
Tinytest.addAsync(
|
||||
'passwords - login token observes get cleaned up',
|
||||
@@ -1295,7 +1323,8 @@ if (Meteor.isServer) (function () {
|
||||
|
||||
Accounts.sendResetPasswordEmail(userId, email);
|
||||
|
||||
var resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0];
|
||||
var resetPasswordEmailOptions =
|
||||
Meteor.call("getInterceptedEmails", email)[0];
|
||||
|
||||
var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)");
|
||||
var match = resetPasswordEmailOptions.text.match(re);
|
||||
@@ -1312,4 +1341,164 @@ if (Meteor.isServer) (function () {
|
||||
Meteor.call("login", {user: {username: username}, password: "new-password"});
|
||||
}, /Incorrect password/);
|
||||
});
|
||||
|
||||
// We should be able to change the username
|
||||
Tinytest.add("passwords - change username", function (test) {
|
||||
var username = Random.id();
|
||||
var userId = Accounts.createUser({
|
||||
username: username
|
||||
});
|
||||
|
||||
test.isTrue(userId);
|
||||
|
||||
var newUsername = Random.id();
|
||||
Accounts.setUsername(userId, newUsername);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername);
|
||||
|
||||
// Test findUserByUsername as well while we're here
|
||||
test.equal(Accounts.findUserByUsername(newUsername)._id, userId);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - change username to a new one only differing " +
|
||||
"in case", function (test) {
|
||||
var username = Random.id() + "user";
|
||||
var userId = Accounts.createUser({
|
||||
username: username.toUpperCase()
|
||||
});
|
||||
|
||||
test.isTrue(userId);
|
||||
|
||||
var newUsername = username.toLowerCase();
|
||||
Accounts.setUsername(userId, newUsername);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername);
|
||||
});
|
||||
|
||||
// We should not be able to change the username to one that only
|
||||
// differs in case from an existing one
|
||||
Tinytest.add("passwords - change username should fail when there are " +
|
||||
"existing users with a username only differing in case", function (test) {
|
||||
var username = Random.id() + "user";
|
||||
var usernameUpper = username.toUpperCase();
|
||||
|
||||
var userId1 = Accounts.createUser({
|
||||
username: username
|
||||
});
|
||||
|
||||
var user2OriginalUsername = Random.id();
|
||||
var userId2 = Accounts.createUser({
|
||||
username: user2OriginalUsername
|
||||
});
|
||||
|
||||
test.isTrue(userId1);
|
||||
test.isTrue(userId2);
|
||||
|
||||
test.throws(function () {
|
||||
Accounts.setUsername(userId2, usernameUpper);
|
||||
}, /Username already exists/);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId2}).username,
|
||||
user2OriginalUsername);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - add email", function (test) {
|
||||
var origEmail = Random.id() + "@turing.com";
|
||||
var userId = Accounts.createUser({
|
||||
email: origEmail
|
||||
});
|
||||
|
||||
var newEmail = Random.id() + "@turing.com";
|
||||
Accounts.addEmail(userId, newEmail);
|
||||
|
||||
var thirdEmail = Random.id() + "@turing.com";
|
||||
Accounts.addEmail(userId, thirdEmail, true);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).emails, [
|
||||
{ address: origEmail, verified: false },
|
||||
{ address: newEmail, verified: false },
|
||||
{ address: thirdEmail, verified: true }
|
||||
]);
|
||||
|
||||
// Test findUserByEmail as well while we're here
|
||||
test.equal(Accounts.findUserByEmail(origEmail)._id, userId);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - add email when the user has an existing email " +
|
||||
"only differing in case", function (test) {
|
||||
var origEmail = Random.id() + "@turing.com";
|
||||
var userId = Accounts.createUser({
|
||||
email: origEmail
|
||||
});
|
||||
|
||||
var newEmail = Random.id() + "@turing.com";
|
||||
Accounts.addEmail(userId, newEmail);
|
||||
|
||||
var thirdEmail = origEmail.toUpperCase();
|
||||
Accounts.addEmail(userId, thirdEmail, true);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).emails, [
|
||||
{ address: thirdEmail, verified: true },
|
||||
{ address: newEmail, verified: false }
|
||||
]);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - add email should fail when there is an existing " +
|
||||
"user with an email only differing in case", function (test) {
|
||||
var user1Email = Random.id() + "@turing.com";
|
||||
var userId1 = Accounts.createUser({
|
||||
email: user1Email
|
||||
});
|
||||
|
||||
var user2Email = Random.id() + "@turing.com";
|
||||
var userId2 = Accounts.createUser({
|
||||
email: user2Email
|
||||
});
|
||||
|
||||
var dupEmail = user1Email.toUpperCase();
|
||||
test.throws(function () {
|
||||
Accounts.addEmail(userId2, dupEmail);
|
||||
}, /Email already exists/);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId1}).emails, [
|
||||
{ address: user1Email, verified: false }
|
||||
]);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId2}).emails, [
|
||||
{ address: user2Email, verified: false }
|
||||
]);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - remove email", function (test) {
|
||||
var origEmail = Random.id() + "@turing.com";
|
||||
var userId = Accounts.createUser({
|
||||
email: origEmail
|
||||
});
|
||||
|
||||
var newEmail = Random.id() + "@turing.com";
|
||||
Accounts.addEmail(userId, newEmail);
|
||||
|
||||
var thirdEmail = Random.id() + "@turing.com";
|
||||
Accounts.addEmail(userId, thirdEmail, true);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).emails, [
|
||||
{ address: origEmail, verified: false },
|
||||
{ address: newEmail, verified: false },
|
||||
{ address: thirdEmail, verified: true }
|
||||
]);
|
||||
|
||||
Accounts.removeEmail(userId, newEmail);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).emails, [
|
||||
{ address: origEmail, verified: false },
|
||||
{ address: thirdEmail, verified: true }
|
||||
]);
|
||||
|
||||
Accounts.removeEmail(userId, origEmail);
|
||||
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).emails, [
|
||||
{ address: thirdEmail, verified: true }
|
||||
]);
|
||||
});
|
||||
|
||||
}) ();
|
||||
|
||||
Reference in New Issue
Block a user