packages/fxa-content-server/app/scripts/views/complete_reset_password.js (184 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Cocktail from 'cocktail';
import Template from 'templates/complete_reset_password.mustache';
import _ from 'underscore';
import AuthErrors from '../lib/auth-errors';
import Notifier from '../lib/channels/notifier';
import Url from '../lib/url';
import AccountRecoveryVerificationInfo from '../models/verification/account-recovery';
import VerificationInfo from '../models/verification/reset-password';
import preventDefaultThen from './decorators/prevent_default_then';
import FormView from './form';
import FlowEventsMixin from './mixins/flow-events-mixin';
import PasswordMixin from './mixins/password-mixin';
import PasswordResetMixin from './mixins/password-reset-mixin';
import PasswordStrengthMixin from './mixins/password-strength-mixin';
import ResendMixin from './mixins/resend-mixin';
import ServiceMixin from './mixins/service-mixin';
const proto = FormView.prototype;
const View = FormView.extend({
template: Template,
className: 'complete-reset-password',
events: _.extend({}, FormView.prototype.events, {
'click #remember-password': preventDefaultThen('_navigateToSignin'),
}),
_navigateToSignin() {
this.navigate('signin');
},
initialize(options) {
options = options || {};
const searchParams = Url.searchParams(this.window.location.search);
this._verificationInfo = new VerificationInfo(searchParams);
const model = options.model;
// If this property is set, this will ensure that a regular password reset
// is preformed by *not* setting any `recoveryKeyId` data. Additionally,
// if we already have a valid accountResetToken, don't attempt to verify it
// again.
this.lostRecoveryKey = model && model.get('lostRecoveryKey');
this.accountResetToken = model && model.get('accountResetToken');
if (this.lostRecoveryKey) {
return;
}
// If the complete password screen was navigated from the account recovery confirm
// key view, then these properties must be set in order to recover the account
// using the account recovery key.
if (model && model.get('recoveryKeyId')) {
this._accountRecoveryVerficationInfo =
new AccountRecoveryVerificationInfo(model.toJSON());
}
},
getAccount() {
const email = this._verificationInfo.get('email');
const accountResetToken = this.accountResetToken;
return this.user.initAccount({ email, accountResetToken });
},
// beforeRender is asynchronous and returns a promise. Only render
// after beforeRender has finished its business.
beforeRender() {
this.logViewEvent('verification.clicked');
const verificationInfo = this._verificationInfo;
if (!verificationInfo.isValid()) {
// One or more parameters fails validation. Abort and show an
// error message before doing any more checks.
this.logError(AuthErrors.toError('DAMAGED_VERIFICATION_LINK'));
return;
}
const account = this.getAccount();
const token = verificationInfo.get('token');
return account.isPasswordResetComplete(token).then((isComplete) => {
if (this._accountRecoveryVerficationInfo || this.lostRecoveryKey) {
return;
}
if (isComplete && !this._accountRecoveryVerficationInfo) {
verificationInfo.markExpired();
this.logError(AuthErrors.toError('EXPIRED_VERIFICATION_LINK'));
return;
}
// When the user clicks the confirm password reset link from their
// email, we should check to see if they have an account recovery key.
// If so, navigate to the confirm account recovery key view, else continue with
// a regular password reset.
return account.checkRecoveryKeyExistsByEmail().then((result) => {
if (result.exists) {
return this.navigate('account_recovery_confirm_key');
}
});
});
},
afterVisible() {
// The originating tab will start listening for `login` events once
// it knows the complete reset password tab is open in the same browser.
this.notifier.triggerRemote(Notifier.COMPLETE_RESET_PASSWORD_TAB_OPEN);
return proto.afterVisible.call(this);
},
setInitialContext(context) {
const verificationInfo = this._verificationInfo;
const doesLinkValidate = verificationInfo.isValid();
const isLinkExpired = verificationInfo.isExpired();
let showSyncWarning = this.relier.get('resetPasswordConfirm');
const showAccountRecoveryInfo = !!this._accountRecoveryVerficationInfo;
if (showAccountRecoveryInfo) {
// Don't show the sync warning if the user is resetting password with
// account recovery
showSyncWarning = false;
}
// damaged and expired links have special messages.
context.set({
email: verificationInfo.get('email'),
isLinkDamaged: !doesLinkValidate,
isLinkExpired: isLinkExpired,
isLinkValid: doesLinkValidate && !isLinkExpired,
showAccountRecoveryInfo: showAccountRecoveryInfo,
showSyncWarning: showSyncWarning,
});
},
isValidEnd() {
return this._getPassword() === this._getVPassword();
},
showValidationErrorsEnd() {
if (this._getPassword() !== this._getVPassword()) {
const err = AuthErrors.toError('PASSWORDS_DO_NOT_MATCH');
this.displayError(err);
}
},
submit() {
const verificationInfo = this._verificationInfo;
const password = this._getPassword();
const token = verificationInfo.get('token');
const code = verificationInfo.get('code');
const emailToHashWith = verificationInfo.get('emailToHashWith');
// If the user verifies in the same browser and the original tab
// is still open, we want the original tab to redirect back to
// the RP. The only way to do that is for this tab to get a
// sessionToken that can be used by the original tab. This tab
// will store the sessionToken in localStorage, when the
// reset password complete poll completes in the original tab,
// it will fetch the sessionToken from localStorage and go to town.
const account = this.getAccount();
return Promise.resolve()
.then(() => {
// The account recovery verification info will be set from the
// `confirm account recovery key` view. If the are not set, then perform
// a regular password reset.
const accountRecoveryVerificationInfo =
this._accountRecoveryVerficationInfo;
if (accountRecoveryVerificationInfo) {
return this.user.completeAccountPasswordResetWithRecoveryKey(
account,
password,
accountRecoveryVerificationInfo.get('accountResetToken'),
accountRecoveryVerificationInfo.get('recoveryKeyId'),
accountRecoveryVerificationInfo.get('kB'),
this.relier,
emailToHashWith
);
}
return this.user.completeAccountPasswordReset(
account,
password,
token,
code,
this.relier,
emailToHashWith
);
})
.then((updatedAccount) => {
// The password was reset, future attempts should ask confirmation.
this.relier.set('resetPasswordConfirm', true);
// See the above note about notifying the original tab.
this.logViewEvent('verification.success');
return this.invokeBrokerMethod(
'afterCompleteResetPassword',
updatedAccount
);
})
.then(() => {
const accountRecoveryVerificationInfo =
this._accountRecoveryVerficationInfo;
if (!accountRecoveryVerificationInfo) {
this.navigate('reset_password_verified');
} else {
this.metrics.logUserPreferences('account-recovery', false);
this.logFlowEventOnce('recovery-key-consume.success', this.viewName);
this.navigate('reset_password_with_recovery_key_verified');
}
})
.catch((err) => {
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
this.logError(err);
delete this._accountRecoveryVerficationInfo;
// The token has expired since the first check, re-render to
// show a view that allows the user to receive a new link.
return this.render();
}
// all other errors are unexpected, bail.
throw err;
});
},
_getPassword() {
return this.$('#password').val();
},
_getVPassword() {
return this.$('#vpassword').val();
},
resend() {
return this.resetPassword(this._verificationInfo.get('email'));
},
});
Cocktail.mixin(
View,
FlowEventsMixin,
PasswordMixin,
PasswordResetMixin,
PasswordStrengthMixin({
balloonEl: '#password-strength-balloon-container',
passwordEl: '#password',
}),
ResendMixin(),
ServiceMixin
);
export default View;