mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge pull request #10818 from wildhart/fix-issue-10469
Limit fetching full user objects (performance) Fixes #10469
This commit is contained in:
21
History.md
21
History.md
@@ -128,6 +128,25 @@ N/A
|
||||
|
||||
* Facebook OAuth has been updated to call v5 API endpoints. [PR #10738](https://github.com/meteor/meteor/pull/10738)
|
||||
|
||||
### Changes
|
||||
|
||||
* `Meteor.user()`, `Meteor.findUserByEmail()` and `Meteor.findUserByUserName()` can take a new
|
||||
`options` parameter which can be used to limit the returned fields. Useful for minimizing
|
||||
DB bandwidth on the server and avoiding unnecessary reactive UI updates on the client.
|
||||
[Issue #10469](https://github.com/meteor/meteor/issues/10469)
|
||||
|
||||
* `Accounts.config()` has a new option `defaultFieldSelector` which will apply to all
|
||||
`Meteor.user()` and `Meteor.findUserBy...()` functions without explicit field selectors, and
|
||||
also to all `onLogin`, `onLogout` and `onLoginFailure` callbacks. This is useful if you store
|
||||
large data on the user document (e.g. a growing list of transactions) which do no need to be
|
||||
retrieved from the DB whenever you or a package author call `Meteor.user()` without limiting the
|
||||
fields. [Issue #10469](https://github.com/meteor/meteor/issues/10469)
|
||||
|
||||
* Lots of internal calls to `Meteor.user()` without field specifiers in `accounts-base` and
|
||||
`accounts-password` packages have been optimized with explicit field selectors to only fetch
|
||||
the fields needed by the functions they are in.
|
||||
[Issue #10469](https://github.com/meteor/meteor/issues/10469)
|
||||
|
||||
## v1.8.3, 2019-12-19
|
||||
|
||||
### Migration Steps
|
||||
@@ -145,6 +164,8 @@ N/A
|
||||
|
||||
### Changes
|
||||
|
||||
* The `meteor-babel` npm package has been updated to version 7.7.4.
|
||||
|
||||
* Node has been updated to version
|
||||
[8.17.0](https://nodejs.org/en/blog/release/v8.17.0/).
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
const username = 'jsmith';
|
||||
const password = 'password';
|
||||
const excludeField = 'excludeField';
|
||||
const defaultExcludeField = 'defaultExcludeField';
|
||||
const excludeValue = 'foo';
|
||||
const profile = {
|
||||
name: username,
|
||||
[excludeField]: excludeValue,
|
||||
[defaultExcludeField]: excludeValue,
|
||||
}
|
||||
|
||||
const logoutAndCreateUser = (test, done, nextTests) => {
|
||||
Meteor.logout(() => {
|
||||
@@ -7,7 +15,7 @@ const logoutAndCreateUser = (test, done, nextTests) => {
|
||||
test.isFalse(Meteor.user());
|
||||
|
||||
// Setup a new test user
|
||||
Accounts.createUser({ username, password }, () => {
|
||||
Accounts.createUser({ username, password, profile }, () => {
|
||||
// Handle next tests
|
||||
nextTests(test, done);
|
||||
});
|
||||
@@ -100,3 +108,32 @@ Tinytest.addAsync(
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'accounts - Meteor.user obeys explicit and default field selectors',
|
||||
(test, done) => {
|
||||
logoutAndCreateUser(test, done, () => {
|
||||
Meteor.loginWithPassword(username, password, () => {
|
||||
// by default, all fields should be returned
|
||||
test.equal(Meteor.user().profile[excludeField], excludeValue);
|
||||
|
||||
// this time we want to exclude the default fields
|
||||
const options = Accounts._options;
|
||||
Accounts._options = {};
|
||||
Accounts.config({defaultFieldSelector: {['profile.'+defaultExcludeField]: 0}});
|
||||
let user = Meteor.user();
|
||||
test.isUndefined(user.profile[defaultExcludeField]);
|
||||
test.equal(user.profile[excludeField], excludeValue);
|
||||
test.equal(user.profile.name, username);
|
||||
|
||||
// this time we only want certain fields...
|
||||
user = Meteor.user({fields: {'profile.name': 1}});
|
||||
test.isUndefined(user.profile[excludeField]);
|
||||
test.isUndefined(user.profile[defaultExcludeField]);
|
||||
test.equal(user.profile.name, username);
|
||||
Accounts._options = options;
|
||||
removeTestUser(done);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -77,13 +77,46 @@ export class AccountsCommon {
|
||||
throw new Error("userId method not implemented");
|
||||
}
|
||||
|
||||
// merge the defaultFieldSelector with an existing options object
|
||||
_addDefaultFieldSelector(options = {}) {
|
||||
// this will be the most common case for most people, so make it quick
|
||||
if (!this._options.defaultFieldSelector) return options;
|
||||
|
||||
// if no field selector then just use defaultFieldSelector
|
||||
if (!options.fields) return {
|
||||
...options,
|
||||
fields: this._options.defaultFieldSelector,
|
||||
};
|
||||
|
||||
// if empty field selector then the full user object is explicitly requested, so obey
|
||||
const keys = Object.keys(options.fields);
|
||||
if (!keys.length) return options;
|
||||
|
||||
// if the requested fields are +ve then ignore defaultFieldSelector
|
||||
// assume they are all either +ve or -ve because Mongo doesn't like mixed
|
||||
if (!!options.fields[keys[0]]) return options;
|
||||
|
||||
// The requested fields are -ve.
|
||||
// If the defaultFieldSelector is +ve then use requested fields, otherwise merge them
|
||||
const keys2 = Object.keys(this._options.defaultFieldSelector);
|
||||
return this._options.defaultFieldSelector[keys2[0]] ? options : {
|
||||
...options,
|
||||
fields: {
|
||||
...options.fields,
|
||||
...this._options.defaultFieldSelector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source.
|
||||
* @locus Anywhere
|
||||
* @param {Object} [options]
|
||||
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
||||
*/
|
||||
user() {
|
||||
user(options) {
|
||||
const userId = this.userId();
|
||||
return userId ? this.users.findOne(userId) : null;
|
||||
return userId ? this.users.findOne(userId, this._addDefaultFieldSelector(options)) : null;
|
||||
}
|
||||
|
||||
// Set up config for the accounts system. Call this on both the client
|
||||
@@ -132,6 +165,7 @@ export class AccountsCommon {
|
||||
* @param {Number} options.passwordResetTokenExpirationInDays The number of days from when a link to reset password is sent until token expires and user can't reset password with the link anymore. Defaults to 3.
|
||||
* @param {Number} options.passwordEnrollTokenExpirationInDays The number of days from when a link to set inital password is sent until token expires and user can't set password with the link anymore. Defaults to 30.
|
||||
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to false.
|
||||
* @param {MongoFieldSpecifier} options.defaultFieldSelector To exclude by default large custom fields from `Meteor.user()` and `Meteor.findUserBy...()` functions when called without a field selector, and all `onLogin`, `onLoginFailure` and `onLogout` callbacks. Example: `Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})`.
|
||||
*/
|
||||
config(options) {
|
||||
// We don't want users to accidentally only call Accounts.config on the
|
||||
@@ -166,7 +200,8 @@ export class AccountsCommon {
|
||||
// validate option keys
|
||||
const VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpirationInDays",
|
||||
"restrictCreationByEmailDomain", "loginExpirationInDays", "passwordResetTokenExpirationInDays",
|
||||
"ambiguousErrorMessages", "bcryptRounds"];
|
||||
"ambiguousErrorMessages", "bcryptRounds", "defaultFieldSelector"];
|
||||
|
||||
Object.keys(options).forEach(key => {
|
||||
if (!VALID_KEYS.includes(key)) {
|
||||
throw new Error(`Accounts.config: Invalid key: ${key}`);
|
||||
@@ -307,8 +342,10 @@ Meteor.userId = () => Accounts.userId();
|
||||
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source.
|
||||
* @locus Anywhere but publish functions
|
||||
* @importFromPackage meteor
|
||||
* @param {Object} [options]
|
||||
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
||||
*/
|
||||
Meteor.user = () => Accounts.user();
|
||||
Meteor.user = (options) => Accounts.user(options);
|
||||
|
||||
// how long (in days) until a login token expires
|
||||
const DEFAULT_LOGIN_EXPIRATION_DAYS = 90;
|
||||
|
||||
@@ -188,8 +188,10 @@ export class AccountsServer extends AccountsCommon {
|
||||
};
|
||||
|
||||
_successfulLogout(connection, userId) {
|
||||
const user = userId && this.users.findOne(userId);
|
||||
// don't fetch the user object unless there are some callbacks registered
|
||||
let user;
|
||||
this._onLogoutHook.each(callback => {
|
||||
if (!user && userId) user = this.users.findOne(userId, {fields: this._options.defaultFieldSelector});
|
||||
callback({ user, connection });
|
||||
return true;
|
||||
});
|
||||
@@ -317,7 +319,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
|
||||
let user;
|
||||
if (result.userId)
|
||||
user = this.users.findOne(result.userId);
|
||||
user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector});
|
||||
|
||||
const attempt = {
|
||||
type: result.type || "unknown",
|
||||
@@ -398,7 +400,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
};
|
||||
|
||||
if (result.userId) {
|
||||
attempt.user = this.users.findOne(result.userId);
|
||||
attempt.user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector});
|
||||
}
|
||||
|
||||
this._validateLogin(methodInvocation.connection, attempt);
|
||||
@@ -1145,9 +1147,9 @@ export class AccountsServer extends AccountsCommon {
|
||||
Meteor.startup(() => {
|
||||
this.users.find({
|
||||
"services.resume.haveLoginTokensToDelete": true
|
||||
}, {
|
||||
}, {fields: {
|
||||
"services.resume.loginTokensToDelete": 1
|
||||
}).forEach(user => {
|
||||
}}).forEach(user => {
|
||||
this._deleteSavedTokensForUser(
|
||||
user._id,
|
||||
user.services.resume.loginTokensToDelete
|
||||
@@ -1207,7 +1209,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
selector[serviceIdKey] = serviceData.id;
|
||||
}
|
||||
|
||||
let user = this.users.findOne(selector);
|
||||
let user = this.users.findOne(selector, {fields: this._options.defaultFieldSelector});
|
||||
|
||||
// When creating a new user we pass through all options. When updating an
|
||||
// existing user, by default we only process/pass through the serviceData
|
||||
@@ -1317,7 +1319,8 @@ const defaultResumeLoginHandler = (accounts, options) => {
|
||||
// sending the unhashed token to the database in a query if we don't
|
||||
// need to.
|
||||
let user = accounts.users.findOne(
|
||||
{"services.resume.loginTokens.hashedToken": hashedToken});
|
||||
{"services.resume.loginTokens.hashedToken": hashedToken},
|
||||
{fields: {"services.resume.loginTokens.$": 1}});
|
||||
|
||||
if (! user) {
|
||||
// If we didn't find the hashed login token, try also looking for
|
||||
@@ -1330,7 +1333,9 @@ const defaultResumeLoginHandler = (accounts, options) => {
|
||||
{"services.resume.loginTokens.hashedToken": hashedToken},
|
||||
{"services.resume.loginTokens.token": options.resume}
|
||||
]
|
||||
});
|
||||
},
|
||||
// Note: Cannot use ...loginTokens.$ positional operator with $or query.
|
||||
{fields: {"services.resume.loginTokens": 1}});
|
||||
}
|
||||
|
||||
if (! user)
|
||||
|
||||
@@ -57,6 +57,15 @@ Tinytest.add('accounts - config - default token lifetime', test => {
|
||||
Accounts._options = options;
|
||||
});
|
||||
|
||||
Tinytest.add('accounts - config - defaultFieldSelector', test => {
|
||||
const options = Accounts._options;
|
||||
Accounts._options = {};
|
||||
const setValue = {bigArray: 0};
|
||||
Accounts.config({defaultFieldSelector: setValue});
|
||||
test.equal(Accounts._options.defaultFieldSelector, setValue);
|
||||
Accounts._options = options;
|
||||
});
|
||||
|
||||
const idsInValidateNewUser = {};
|
||||
Accounts.validateNewUser(user => {
|
||||
idsInValidateNewUser[user._id] = true;
|
||||
@@ -451,6 +460,109 @@ Tinytest.add(
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
'accounts - hook callbacks obey options.defaultFieldSelector',
|
||||
test => {
|
||||
const ignoreFieldName = "bigArray";
|
||||
const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1] });
|
||||
const stampedToken = Accounts._generateStampedLoginToken();
|
||||
Accounts._insertLoginToken(userId, stampedToken);
|
||||
const options = Accounts._options;
|
||||
Accounts._options = {};
|
||||
Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}});
|
||||
test.equal(Accounts._options.defaultFieldSelector, {[ignoreFieldName]: 0}, 'defaultFieldSelector');
|
||||
|
||||
const validateStopper = Accounts.validateLoginAttempt(attempt => {
|
||||
test.isUndefined(allowLogin != 'bogus' ? attempt.user[ignoreFieldName] : attempt.user, "validateLoginAttempt")
|
||||
return allowLogin;
|
||||
});
|
||||
const onLoginStopper = Accounts.onLogin(attempt =>
|
||||
test.isUndefined(attempt.user[ignoreFieldName], "onLogin")
|
||||
);
|
||||
const onLogoutStopper = Accounts.onLogout(logoutContext =>
|
||||
test.isUndefined(logoutContext.user[ignoreFieldName], "onLogout")
|
||||
);
|
||||
const onLoginFailureStopper = Accounts.onLoginFailure(attempt =>
|
||||
test.isUndefined(allowLogin != 'bogus' ? attempt.user[ignoreFieldName] : attempt.user, "onLoginFailure")
|
||||
);
|
||||
|
||||
const conn = DDP.connect(Meteor.absoluteUrl());
|
||||
|
||||
// test a new connection
|
||||
let allowLogin = true;
|
||||
conn.call('login', { resume: stampedToken.token });
|
||||
|
||||
// Now that the user is logged in on the connection, Meteor.userId() should
|
||||
// return that user.
|
||||
conn.call('login', { resume: stampedToken.token });
|
||||
|
||||
// Trigger onLoginFailure callbacks, this will not include the user object
|
||||
allowLogin = 'bogus';
|
||||
test.throws(() => conn.call('login', { resume: "bogus" }), '403');
|
||||
|
||||
// test a forced login fail which WILL include the user object
|
||||
allowLogin = false;
|
||||
test.throws(() => conn.call('login', { resume: stampedToken.token }), '403');
|
||||
|
||||
// Trigger onLogout callbacks
|
||||
const onLogoutExpectedUserId = userId;
|
||||
conn.call('logout');
|
||||
|
||||
Accounts._options = options;
|
||||
conn.disconnect();
|
||||
validateStopper.stop();
|
||||
onLoginStopper.stop();
|
||||
onLogoutStopper.stop();
|
||||
onLoginFailureStopper.stop();
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
'accounts - Meteor.user() obeys options.defaultFieldSelector',
|
||||
test => {
|
||||
const ignoreFieldName = "bigArray";
|
||||
const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1] });
|
||||
const stampedToken = Accounts._generateStampedLoginToken();
|
||||
Accounts._insertLoginToken(userId, stampedToken);
|
||||
const options = Accounts._options;
|
||||
|
||||
// stub Meteor.userId() so it works outside methods and returns the correct user:
|
||||
const origAccountsUserId = Accounts.userId;
|
||||
Accounts.userId = () => userId;
|
||||
|
||||
Accounts._options = {};
|
||||
|
||||
// test the field is included by default
|
||||
let user = Meteor.user();
|
||||
test.isNotUndefined(user[ignoreFieldName], 'included by default');
|
||||
|
||||
// test the field is excluded
|
||||
Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}});
|
||||
user = Meteor.user();
|
||||
test.isUndefined(user[ignoreFieldName], 'excluded');
|
||||
user = Meteor.user({});
|
||||
test.isUndefined(user[ignoreFieldName], 'excluded {}');
|
||||
|
||||
// test the field can still be retrieved if required
|
||||
user = Meteor.user({fields: {[ignoreFieldName]: 1}});
|
||||
test.isNotUndefined(user[ignoreFieldName], 'field can be retrieved');
|
||||
test.isUndefined(user.username, 'field can be retrieved username');
|
||||
|
||||
// test a combined negative field specifier
|
||||
user = Meteor.user({fields: {username: 0}});
|
||||
test.isUndefined(user[ignoreFieldName], 'combined field selector');
|
||||
test.isUndefined(user.username, 'combined field selector username');
|
||||
|
||||
// test an explicit request for the full user object
|
||||
user = Meteor.user({fields: {}});
|
||||
test.isNotUndefined(user[ignoreFieldName], 'full selector');
|
||||
test.isNotUndefined(user.username, 'full selector username');
|
||||
|
||||
Accounts._options = options;
|
||||
Accounts.userId = origAccountsUserId;
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
'accounts - verify onExternalLogin hook can update oauth user profiles',
|
||||
test => {
|
||||
@@ -461,16 +573,24 @@ Tinytest.add(
|
||||
'facebook',
|
||||
{ id: facebookId },
|
||||
{ profile: { foo: 1 } },
|
||||
).id;
|
||||
).userId;
|
||||
const ignoreFieldName = "bigArray";
|
||||
const c = Meteor.users.update(uid1, {$set: {[ignoreFieldName]: [1]}});
|
||||
let users =
|
||||
Meteor.users.find({ 'services.facebook.id': facebookId }).fetch();
|
||||
test.length(users, 1);
|
||||
test.equal(users[0].profile.foo, 1);
|
||||
test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - before limit fields');
|
||||
|
||||
// Verify user profile data can be modified using the onExternalLogin
|
||||
// hook, for existing users.
|
||||
Accounts.onExternalLogin((options) => {
|
||||
// Also verify that the user object is filtered by _options.defaultFieldSelector
|
||||
const accountsOptions = Accounts._options;
|
||||
Accounts._options = {};
|
||||
Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}});
|
||||
Accounts.onExternalLogin((options, user) => {
|
||||
options.profile.foo = 2;
|
||||
test.isUndefined(users[ignoreFieldName], 'ignoreField - after limit fields');
|
||||
return options;
|
||||
});
|
||||
Accounts.updateOrCreateUserFromExternalService(
|
||||
@@ -478,9 +598,11 @@ Tinytest.add(
|
||||
{ id: facebookId },
|
||||
{ profile: { foo: 1 } },
|
||||
);
|
||||
// test.isUndefined(users[0][ignoreFieldName], 'ignoreField - fields limited');
|
||||
users = Meteor.users.find({ 'services.facebook.id': facebookId }).fetch();
|
||||
test.length(users, 1);
|
||||
test.equal(users[0].profile.foo, 2);
|
||||
test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - still there');
|
||||
|
||||
// Verify user profile data can be modified using the onExternalLogin
|
||||
// hook, for new users.
|
||||
@@ -489,7 +611,7 @@ Tinytest.add(
|
||||
'facebook',
|
||||
{ id: facebookId },
|
||||
{ profile: { foo: 3 } },
|
||||
).id;
|
||||
).userId;
|
||||
users = Meteor.users.find({ 'services.facebook.id': facebookId }).fetch();
|
||||
test.length(users, 1);
|
||||
test.equal(users[0].profile.foo, 2);
|
||||
@@ -498,5 +620,6 @@ Tinytest.add(
|
||||
Meteor.users.remove(uid1);
|
||||
Meteor.users.remove(uid2);
|
||||
Accounts._onExternalLoginHook = null;
|
||||
Accounts._options = accountsOptions;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ const bcryptHash = Meteor.wrapAsync(bcrypt.hash);
|
||||
const bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
|
||||
|
||||
// Utility for grabbing user
|
||||
const getUserById = id => Meteor.users.findOne(id);
|
||||
const getUserById = (id, options) => Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options));
|
||||
|
||||
// User records have a 'services.password.bcrypt' field on them to hold
|
||||
// their hashed passwords (unless they have a 'services.password.srp'
|
||||
@@ -73,6 +73,9 @@ const getRoundsFromBcryptHash = hash => {
|
||||
// properties `digest` and `algorithm` (in which case we bcrypt
|
||||
// `password.digest`).
|
||||
//
|
||||
// The user parameter needs at least user._id and user.services
|
||||
Accounts._checkPasswordUserFields = {_id: 1, services: 1},
|
||||
//
|
||||
Accounts._checkPassword = (user, password) => {
|
||||
const result = {
|
||||
userId: user._id
|
||||
@@ -120,12 +123,14 @@ const handleError = (msg, throwError = true) => {
|
||||
/// LOGIN
|
||||
///
|
||||
|
||||
Accounts._findUserByQuery = query => {
|
||||
Accounts._findUserByQuery = (query, options) => {
|
||||
let user = null;
|
||||
|
||||
if (query.id) {
|
||||
user = getUserById(query.id);
|
||||
// default field selector is added within getUserById()
|
||||
user = getUserById(query.id, options);
|
||||
} else {
|
||||
options = Accounts._addDefaultFieldSelector(options);
|
||||
let fieldName;
|
||||
let fieldValue;
|
||||
if (query.username) {
|
||||
@@ -139,11 +144,11 @@ Accounts._findUserByQuery = query => {
|
||||
}
|
||||
let selector = {};
|
||||
selector[fieldName] = fieldValue;
|
||||
user = Meteor.users.findOne(selector);
|
||||
user = Meteor.users.findOne(selector, options);
|
||||
// If user is not found, try a case insensitive lookup
|
||||
if (!user) {
|
||||
selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue);
|
||||
const candidateUsers = Meteor.users.find(selector).fetch();
|
||||
const candidateUsers = Meteor.users.find(selector, options).fetch();
|
||||
// No match if multiple candidates are found
|
||||
if (candidateUsers.length === 1) {
|
||||
user = candidateUsers[0];
|
||||
@@ -161,11 +166,13 @@ Accounts._findUserByQuery = query => {
|
||||
* insensitive search, it returns null.
|
||||
* @locus Server
|
||||
* @param {String} username The username to look for
|
||||
* @param {Object} [options]
|
||||
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
||||
* @returns {Object} A user if found, else null
|
||||
* @importFromPackage accounts-base
|
||||
*/
|
||||
Accounts.findUserByUsername =
|
||||
username => Accounts._findUserByQuery({ username });
|
||||
(username, options) => Accounts._findUserByQuery({ username }, options);
|
||||
|
||||
/**
|
||||
* @summary Finds the user with the specified email.
|
||||
@@ -174,10 +181,13 @@ Accounts.findUserByUsername =
|
||||
* insensitive search, it returns null.
|
||||
* @locus Server
|
||||
* @param {String} email The email address to look for
|
||||
* @param {Object} [options]
|
||||
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
||||
* @returns {Object} A user if found, else null
|
||||
* @importFromPackage accounts-base
|
||||
*/
|
||||
Accounts.findUserByEmail = email => Accounts._findUserByQuery({ email });
|
||||
Accounts.findUserByEmail =
|
||||
(email, options) => Accounts._findUserByQuery({ email }, options);
|
||||
|
||||
// Generates a MongoDB selector that can be used to perform a fast case
|
||||
// insensitive lookup for the given fieldName and string. Since MongoDB does
|
||||
@@ -230,7 +240,13 @@ const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, o
|
||||
|
||||
if (fieldValue && !skipCheck) {
|
||||
const matchedUsers = Meteor.users.find(
|
||||
selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch();
|
||||
selectorForFastCaseInsensitiveLookup(fieldName, fieldValue),
|
||||
{
|
||||
fields: {_id: 1},
|
||||
// we only need a maximum of 2 users for the logic below to work
|
||||
limit: 2,
|
||||
}
|
||||
).fetch();
|
||||
|
||||
if (matchedUsers.length > 0 &&
|
||||
// If we don't have a userId yet, any match we find is a duplicate
|
||||
@@ -289,7 +305,10 @@ Accounts.registerLoginHandler("password", options => {
|
||||
});
|
||||
|
||||
|
||||
const user = Accounts._findUserByQuery(options.user);
|
||||
const user = Accounts._findUserByQuery(options.user, {fields: {
|
||||
services: 1,
|
||||
...Accounts._checkPasswordUserFields,
|
||||
}});
|
||||
if (!user) {
|
||||
handleError("User not found");
|
||||
}
|
||||
@@ -358,7 +377,10 @@ Accounts.registerLoginHandler("password", options => {
|
||||
password: passwordValidator
|
||||
});
|
||||
|
||||
const user = Accounts._findUserByQuery(options.user);
|
||||
const user = Accounts._findUserByQuery(options.user, {fields: {
|
||||
services: 1,
|
||||
...Accounts._checkPasswordUserFields,
|
||||
}});
|
||||
if (!user) {
|
||||
handleError("User not found");
|
||||
}
|
||||
@@ -419,7 +441,9 @@ Accounts.setUsername = (userId, newUsername) => {
|
||||
check(userId, NonEmptyString);
|
||||
check(newUsername, NonEmptyString);
|
||||
|
||||
const user = getUserById(userId);
|
||||
const user = getUserById(userId, {fields: {
|
||||
username: 1,
|
||||
}});
|
||||
if (!user) {
|
||||
handleError("User not found");
|
||||
}
|
||||
@@ -465,7 +489,10 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
throw new Meteor.Error(401, "Must be logged in");
|
||||
}
|
||||
|
||||
const user = getUserById(this.userId);
|
||||
const user = getUserById(this.userId, {fields: {
|
||||
services: 1,
|
||||
...Accounts._checkPasswordUserFields,
|
||||
}});
|
||||
if (!user) {
|
||||
handleError("User not found");
|
||||
}
|
||||
@@ -523,7 +550,7 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
|
||||
options = { logout: true , ...options };
|
||||
|
||||
const user = getUserById(userId);
|
||||
const user = getUserById(userId, {fields: {_id: 1}});
|
||||
if (!user) {
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
}
|
||||
@@ -556,7 +583,7 @@ const pluckAddresses = (emails = []) => emails.map(email => email.address);
|
||||
Meteor.methods({forgotPassword: options => {
|
||||
check(options, {email: String});
|
||||
|
||||
const user = Accounts.findUserByEmail(options.email);
|
||||
const user = Accounts.findUserByEmail(options.email, {fields: {emails: 1}});
|
||||
if (!user) {
|
||||
handleError("User not found");
|
||||
}
|
||||
@@ -581,6 +608,8 @@ Meteor.methods({forgotPassword: options => {
|
||||
*/
|
||||
Accounts.generateResetToken = (userId, email, reason, extraTokenData) => {
|
||||
// Make sure the user exists, and email is one of their addresses.
|
||||
// Don't limit the fields in the user object since the user is returned
|
||||
// by the function and some other fields might be used elsewhere.
|
||||
const user = getUserById(userId);
|
||||
if (!user) {
|
||||
handleError("Can't find user");
|
||||
@@ -638,6 +667,8 @@ Accounts.generateResetToken = (userId, email, reason, extraTokenData) => {
|
||||
*/
|
||||
Accounts.generateVerificationToken = (userId, email, extraTokenData) => {
|
||||
// Make sure the user exists, and email is one of their addresses.
|
||||
// Don't limit the fields in the user object since the user is returned
|
||||
// by the function and some other fields might be used elsewhere.
|
||||
const user = getUserById(userId);
|
||||
if (!user) {
|
||||
handleError("Can't find user");
|
||||
@@ -782,8 +813,13 @@ Meteor.methods({resetPassword: function (...args) {
|
||||
check(token, String);
|
||||
check(newPassword, passwordValidator);
|
||||
|
||||
const user = Meteor.users.findOne({
|
||||
"services.password.reset.token": token});
|
||||
const user = Meteor.users.findOne(
|
||||
{"services.password.reset.token": token},
|
||||
{fields: {
|
||||
services: 1,
|
||||
emails: 1,
|
||||
}}
|
||||
);
|
||||
if (!user) {
|
||||
throw new Meteor.Error(403, "Token expired");
|
||||
}
|
||||
@@ -889,7 +925,12 @@ Meteor.methods({verifyEmail: function (...args) {
|
||||
check(token, String);
|
||||
|
||||
const user = Meteor.users.findOne(
|
||||
{'services.email.verificationTokens.token': token});
|
||||
{'services.email.verificationTokens.token': token},
|
||||
{fields: {
|
||||
services: 1,
|
||||
emails: 1,
|
||||
}}
|
||||
);
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "Verify email link expired");
|
||||
|
||||
@@ -948,7 +989,7 @@ Accounts.addEmail = (userId, newEmail, verified) => {
|
||||
verified = false;
|
||||
}
|
||||
|
||||
const user = getUserById(userId);
|
||||
const user = getUserById(userId, {fields: {emails: 1}});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
@@ -1030,7 +1071,7 @@ Accounts.removeEmail = (userId, email) => {
|
||||
check(userId, NonEmptyString);
|
||||
check(email, NonEmptyString);
|
||||
|
||||
const user = getUserById(userId);
|
||||
const user = getUserById(userId, {fields: {_id: 1}});
|
||||
if (!user)
|
||||
throw new Meteor.Error(403, "User not found");
|
||||
|
||||
|
||||
@@ -1700,10 +1700,12 @@ if (Meteor.isServer) (() => {
|
||||
)
|
||||
|
||||
// We should be able to change the username
|
||||
Tinytest.add("passwords - change username", test => {
|
||||
Tinytest.add("passwords - change username & findUserByUsername", test => {
|
||||
const username = Random.id();
|
||||
const ignoreFieldName = "profile";
|
||||
const userId = Accounts.createUser({
|
||||
username: username
|
||||
username,
|
||||
[ignoreFieldName]: {name: 'foo'},
|
||||
});
|
||||
|
||||
test.isTrue(userId);
|
||||
@@ -1714,7 +1716,27 @@ if (Meteor.isServer) (() => {
|
||||
test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername);
|
||||
|
||||
// Test findUserByUsername as well while we're here
|
||||
test.equal(Accounts.findUserByUsername(newUsername)._id, userId);
|
||||
let user = Accounts.findUserByUsername(newUsername);
|
||||
test.equal(user._id, userId, 'userId - ignore');
|
||||
test.isNotUndefined(user[ignoreFieldName], 'field - no ignore');
|
||||
|
||||
// Test default field selector
|
||||
const options = Accounts._options;
|
||||
Accounts._options = {defaultFieldSelector: {[ignoreFieldName]: 0}};
|
||||
user = Accounts.findUserByUsername(newUsername);
|
||||
test.equal(user.username, newUsername, 'username - default ignore');
|
||||
test.isUndefined(user[ignoreFieldName], 'field - default ignore');
|
||||
|
||||
// Test default field selector over-ride
|
||||
user = Accounts.findUserByUsername(newUsername, {
|
||||
fields: {
|
||||
[ignoreFieldName]: 1
|
||||
}
|
||||
});
|
||||
test.isUndefined(user.username, 'username - override');
|
||||
test.isNotUndefined(user[ignoreFieldName], 'field - override');
|
||||
|
||||
Accounts._options = options;
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - change username to a new one only differing " +
|
||||
@@ -1760,10 +1782,14 @@ if (Meteor.isServer) (() => {
|
||||
user2OriginalUsername);
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - add email", test => {
|
||||
Tinytest.add("passwords - add email & findUserByEmail", test => {
|
||||
const origEmail = `${Random.id()}@turing.com`;
|
||||
const username = Random.id();
|
||||
const ignoreFieldName = "profile";
|
||||
const userId = Accounts.createUser({
|
||||
email: origEmail
|
||||
email: origEmail,
|
||||
username,
|
||||
[ignoreFieldName]: {name: 'foo'},
|
||||
});
|
||||
|
||||
const newEmail = `${Random.id()}@turing.com`;
|
||||
@@ -1779,7 +1805,28 @@ if (Meteor.isServer) (() => {
|
||||
]);
|
||||
|
||||
// Test findUserByEmail as well while we're here
|
||||
test.equal(Accounts.findUserByEmail(origEmail)._id, userId);
|
||||
let user = Accounts.findUserByEmail(origEmail);
|
||||
test.equal(user._id, userId);
|
||||
test.isNotUndefined(user[ignoreFieldName], 'field - no ignore');
|
||||
|
||||
// Test default field selector
|
||||
const options = Accounts._options;
|
||||
Accounts._options = {defaultFieldSelector: {[ignoreFieldName]: 0}};
|
||||
user = Accounts.findUserByEmail(origEmail);
|
||||
test.equal(user.username, username, 'username - default ignore');
|
||||
test.isUndefined(user[ignoreFieldName], 'field - default ignore');
|
||||
|
||||
// Test default field selector over-ride
|
||||
user = Accounts.findUserByEmail(origEmail, {
|
||||
fields: {
|
||||
[ignoreFieldName]: 1
|
||||
}
|
||||
});
|
||||
test.equal(user._id, userId, 'userId - override');
|
||||
test.isUndefined(user.username, 'username - override');
|
||||
test.isNotUndefined(user[ignoreFieldName], 'field - override');
|
||||
|
||||
Accounts._options = options;
|
||||
});
|
||||
|
||||
Tinytest.add("passwords - add email when user has not an existing email", test => {
|
||||
|
||||
Reference in New Issue
Block a user