1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/accounts-password.js
2016-04-29 16:32:48 +02:00

1046 lines
122 KiB
JavaScript

(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var NpmModuleBcrypt = Package['npm-bcrypt'].NpmModuleBcrypt;
var Accounts = Package['accounts-base'].Accounts;
var AccountsServer = Package['accounts-base'].AccountsServer;
var SRP = Package.srp.SRP;
var SHA256 = Package.sha.SHA256;
var EJSON = Package.ejson.EJSON;
var DDP = Package['ddp-client'].DDP;
var DDPServer = Package['ddp-server'].DDPServer;
var Email = Package.email.Email;
var EmailInternals = Package.email.EmailInternals;
var Random = Package.random.Random;
var check = Package.check.check;
var Match = Package.check.Match;
var _ = Package.underscore._;
var ECMAScript = Package.ecmascript.ECMAScript;
var babelHelpers = Package['babel-runtime'].babelHelpers;
var Symbol = Package['ecmascript-runtime'].Symbol;
var Map = Package['ecmascript-runtime'].Map;
var Set = Package['ecmascript-runtime'].Set;
var Promise = Package.promise.Promise;
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-password/email_templates.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/** //
* @summary Options to customize emails sent from the Accounts system. //
* @locus Server //
*/ //
//
function greet(welcomeMsg) { // 6
return function (user, url) { // 7
var greeting = user.profile && user.profile.name ? "Hello " + user.profile.name + "," : "Hello,"; // 8
return greeting + "\n\n" + welcomeMsg + ", simply click the link below.\n\n" + url + "\n\nThanks.\n"; // 10
}; //
} //
//
Accounts.emailTemplates = { // 21
from: "Meteor Accounts <no-reply@meteor.com>", // 22
siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), // 23
//
resetPassword: { // 25
subject: function (user) { // 26
return "How to reset your password on " + Accounts.emailTemplates.siteName; // 27
}, //
text: function (user, url) { // 29
var greeting = user.profile && user.profile.name ? "Hello " + user.profile.name + "," : "Hello,"; // 30
return greeting + "\n\nTo reset your password, simply click the link below.\n\n" + url + "\n\nThanks.\n"; // 32
} //
}, //
verifyEmail: { // 42
subject: function (user) { // 43
return "How to verify email address on " + Accounts.emailTemplates.siteName; // 44
}, //
text: greet("To verify your account email") // 46
}, //
enrollAccount: { // 48
subject: function (user) { // 49
return "An account has been created for you on " + Accounts.emailTemplates.siteName; // 50
}, //
text: greet("To start using the service") // 52
} //
}; //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-password/password_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/// BCRYPT //
//
var bcrypt = NpmModuleBcrypt; // 3
var bcryptHash = Meteor.wrapAsync(bcrypt.hash); // 4
var bcryptCompare = Meteor.wrapAsync(bcrypt.compare); // 5
//
// User records have a 'services.password.bcrypt' field on them to hold //
// their hashed passwords (unless they have a 'services.password.srp' //
// field, in which case they will be upgraded to bcrypt the next time //
// they log in). //
// //
// When the client sends a password to the server, it can either be a //
// string (the plaintext password) or an object with keys 'digest' and //
// 'algorithm' (must be "sha-256" for now). The Meteor client always sends //
// password objects { digest: *, algorithm: "sha-256" }, but DDP clients //
// that don't have access to SHA can just send plaintext passwords as //
// strings. //
// //
// When the server receives a plaintext password as a string, it always //
// hashes it with SHA256 before passing it into bcrypt. When the server //
// receives a password as an object, it asserts that the algorithm is //
// "sha-256" and then passes the digest to bcrypt. //
//
Accounts._bcryptRounds = 10; // 25
//
// Given a 'password' from the client, extract the string that we should //
// bcrypt. 'password' can be one of: //
// - String (the plaintext password) //
// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256". //
// //
var getPasswordString = function (password) { // 32
if (typeof password === "string") { // 33
password = SHA256(password); // 34
} else { //
// 'password' is an object //
if (password.algorithm !== "sha-256") { // 36
throw new Error("Invalid password hash algorithm. " + "Only 'sha-256' is allowed."); // 37
} //
password = password.digest; // 40
} //
return password; // 42
}; //
//
// Use bcrypt to hash the password for storage in the database. //
// `password` can be a string (in which case it will be run through //
// SHA256 before bcrypt) or an object with properties `digest` and //
// `algorithm` (in which case we bcrypt `password.digest`). //
// //
var hashPassword = function (password) { // 50
password = getPasswordString(password); // 51
return bcryptHash(password, Accounts._bcryptRounds); // 52
}; //
//
// Check whether the provided password matches the bcrypt'ed password in //
// the database user record. `password` can be a string (in which case //
// it will be run through SHA256 before bcrypt) or an object with //
// properties `digest` and `algorithm` (in which case we bcrypt //
// `password.digest`). //
// //
Accounts._checkPassword = function (user, password) { // 61
var result = { // 62
userId: user._id // 63
}; //
//
password = getPasswordString(password); // 66
//
if (!bcryptCompare(password, user.services.password.bcrypt)) { // 68
result.error = new Meteor.Error(403, "Incorrect password"); // 69
} //
//
return result; // 72
}; //
var checkPassword = Accounts._checkPassword; // 74
//
/// //
/// LOGIN //
/// //
//
Accounts._findUserByQuery = function (query) { // 80
var user = null; // 81
//
if (query.id) { // 83
user = Meteor.users.findOne({ _id: query.id }); // 84
} else { //
var fieldName; // 86
var fieldValue; // 87
if (query.username) { // 88
fieldName = 'username'; // 89
fieldValue = query.username; // 90
} else if (query.email) { //
fieldName = 'emails.address'; // 92
fieldValue = query.email; // 93
} else { //
throw new Error("shouldn't happen (validation missed something)"); // 95
} //
var selector = {}; // 97
selector[fieldName] = fieldValue; // 98
user = Meteor.users.findOne(selector); // 99
// If user is not found, try a case insensitive lookup //
if (!user) { // 101
selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue); // 102
var candidateUsers = Meteor.users.find(selector).fetch(); // 103
// No match if multiple candidates are found //
if (candidateUsers.length === 1) { // 105
user = candidateUsers[0]; // 106
} //
} //
} //
//
return user; // 111
}; //
//
/** //
* @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) { // 123
return Accounts._findUserByQuery({ // 124
username: username // 125
}); //
}; //
//
/** //
* @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) { // 138
return Accounts._findUserByQuery({ // 139
email: email // 140
}); //
}; //
//
// Generates a MongoDB selector that can be used to perform a fast case //
// insensitive lookup for the given fieldName and string. Since MongoDB does //
// not support case insensitive indexes, and case insensitive regex queries //
// are slow, we construct a set of prefix selectors for all permutations of //
// the first 4 characters ourselves. We first attempt to matching against //
// these, and because 'prefix expression' regex queries do use indexes (see //
// http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use), //
// this has been found to greatly improve performance (from 1200ms to 5ms in a //
// test with 1.000.000 users). //
var selectorForFastCaseInsensitiveLookup = function (fieldName, string) { // 153
// Performance seems to improve up to 4 prefix characters //
var prefix = string.substring(0, Math.min(string.length, 4)); // 155
var orClause = _.map(generateCasePermutationsForString(prefix), function (prefixPermutation) { // 156
var selector = {}; // 158
selector[fieldName] = new RegExp('^' + Meteor._escapeRegExp(prefixPermutation)); // 159
return selector; // 161
}); //
var caseInsensitiveClause = {}; // 163
caseInsensitiveClause[fieldName] = new RegExp('^' + Meteor._escapeRegExp(string) + '$', 'i'); // 164
return { $and: [{ $or: orClause }, caseInsensitiveClause] }; // 166
}; //
//
// Generates permutations of all case variations of a given string. //
var generateCasePermutationsForString = function (string) { // 170
var permutations = ['']; // 171
for (var i = 0; i < string.length; i++) { // 172
var ch = string.charAt(i); // 173
permutations = _.flatten(_.map(permutations, function (prefix) { // 174
var lowerCaseChar = ch.toLowerCase(); // 175
var upperCaseChar = ch.toUpperCase(); // 176
// Don't add unneccesary permutations when ch is not a letter //
if (lowerCaseChar === upperCaseChar) { // 178
return [prefix + ch]; // 179
} else { //
return [prefix + lowerCaseChar, prefix + upperCaseChar]; // 181
} //
})); //
} //
return permutations; // 185
}; //
//
var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) { // 188
// Some tests need the ability to add users with the same case insensitive //
// value, hence the _skipCaseInsensitiveChecksForTest check //
var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); // 191
//
if (fieldValue && !skipCheck) { // 193
var matchedUsers = Meteor.users.find(selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch(); // 194
//
if (matchedUsers.length > 0 && ( // 197
// If we don't have a userId yet, any match we find is a duplicate //
!ownUserId || ( // 199
// Otherwise, check to see if there are multiple matches or a match //
// that is not us //
matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { // 202
throw new Meteor.Error(403, displayName + " already exists."); // 203
} //
} //
}; //
//
// XXX maybe this belongs in the check package //
var NonEmptyString = Match.Where(function (x) { // 209
check(x, String); // 210
return x.length > 0; // 211
}); //
//
var userQueryValidator = Match.Where(function (user) { // 214
check(user, { // 215
id: Match.Optional(NonEmptyString), // 216
username: Match.Optional(NonEmptyString), // 217
email: Match.Optional(NonEmptyString) // 218
}); //
if (_.keys(user).length !== 1) throw new Match.Error("User property must have exactly one field"); // 220
return true; // 222
}); //
//
var passwordValidator = Match.OneOf(String, { digest: String, algorithm: String }); // 225
//
// Handler to login with a password. //
// //
// The Meteor client sets options.password to an object with keys //
// 'digest' (set to SHA256(password)) and 'algorithm' ("sha-256"). //
// //
// For other DDP clients which don't have access to SHA, the handler //
// also accepts the plaintext password in options.password as a string. //
// //
// (It might be nice if servers could turn the plaintext password //
// option off. Or maybe it should be opt-in, not opt-out? //
// Accounts.config option?) //
// //
// Note that neither password option is secure without SSL. //
// //
Accounts.registerLoginHandler("password", function (options) { // 244
if (!options.password || options.srp) return undefined; // don't handle // 245
//
check(options, { // 248
user: userQueryValidator, // 249
password: passwordValidator // 250
}); //
//
var user = Accounts._findUserByQuery(options.user); // 254
if (!user) throw new Meteor.Error(403, "User not found"); // 255
//
if (!user.services || !user.services.password || !(user.services.password.bcrypt || user.services.password.srp)) throw new Meteor.Error(403, "User has no password set");
//
if (!user.services.password.bcrypt) { // 262
if (typeof options.password === "string") { // 263
// The client has presented a plaintext password, and the user is //
// not upgraded to bcrypt yet. We don't attempt to tell the client //
// to upgrade to bcrypt, because it might be a standalone DDP //
// client doesn't know how to do such a thing. //
var verifier = user.services.password.srp; // 268
var newVerifier = SRP.generateVerifier(options.password, { // 269
identity: verifier.identity, salt: verifier.salt }); // 270
//
if (verifier.verifier !== newVerifier.verifier) { // 272
return { // 273
userId: user._id, // 274
error: new Meteor.Error(403, "Incorrect password") // 275
}; //
} //
//
return { userId: user._id }; // 279
} else { //
// Tell the client to use the SRP upgrade process. //
throw new Meteor.Error(400, "old password format", EJSON.stringify({ // 282
format: 'srp', // 283
identity: user.services.password.srp.identity // 284
})); //
} //
} //
//
return checkPassword(user, options.password); // 289
}); //
//
// Handler to login using the SRP upgrade path. To use this login //
// handler, the client must provide: //
// - srp: H(identity + ":" + password) //
// - password: a string or an object with properties 'digest' and 'algorithm' //
// //
// We use `options.srp` to verify that the client knows the correct //
// password without doing a full SRP flow. Once we've checked that, we //
// upgrade the user to bcrypt and remove the SRP information from the //
// user document. //
// //
// The client ends up using this login handler after trying the normal //
// login handler (above), which throws an error telling the client to //
// try the SRP upgrade path. //
// //
// XXX COMPAT WITH 0.8.1.3 //
Accounts.registerLoginHandler("password", function (options) { // 310
if (!options.srp || !options.password) return undefined; // don't handle // 311
//
check(options, { // 314
user: userQueryValidator, // 315
srp: String, // 316
password: passwordValidator // 317
}); //
//
var user = Accounts._findUserByQuery(options.user); // 320
if (!user) throw new Meteor.Error(403, "User not found"); // 321
//
// Check to see if another simultaneous login has already upgraded //
// the user record to bcrypt. //
if (user.services && user.services.password && user.services.password.bcrypt) return checkPassword(user, options.password);
//
if (!(user.services && user.services.password && user.services.password.srp)) throw new Meteor.Error(403, "User has no password set");
//
var v1 = user.services.password.srp.verifier; // 332
var v2 = SRP.generateVerifier(null, { // 333
hashedIdentityAndPassword: options.srp, // 336
salt: user.services.password.srp.salt // 337
}).verifier; //
if (v1 !== v2) return { // 340
userId: user._id, // 342
error: new Meteor.Error(403, "Incorrect password") // 343
}; //
//
// Upgrade to bcrypt on successful login. //
var salted = hashPassword(options.password); // 347
Meteor.users.update(user._id, { // 348
$unset: { 'services.password.srp': 1 }, // 351
$set: { 'services.password.bcrypt': salted } // 352
}); //
//
return { userId: user._id }; // 356
}); //
//
/// //
/// 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) { // 372
check(userId, NonEmptyString); // 373
check(newUsername, NonEmptyString); // 374
//
var user = Meteor.users.findOne(userId); // 376
if (!user) throw new Meteor.Error(403, "User not found"); // 377
//
var oldUsername = user.username; // 380
//
// Perform a case insensitive check fro duplicates before update //
checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); // 383
//
Meteor.users.update({ _id: user._id }, { $set: { username: newUsername } }); // 385
//
// Perform another check after update, in case a matching user has been //
// inserted in the meantime //
try { // 389
checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); // 390
} catch (ex) { //
// Undo update if the check fails //
Meteor.users.update({ _id: user._id }, { $set: { username: oldUsername } }); // 393
throw ex; // 394
} //
}; //
//
// 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). //
// //
// XXX COMPAT WITH 0.8.1.3 //
// Like the login method, if the user hasn't been upgraded from SRP to //
// bcrypt yet, then this method will throw an 'old password format' //
// error. The client should call the SRP upgrade login handler and then //
// retry this method again. //
// //
// UNLIKE the login method, there is no way to avoid getting SRP upgrade //
// errors thrown. The reasoning for this is that clients using this //
// method directly will need to be updated anyway because we no longer //
// support the SRP flow that they would have been doing to use this //
// method previously. //
Meteor.methods({ changePassword: function (oldPassword, newPassword) { // 413
check(oldPassword, passwordValidator); // 414
check(newPassword, passwordValidator); // 415
//
if (!this.userId) throw new Meteor.Error(401, "Must be logged in"); // 417
//
var user = Meteor.users.findOne(this.userId); // 420
if (!user) throw new Meteor.Error(403, "User not found"); // 421
//
if (!user.services || !user.services.password || !user.services.password.bcrypt && !user.services.password.srp) throw new Meteor.Error(403, "User has no password set");
//
if (!user.services.password.bcrypt) { // 428
throw new Meteor.Error(400, "old password format", EJSON.stringify({ // 429
format: 'srp', // 430
identity: user.services.password.srp.identity // 431
})); //
} //
//
var result = checkPassword(user, oldPassword); // 435
if (result.error) throw result.error; // 436
//
var hashed = hashPassword(newPassword); // 439
//
// It would be better if this removed ALL existing tokens and replaced //
// the token for the current connection with a new one, but that would //
// be tricky, so we'll settle for just replacing all tokens other than //
// the one for the current connection. //
var currentToken = Accounts._getLoginToken(this.connection.id); // 445
Meteor.users.update({ _id: this.userId }, { // 446
$set: { 'services.password.bcrypt': hashed }, // 449
$pull: { // 450
'services.resume.loginTokens': { hashedToken: { $ne: currentToken } } // 451
}, //
$unset: { 'services.password.reset': 1 } // 453
}); //
//
return { passwordChanged: true }; // 457
} }); //
//
// Force change the users password. //
//
/** //
* @summary Forcibly change the password for a user. //
* @locus Server //
* @param {String} userId The id of the user to update. //
* @param {String} newPassword A new password for the user. //
* @param {Object} [options] //
* @param {Object} options.logout Logout all current connections with this userId (default: true) //
*/ //
Accounts.setPassword = function (userId, newPlaintextPassword, options) { // 471
options = _.extend({ logout: true }, options); // 472
//
var user = Meteor.users.findOne(userId); // 474
if (!user) throw new Meteor.Error(403, "User not found"); // 475
//
var update = { // 478
$unset: { // 479
'services.password.srp': 1, // XXX COMPAT WITH 0.8.1.3 // 480
'services.password.reset': 1 // 481
}, //
$set: { 'services.password.bcrypt': hashPassword(newPlaintextPassword) } // 483
}; //
//
if (options.logout) { // 486
update.$unset['services.resume.loginTokens'] = 1; // 487
} //
//
Meteor.users.update({ _id: user._id }, update); // 490
}; //
//
/// //
/// RESETTING VIA EMAIL //
/// //
//
// Method called by a user to request a password reset email. This is //
// the start of the reset process. //
Meteor.methods({ forgotPassword: function (options) { // 500
check(options, { email: String }); // 501
//
var user = Meteor.users.findOne({ "emails.address": options.email }); // 503
if (!user) throw new Meteor.Error(403, "User not found"); // 504
//
Accounts.sendResetPasswordEmail(user._id, options.email); // 507
} }); //
//
// send the user an email with a link that when opened allows the user //
// to set a new password, without the old password. //
//
/** //
* @summary Send an email with a link the user can use to reset their password. //
* @locus Server //
* @param {String} userId The id of the user to send email to. //
* @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list.
*/ //
Accounts.sendResetPasswordEmail = function (userId, email) { // 519
// Make sure the user exists, and email is one of their addresses. //
var user = Meteor.users.findOne(userId); // 521
if (!user) throw new Error("Can't find user"); // 522
// pick the first email if we weren't passed an email. //
if (!email && user.emails && user.emails[0]) email = user.emails[0].address; // 525
// make sure we have a valid email //
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) throw new Error("No such email for user.");
//
var token = Random.secret(); // 531
var when = new Date(); // 532
var tokenRecord = { // 533
token: token, // 534
email: email, // 535
when: when // 536
}; //
Meteor.users.update(userId, { $set: { // 538
"services.password.reset": tokenRecord // 539
} }); //
// before passing to template, update user object with new token //
Meteor._ensure(user, 'services', 'password').reset = tokenRecord; // 542
//
var resetPasswordUrl = Accounts.urls.resetPassword(token); // 544
//
var options = { // 546
to: email, // 547
from: Accounts.emailTemplates.resetPassword.from ? Accounts.emailTemplates.resetPassword.from(user) : Accounts.emailTemplates.from,
subject: Accounts.emailTemplates.resetPassword.subject(user) // 551
}; //
//
if (typeof Accounts.emailTemplates.resetPassword.text === 'function') { // 554
options.text = Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl); // 555
} //
//
if (typeof Accounts.emailTemplates.resetPassword.html === 'function') options.html = Accounts.emailTemplates.resetPassword.html(user, resetPasswordUrl);
//
if (typeof Accounts.emailTemplates.headers === 'object') { // 563
options.headers = Accounts.emailTemplates.headers; // 564
} //
//
Email.send(options); // 567
}; //
//
// send the user an email informing them that their account was created, with //
// a link that when opened both marks their email as verified and forces them //
// to choose their password. The email must be one of the addresses in the //
// user's emails field, or undefined to pick the first email automatically. //
// //
// This is not called automatically. It must be called manually if you //
// want to use enrollment emails. //
//
/** //
* @summary Send an email with a link the user can use to set their initial password. //
* @locus Server //
* @param {String} userId The id of the user to send email to. //
* @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list.
*/ //
Accounts.sendEnrollmentEmail = function (userId, email) { // 584
// XXX refactor! This is basically identical to sendResetPasswordEmail. //
//
// Make sure the user exists, and email is in their addresses. //
var user = Meteor.users.findOne(userId); // 588
if (!user) throw new Error("Can't find user"); // 589
// pick the first email if we weren't passed an email. //
if (!email && user.emails && user.emails[0]) email = user.emails[0].address; // 592
// make sure we have a valid email //
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email)) throw new Error("No such email for user.");
//
var token = Random.secret(); // 598
var when = new Date(); // 599
var tokenRecord = { // 600
token: token, // 601
email: email, // 602
when: when // 603
}; //
Meteor.users.update(userId, { $set: { // 605
"services.password.reset": tokenRecord // 606
} }); //
//
// before passing to template, update user object with new token //
Meteor._ensure(user, 'services', 'password').reset = tokenRecord; // 610
//
var enrollAccountUrl = Accounts.urls.enrollAccount(token); // 612
//
var options = { // 614
to: email, // 615
from: Accounts.emailTemplates.enrollAccount.from ? Accounts.emailTemplates.enrollAccount.from(user) : Accounts.emailTemplates.from,
subject: Accounts.emailTemplates.enrollAccount.subject(user) // 619
}; //
//
if (typeof Accounts.emailTemplates.enrollAccount.text === 'function') { // 622
options.text = Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl); // 623
} //
//
if (typeof Accounts.emailTemplates.enrollAccount.html === 'function') options.html = Accounts.emailTemplates.enrollAccount.html(user, enrollAccountUrl);
//
if (typeof Accounts.emailTemplates.headers === 'object') { // 631
options.headers = Accounts.emailTemplates.headers; // 632
} //
//
Email.send(options); // 635
}; //
//
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change //
// the users password, and log them in. //
Meteor.methods({ resetPassword: function (token, newPassword) { // 641
var self = this; // 642
return Accounts._loginMethod(self, "resetPassword", arguments, "password", function () { // 643
check(token, String); // 649
check(newPassword, passwordValidator); // 650
//
var user = Meteor.users.findOne({ // 652
"services.password.reset.token": token }); // 653
if (!user) throw new Meteor.Error(403, "Token expired"); // 654
var email = user.services.password.reset.email; // 656
if (!_.include(_.pluck(user.emails || [], 'address'), email)) return { // 657
userId: user._id, // 659
error: new Meteor.Error(403, "Token has invalid email address") // 660
}; //
//
var hashed = hashPassword(newPassword); // 663
//
// NOTE: We're about to invalidate tokens on the user, who we might be //
// logged in as. Make sure to avoid logging ourselves out if this //
// happens. But also make sure not to leave the connection in a state //
// of having a bad token set if things fail. //
var oldToken = Accounts._getLoginToken(self.connection.id); // 669
Accounts._setLoginToken(user._id, self.connection, null); // 670
var resetToOldToken = function () { // 671
Accounts._setLoginToken(user._id, self.connection, oldToken); // 672
}; //
//
try { // 675
// Update the user record by: //
// - Changing the password to the new one //
// - Forgetting about the reset token that was just used //
// - Verifying their email, since they got the password reset via email. //
var affectedRecords = Meteor.users.update({ // 680
_id: user._id, // 682
'emails.address': email, // 683
'services.password.reset.token': token // 684
}, { $set: { 'services.password.bcrypt': hashed, //
'emails.$.verified': true }, // 687
$unset: { 'services.password.reset': 1, // 688
'services.password.srp': 1 } }); // 689
if (affectedRecords !== 1) return { // 690
userId: user._id, // 692
error: new Meteor.Error(403, "Invalid email") // 693
}; //
} catch (err) { //
resetToOldToken(); // 696
throw err; // 697
} //
//
// Replace all valid login tokens with new ones (changing //
// password should invalidate existing sessions). //
Accounts._clearAllLoginTokens(user._id); // 702
//
return { userId: user._id }; // 704
}); //
} }); //
//
/// //
/// EMAIL VERIFICATION //
/// //
//
// send the user an email with a link that when opened marks that //
// address as verified //
//
/** //
* @summary Send an email with a link the user can use verify their email address. //
* @locus Server //
* @param {String} userId The id of the user to send email to. //
* @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first unverified email in the list.
*/ //
Accounts.sendVerificationEmail = function (userId, address) { // 723
// XXX Also generate a link using which someone can delete this //
// account if they own said address but weren't those who created //
// this account. //
//
// Make sure the user exists, and address is one of their addresses. //
var user = Meteor.users.findOne(userId); // 729
if (!user) throw new Error("Can't find user"); // 730
// pick the first unverified address if we weren't passed an address. //
if (!address) { // 733
var email = _.find(user.emails || [], function (e) { // 734
return !e.verified; // 735
}); //
address = (email || {}).address; // 736
} //
// make sure we have a valid address //
if (!address || !_.contains(_.pluck(user.emails || [], 'address'), address)) throw new Error("No such email address for user.");
//
var tokenRecord = { // 743
token: Random.secret(), // 744
address: address, // 745
when: new Date() }; // 746
Meteor.users.update({ _id: userId }, { $push: { 'services.email.verificationTokens': tokenRecord } }); // 747
//
// before passing to template, update user object with new token //
Meteor._ensure(user, 'services', 'email'); // 752
if (!user.services.email.verificationTokens) { // 753
user.services.email.verificationTokens = []; // 754
} //
user.services.email.verificationTokens.push(tokenRecord); // 756
//
var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token); // 758
//
var options = { // 760
to: address, // 761
from: Accounts.emailTemplates.verifyEmail.from ? Accounts.emailTemplates.verifyEmail.from(user) : Accounts.emailTemplates.from,
subject: Accounts.emailTemplates.verifyEmail.subject(user) // 765
}; //
//
if (typeof Accounts.emailTemplates.verifyEmail.text === 'function') { // 768
options.text = Accounts.emailTemplates.verifyEmail.text(user, verifyEmailUrl); // 769
} //
//
if (typeof Accounts.emailTemplates.verifyEmail.html === 'function') options.html = Accounts.emailTemplates.verifyEmail.html(user, verifyEmailUrl);
//
if (typeof Accounts.emailTemplates.headers === 'object') { // 777
options.headers = Accounts.emailTemplates.headers; // 778
} //
//
Email.send(options); // 781
}; //
//
// Take token from sendVerificationEmail, mark the email as verified, //
// and log them in. //
Meteor.methods({ verifyEmail: function (token) { // 786
var self = this; // 787
return Accounts._loginMethod(self, "verifyEmail", arguments, "password", function () { // 788
check(token, String); // 794
//
var user = Meteor.users.findOne({ 'services.email.verificationTokens.token': token }); // 796
if (!user) throw new Meteor.Error(403, "Verify email link expired"); // 798
//
var tokenRecord = _.find(user.services.email.verificationTokens, function (t) { // 801
return t.token == token; // 803
}); //
if (!tokenRecord) return { // 805
userId: user._id, // 807
error: new Meteor.Error(403, "Verify email link expired") // 808
}; //
//
var emailsRecord = _.find(user.emails, function (e) { // 811
return e.address == tokenRecord.address; // 812
}); //
if (!emailsRecord) return { // 814
userId: user._id, // 816
error: new Meteor.Error(403, "Verify email link is for unknown address") // 817
}; //
//
// By including the address in the query, we can use 'emails.$' in the //
// modifier to get a reference to the specific object in the emails //
// array. See //
// http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator) //
// http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull //
Meteor.users.update({ _id: user._id, // 825
'emails.address': tokenRecord.address }, { $set: { 'emails.$.verified': true }, // 827
$pull: { 'services.email.verificationTokens': { address: tokenRecord.address } } }); // 829
//
return { userId: user._id }; // 831
}); //
} }); //
//
/** //
* @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) { // 847
check(userId, NonEmptyString); // 848
check(newEmail, NonEmptyString); // 849
check(verified, Match.Optional(Boolean)); // 850
//
if (_.isUndefined(verified)) { // 852
verified = false; // 853
} //
//
var user = Meteor.users.findOne(userId); // 856
if (!user) throw new Meteor.Error(403, "User not found"); // 857
//
// 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'); // 868
//
var didUpdateOwnEmail = _.any(user.emails, function (email, index) { // 871
if (caseInsensitiveRegExp.test(email.address)) { // 872
Meteor.users.update({ // 873
_id: user._id, // 874
'emails.address': email.address // 875
}, { $set: { //
'emails.$.address': newEmail, // 877
'emails.$.verified': verified // 878
} }); //
return true; // 880
} //
//
return false; // 883
}); //
//
// 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) { // 893
return; // 894
} //
//
// Perform a case insensitive check for duplicates before update //
checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); // 898
//
Meteor.users.update({ // 900
_id: user._id // 901
}, { //
$addToSet: { // 903
emails: { // 904
address: newEmail, // 905
verified: verified // 906
} //
} //
}); //
//
// Perform another check after update, in case a matching user has been //
// inserted in the meantime //
try { // 913
checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); // 914
} catch (ex) { //
// Undo update if the check fails //
Meteor.users.update({ _id: user._id }, { $pull: { emails: { address: newEmail } } }); // 917
throw ex; // 919
} //
}; //
//
/** //
* @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) { // 930
check(userId, NonEmptyString); // 931
check(email, NonEmptyString); // 932
//
var user = Meteor.users.findOne(userId); // 934
if (!user) throw new Meteor.Error(403, "User not found"); // 935
//
Meteor.users.update({ _id: user._id }, { $pull: { emails: { address: email } } }); // 938
}; //
//
/// //
/// CREATING USERS //
/// //
//
// Shared createUser function called from the createUser method, both //
// if originates in client or server code. Calls user provided hooks, //
// does the actual user insertion. //
// //
// returns the user id //
var createUser = function (options) { // 951
// Unknown keys allowed, because a onCreateUserHook can take arbitrary //
// options. //
check(options, Match.ObjectIncluding({ // 954
username: Match.Optional(String), // 955
email: Match.Optional(String), // 956
password: Match.Optional(passwordValidator) // 957
})); //
//
var username = options.username; // 960
var email = options.email; // 961
if (!username && !email) throw new Meteor.Error(400, "Need to set a username or email"); // 962
//
var user = { services: {} }; // 965
if (options.password) { // 966
var hashed = hashPassword(options.password); // 967
user.services.password = { bcrypt: hashed }; // 968
} //
//
if (username) user.username = username; // 971
if (email) user.emails = [{ address: email, verified: false }]; // 973
//
// Perform a case insensitive check before insert //
checkForCaseInsensitiveDuplicates('username', 'Username', username); // 977
checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); // 978
//
var userId = Accounts.insertUserDoc(options, user); // 980
// Perform another check after insert, in case a matching user has been //
// inserted in the meantime //
try { // 983
checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); // 984
checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); // 985
} catch (ex) { //
// Remove inserted user if the check fails //
Meteor.users.remove(userId); // 988
throw ex; // 989
} //
return userId; // 991
}; //
//
// method for create user. Requests come from the client. //
Meteor.methods({ createUser: function (options) { // 995
var self = this; // 996
return Accounts._loginMethod(self, "createUser", arguments, "password", function () { // 997
// createUser() above does more checking. //
check(options, Object); // 1004
if (Accounts._options.forbidClientAccountCreation) return { // 1005
error: new Meteor.Error(403, "Signups forbidden") // 1007
}; //
//
// Create user. result contains id and token. //
var userId = createUser(options); // 1011
// safety belt. createUser is supposed to throw on error. send 500 error //
// instead of sending a verification email with empty userid. //
if (!userId) throw new Error("createUser failed to insert new user"); // 1014
//
// If `Accounts._options.sendVerificationEmail` is set, register //
// a token to verify the user's primary email, and send it to //
// that address. //
if (options.email && Accounts._options.sendVerificationEmail) Accounts.sendVerificationEmail(userId, options.email);
//
// client gets logged in as the new user afterwards. //
return { userId: userId }; // 1024
}); //
} }); //
//
// Create user directly on the server. //
// //
// Unlike the client version, this does not log you in as this user //
// after creation. //
// //
// returns userId or throws an error if it can't create //
// //
// XXX add another argument ("server options") that gets sent to onCreateUser, //
// 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, callback) { // 1041
options = _.clone(options); // 1042
//
// XXX allow an optional callback? //
if (callback) { // 1045
throw new Error("Accounts.createUser with callback not supported on the server yet."); // 1046
} //
//
return createUser(options); // 1049
}; //
//
/// //
/// PASSWORD-SPECIFIC INDEXES ON USERS //
/// //
Meteor.users._ensureIndex('services.email.verificationTokens.token', { unique: 1, sparse: 1 }); // 1055
Meteor.users._ensureIndex('services.password.reset.token', { unique: 1, sparse: 1 }); // 1057
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['accounts-password'] = {};
})();
//# sourceMappingURL=accounts-password.js.map