* adds resetTokenValidityDuration setting * adds a validator to validate password that can be used to enforce strong passwords * adds unit tests for passwordPolicy.validator * adds unit tests to to fail reset password function if password is not in a valid format * updates README.md for passwordPolicy * prevents duplicate check for password validator in updateUserPassword * adds optional setting to disallow username in password * updates test cases to use fdescribe instead of describe * updates test cases to use request-promise instead of request * adds ability to use a RegExp or Callback function or both for a passwordPolicy.validator * expect username parameter in redirect to password_reset_success * adds support for _perishable_token_expires_at in postgres
221 lines
6.8 KiB
JavaScript
221 lines
6.8 KiB
JavaScript
import { randomString } from '../cryptoUtils';
|
||
import { inflate } from '../triggers';
|
||
import AdaptableController from './AdaptableController';
|
||
import MailAdapter from '../Adapters/Email/MailAdapter';
|
||
import rest from '../rest';
|
||
|
||
var RestWrite = require('../RestWrite');
|
||
var RestQuery = require('../RestQuery');
|
||
var hash = require('../password').hash;
|
||
var Auth = require('../Auth');
|
||
|
||
export class UserController extends AdaptableController {
|
||
|
||
constructor(adapter, appId, options = {}) {
|
||
super(adapter, appId, options);
|
||
}
|
||
|
||
validateAdapter(adapter) {
|
||
// Allow no adapter
|
||
if (!adapter && !this.shouldVerifyEmails) {
|
||
return;
|
||
}
|
||
super.validateAdapter(adapter);
|
||
}
|
||
|
||
expectedAdapterType() {
|
||
return MailAdapter;
|
||
}
|
||
|
||
get shouldVerifyEmails() {
|
||
return this.options.verifyUserEmails;
|
||
}
|
||
|
||
setEmailVerifyToken(user) {
|
||
if (this.shouldVerifyEmails) {
|
||
user._email_verify_token = randomString(25);
|
||
user.emailVerified = false;
|
||
|
||
if (this.config.emailVerifyTokenValidityDuration) {
|
||
user._email_verify_token_expires_at = Parse._encode(this.config.generateEmailVerifyTokenExpiresAt());
|
||
}
|
||
}
|
||
}
|
||
|
||
verifyEmail(username, token) {
|
||
if (!this.shouldVerifyEmails) {
|
||
// Trying to verify email when not enabled
|
||
// TODO: Better error here.
|
||
throw undefined;
|
||
}
|
||
|
||
let query = {username: username, _email_verify_token: token};
|
||
let updateFields = { emailVerified: true, _email_verify_token: {__op: 'Delete'}};
|
||
|
||
// if the email verify token needs to be validated then
|
||
// add additional query params and additional fields that need to be updated
|
||
if (this.config.emailVerifyTokenValidityDuration) {
|
||
query.emailVerified = false;
|
||
query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) };
|
||
|
||
updateFields._email_verify_token_expires_at = {__op: 'Delete'};
|
||
}
|
||
|
||
return this.config.database.update('_User', query, updateFields).then((document) => {
|
||
if (!document) {
|
||
throw undefined;
|
||
}
|
||
return Promise.resolve(document);
|
||
});
|
||
}
|
||
|
||
checkResetTokenValidity(username, token) {
|
||
return this.config.database.find('_User', {
|
||
username: username,
|
||
_perishable_token: token
|
||
}, {limit: 1}).then(results => {
|
||
if (results.length != 1) {
|
||
throw undefined;
|
||
}
|
||
|
||
if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
|
||
let expiresDate = results[0]._perishable_token_expires_at;
|
||
if (expiresDate && expiresDate.__type == 'Date') {
|
||
expiresDate = new Date(expiresDate.iso);
|
||
}
|
||
if (expiresDate < new Date())
|
||
throw 'The password reset link has expired';
|
||
}
|
||
|
||
return results[0];
|
||
});
|
||
}
|
||
|
||
getUserIfNeeded(user) {
|
||
if (user.username && user.email) {
|
||
return Promise.resolve(user);
|
||
}
|
||
var where = {};
|
||
if (user.username) {
|
||
where.username = user.username;
|
||
}
|
||
if (user.email) {
|
||
where.email = user.email;
|
||
}
|
||
|
||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||
return query.execute().then(function(result){
|
||
if (result.results.length != 1) {
|
||
throw undefined;
|
||
}
|
||
return result.results[0];
|
||
})
|
||
}
|
||
|
||
sendVerificationEmail(user) {
|
||
if (!this.shouldVerifyEmails) {
|
||
return;
|
||
}
|
||
const token = encodeURIComponent(user._email_verify_token);
|
||
// We may need to fetch the user in case of update email
|
||
this.getUserIfNeeded(user).then((user) => {
|
||
const username = encodeURIComponent(user.username);
|
||
let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
|
||
let options = {
|
||
appName: this.config.appName,
|
||
link: link,
|
||
user: inflate('_User', user),
|
||
};
|
||
if (this.adapter.sendVerificationEmail) {
|
||
this.adapter.sendVerificationEmail(options);
|
||
} else {
|
||
this.adapter.sendMail(this.defaultVerificationEmail(options));
|
||
}
|
||
});
|
||
}
|
||
|
||
setPasswordResetToken(email) {
|
||
const token = { _perishable_token: randomString(25) };
|
||
|
||
if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
|
||
token._perishable_token_expires_at = Parse._encode(this.config.generatePasswordResetTokenExpiresAt());
|
||
}
|
||
|
||
return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, token, {}, true)
|
||
}
|
||
|
||
sendPasswordResetEmail(email) {
|
||
if (!this.adapter) {
|
||
throw "Trying to send a reset password but no adapter is set";
|
||
// TODO: No adapter?
|
||
return;
|
||
}
|
||
|
||
return this.setPasswordResetToken(email)
|
||
.then(user => {
|
||
const token = encodeURIComponent(user._perishable_token);
|
||
const username = encodeURIComponent(user.username);
|
||
let link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
|
||
|
||
let options = {
|
||
appName: this.config.appName,
|
||
link: link,
|
||
user: inflate('_User', user),
|
||
};
|
||
|
||
if (this.adapter.sendPasswordResetEmail) {
|
||
this.adapter.sendPasswordResetEmail(options);
|
||
} else {
|
||
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
||
}
|
||
|
||
return Promise.resolve(user);
|
||
});
|
||
}
|
||
|
||
updatePassword(username, token, password, config) {
|
||
return this.checkResetTokenValidity(username, token)
|
||
.then(user => updateUserPassword(user.objectId, password, this.config))
|
||
// clear reset password token
|
||
.then(() => this.config.database.update('_User', {username}, {
|
||
_perishable_token: {__op: 'Delete'},
|
||
_perishable_token_expires_at: {__op: 'Delete'}
|
||
})).catch((error) => {
|
||
if (error.message) { // in case of Parse.Error, fail with the error message only
|
||
return Promise.reject(error.message);
|
||
} else {
|
||
return Promise.reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
defaultVerificationEmail({link, user, appName, }) {
|
||
let text = "Hi,\n\n" +
|
||
"You are being asked to confirm the e-mail address " + user.get("email") + " with " + appName + "\n\n" +
|
||
"" +
|
||
"Click here to confirm it:\n" + link;
|
||
let to = user.get("email");
|
||
let subject = 'Please verify your e-mail for ' + appName;
|
||
return { text, to, subject };
|
||
}
|
||
|
||
defaultResetPasswordEmail({link, user, appName, }) {
|
||
let text = "Hi,\n\n" +
|
||
"You requested to reset your password for " + appName + ".\n\n" +
|
||
"" +
|
||
"Click here to reset it:\n" + link;
|
||
let to = user.get("email") || user.get('username');
|
||
let subject = 'Password Reset for ' + appName;
|
||
return { text, to, subject };
|
||
}
|
||
}
|
||
|
||
// Mark this private
|
||
function updateUserPassword(userId, password, config) {
|
||
return rest.update(config, Auth.master(config), '_User', userId, {
|
||
password: password
|
||
});
|
||
}
|
||
|
||
export default UserController;
|