feat(session): Upgrade user session (#5626), r=@shane-tomlinson, @vladikoff
This commit is contained in:
Родитель
b01a496145
Коммит
04cff4e99f
|
@ -420,7 +420,11 @@ define(function (require, exports, module) {
|
|||
INVALID_WEB_CHANNEL: {
|
||||
errno: 1051,
|
||||
message: 'Browser not configured to accept WebChannel messages from this domain'
|
||||
}
|
||||
},
|
||||
REUSED_PRIMARY_EMAIL_VERIFICATION_CODE: {
|
||||
errno: 1052,
|
||||
message: t('That confirmation link was already used, and can only be used once.')
|
||||
},
|
||||
};
|
||||
/*eslint-enable sorting/sort-object-props*/
|
||||
|
||||
|
|
|
@ -365,6 +365,21 @@ define(function (require, exports, module) {
|
|||
return client.recoveryEmailResendCode(sessionToken, clientOptions);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Sends a verification code to the account's recovery email address
|
||||
* that will verify the current session.
|
||||
*
|
||||
* @param {String} sessionToken sessionToken obtained from signIn
|
||||
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
|
||||
*/
|
||||
sessionVerifyResend: withClient((client, sessionToken) => {
|
||||
const clientOptions = {
|
||||
type: 'upgradeSession'
|
||||
};
|
||||
|
||||
return client.recoveryEmailResendCode(sessionToken, clientOptions);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Destroy the user's current or custom session
|
||||
*
|
||||
|
@ -857,14 +872,6 @@ define(function (require, exports, module) {
|
|||
|
||||
deleteEmail: createClientDelegate('deleteEmail'),
|
||||
|
||||
/**
|
||||
* Responds with whether or not secondary emails is enabled for a user.
|
||||
*
|
||||
* @param {String} sessionToken User session token
|
||||
* @return {Promise} resolves when complete
|
||||
*/
|
||||
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled'),
|
||||
|
||||
/**
|
||||
* Set the new primary email address for a user.
|
||||
*
|
||||
|
|
|
@ -89,6 +89,7 @@ define(function (require, exports, module) {
|
|||
'oauth/force_auth(/)': createViewHandler(ForceAuthView),
|
||||
'oauth/signin(/)': createViewHandler(SignInView),
|
||||
'oauth/signup(/)': createViewHandler(SignUpView),
|
||||
'primary_email_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.PRIMARY_EMAIL_VERIFIED }),
|
||||
'report_signin(/)': createViewHandler(ReportSignInView),
|
||||
'reset_password(/)': createViewHandler(ResetPasswordView),
|
||||
'reset_password_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
|
||||
|
@ -120,6 +121,7 @@ define(function (require, exports, module) {
|
|||
'sms/sent(/)': createViewHandler(SmsSentView),
|
||||
'sms/why(/)': createChildViewHandler(WhyConnectAnotherDeviceView, SmsSendView),
|
||||
'verify_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SIGN_UP }),
|
||||
'verify_primary_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.PRIMARY_EMAIL_VERIFIED }),
|
||||
'verify_secondary_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SECONDARY_EMAIL_VERIFIED })
|
||||
},
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ define(function (require, exports, module) {
|
|||
return {
|
||||
FORCE_AUTH: 'force_auth',
|
||||
PASSWORD_RESET: 'reset_password',
|
||||
PRIMARY_EMAIL_VERIFIED: 'primary_email_verified',
|
||||
SECONDARY_EMAIL_VERIFIED: 'secondary_email_verified',
|
||||
SIGN_IN: 'login',
|
||||
SIGN_UP: 'signup'
|
||||
|
|
|
@ -324,31 +324,6 @@ define(function (require, exports, module) {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check to see if secondary emails is enabled for this user, using support email
|
||||
* domain and is in a verified session.
|
||||
*
|
||||
* @returns {Promise} Resolve to true/false
|
||||
*/
|
||||
recoveryEmailSecondaryEmailEnabled () {
|
||||
const sessionToken = this.get('sessionToken');
|
||||
if (! sessionToken) {
|
||||
return p.reject(AuthErrors.toError('INVALID_TOKEN'));
|
||||
}
|
||||
|
||||
return this._fxaClient.recoveryEmailSecondaryEmailEnabled(sessionToken)
|
||||
.then((resp) => {
|
||||
return resp.ok;
|
||||
}, (err) => {
|
||||
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||
// sessionToken is no longer valid, kill it.
|
||||
this.unset('sessionToken');
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
isSignedIn () {
|
||||
return this._fxaClient.isSignedIn(this.get('sessionToken'));
|
||||
},
|
||||
|
@ -552,6 +527,17 @@ define(function (require, exports, module) {
|
|||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Request to verify current session.
|
||||
*
|
||||
* @returns {Promise} - resolves when complete
|
||||
*/
|
||||
requestVerifySession () {
|
||||
return this._fxaClient.sessionVerifyResend(
|
||||
this.get('sessionToken')
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify the account using the verification code
|
||||
*
|
||||
|
|
|
@ -21,6 +21,7 @@ define(function (require, exports, module) {
|
|||
const SameBrowserVerificationModel = require('../verification/same-browser');
|
||||
const SearchParamMixin = require('../mixins/search-param');
|
||||
const SettingsIfSignedInBehavior = require('../../views/behaviors/settings');
|
||||
const t = (msg) => msg;
|
||||
const Vat = require('../../lib/vat');
|
||||
|
||||
const QUERY_PARAMETER_SCHEMA = {
|
||||
|
@ -64,8 +65,16 @@ define(function (require, exports, module) {
|
|||
*/
|
||||
defaultBehaviors: {
|
||||
afterChangePassword: new NullBehavior(),
|
||||
afterCompletePrimaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('primary_email_verified'), {
|
||||
// Upon verifying primary email, we want to reopen the emails panel to let user continue adding more
|
||||
// emails
|
||||
endpoint: 'settings/emails',
|
||||
success: t('Primary email verified successfully')
|
||||
}),
|
||||
afterCompleteResetPassword: new NullBehavior(),
|
||||
afterCompleteSecondaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('secondary_email_verified')),
|
||||
afterCompleteSecondaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('secondary_email_verified'), {
|
||||
success: t('Secondary email verified successfully')
|
||||
}),
|
||||
afterCompleteSignIn: new NavigateBehavior('signin_verified'),
|
||||
afterCompleteSignUp: new NavigateBehavior('signup_verified'),
|
||||
afterDeleteAccount: new NullBehavior(),
|
||||
|
@ -231,6 +240,17 @@ define(function (require, exports, module) {
|
|||
.then(() => this.getBehavior('afterCompleteSignIn'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after primary email verification, in the verification tab.
|
||||
*
|
||||
* @param {Object} account
|
||||
* @return {Promise}
|
||||
*/
|
||||
afterCompletePrimaryEmail (account) {
|
||||
return this.unpersistVerificationData(account)
|
||||
.then(() => this.getBehavior('afterCompletePrimaryEmail'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after secondary email verification, in the verification tab.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<div id="main-content" class="card">
|
||||
{{#isLinkUsed}}
|
||||
<header>
|
||||
<h1 id="fxa-verification-link-reused-header">{{#t}}Sign-in already confirmed{{/t}}</h1>
|
||||
<h1 id="fxa-verification-link-reused-header">
|
||||
{{#isPrimaryEmailVerification}}{{#t}}Primary email already confirmed{{/t}}{{/isPrimaryEmailVerification}}
|
||||
{{^isPrimaryEmailVerification}}{{#t}}Sign-in already confirmed{{/t}}{{/isPrimaryEmailVerification}}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
|
@ -14,7 +17,7 @@
|
|||
|
||||
</section>
|
||||
{{/isLinkUsed}}
|
||||
|
||||
|
||||
{{#isLinkExpired}}
|
||||
<header>
|
||||
<h1 id="fxa-verification-link-expired-header">{{#t}}Verification link expired{{/t}}</h1>
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
<p class="account-ready-service">{{#t}}You are now ready to use %(serviceName)s{{/t}}</p>
|
||||
{{/serviceName}}
|
||||
{{^serviceName}}
|
||||
{{#secondaryEmailVerified}}
|
||||
<p class="account-ready-service">{{#t}}Account notifications will now also be sent to %(secondaryEmailVerified)s{{/t}}</p>
|
||||
{{/secondaryEmailVerified}}
|
||||
{{^secondaryEmailVerified}}
|
||||
{{#emailVerified}}
|
||||
<p class="account-ready-service">{{{escapedEmailReadyText}}}</p>
|
||||
{{/emailVerified}}
|
||||
|
||||
{{^emailVerified}}
|
||||
<p class="account-ready-generic">{{#t}}Your account is ready!{{/t}}</p>
|
||||
{{/secondaryEmailVerified}}
|
||||
{{/emailVerified}}
|
||||
{{/serviceName}}
|
||||
{{/isSync}}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
|
||||
<div class="button-row">
|
||||
{{#hasSecondaryEmail}}
|
||||
<button class="settings-button email-refresh primary enabled">{{#t}}Refresh{{/t}}</button>
|
||||
<button type="submit" class="settings-button email-refresh primary enabled">{{#t}}Refresh{{/t}}</button>
|
||||
|
||||
<button
|
||||
class="settings-button {{#hasSecondaryVerifiedEmail}}cancel secondary enabled{{/hasSecondaryVerifiedEmail}}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<div id="upgrade-session" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}}">
|
||||
<div class="settings-unit-stub">
|
||||
<header class="settings-unit-summary">
|
||||
<h2 class="settings-unit-title">{{ title }}</h2>
|
||||
</header>
|
||||
<button class="settings-button primary settings-unit-toggle" data-href="{{ gatedHref }}">
|
||||
<span class="unlock-button">{{#t}}Unlock…{{/t}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-unit-details">
|
||||
<div class="error"></div>
|
||||
|
||||
<form novalidate>
|
||||
<p> {{ caption }} </p>
|
||||
<ul class="email-list button-row">
|
||||
<li class="email-address">
|
||||
<div class="address">{{ email }}</div>
|
||||
<div class="details">
|
||||
<div class="not-verified">{{#t}}verify to unlock section{{/t}}</div>
|
||||
</div>
|
||||
<div class="settings-button-group">
|
||||
<button class="settings-button warning send-verification-email" data-id="{{ email }}">
|
||||
{{#t}}Send{{/t}}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<a class="resend" data-id="{{ email }}">{{#t}}Not in inbox or spam folder? Resend{{/t}}</a>
|
||||
</ul>
|
||||
<div class="button-row">
|
||||
<button type="submit" class="settings-button primary refresh-verification-state">{{#t}}Refresh{{/t}}</button>
|
||||
<button class="settings-button secondary cancel">{{#t}}Done{{/t}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -11,16 +11,33 @@ define(function (require, exports, module) {
|
|||
'use strict';
|
||||
|
||||
const NavigateBehavior = require('../behaviors/navigate');
|
||||
|
||||
const t = (msg) => msg;
|
||||
const success = t('Account verified successfully');
|
||||
|
||||
module.exports = function (defaultBehavior) {
|
||||
/**
|
||||
* Creates navigation behavior that displays a success message
|
||||
* and redirects to settings.
|
||||
*
|
||||
* @param {Object} defaultBehavior - default behavior to invoke if not signed in
|
||||
* @param {Object} [options]
|
||||
* @param {String} [options.success] - success message when redirected
|
||||
* @param {String} [options.endpoint] - endpoint to redirect to
|
||||
* @return {Object} promise
|
||||
*/
|
||||
module.exports = function (defaultBehavior, options = {}) {
|
||||
const behavior = function (view, account) {
|
||||
return account.isSignedIn()
|
||||
.then((isSignedIn) => {
|
||||
if (isSignedIn) {
|
||||
return new NavigateBehavior('settings', { success });
|
||||
let success = t('Account verified successfully');
|
||||
let endpoint = 'settings';
|
||||
|
||||
if (options.success) {
|
||||
success = options.success;
|
||||
}
|
||||
if (options.endpoint) {
|
||||
endpoint = options.endpoint;
|
||||
}
|
||||
return new NavigateBehavior(endpoint, { success });
|
||||
}
|
||||
|
||||
return defaultBehavior;
|
||||
|
|
|
@ -73,6 +73,7 @@ define(function (require, exports, module) {
|
|||
|
||||
const code = verificationInfo.get('code');
|
||||
const options = {
|
||||
primaryEmailVerified: this.getSearchParam('primary_email_verified') || null,
|
||||
reminder: verificationInfo.get('reminder'),
|
||||
secondaryEmailVerified: this.getSearchParam('secondary_email_verified') || null,
|
||||
serverVerificationStatus: this.getSearchParam('server_verification') || null,
|
||||
|
@ -94,7 +95,8 @@ define(function (require, exports, module) {
|
|||
// If the link is invalid, print a special error message.
|
||||
isLinkDamaged: ! verificationInfo.isValid(),
|
||||
isLinkExpired: verificationInfo.isExpired(),
|
||||
isLinkUsed: verificationInfo.isUsed()
|
||||
isLinkUsed: verificationInfo.isUsed(),
|
||||
isPrimaryEmailVerification: this.isPrimaryEmail()
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -153,7 +155,9 @@ define(function (require, exports, module) {
|
|||
*/
|
||||
_getBrokerMethod () {
|
||||
let brokerMethod;
|
||||
if (this.isSecondaryEmail()) {
|
||||
if (this.isPrimaryEmail()) {
|
||||
brokerMethod = 'afterCompletePrimaryEmail';
|
||||
} else if (this.isSecondaryEmail()) {
|
||||
brokerMethod = 'afterCompleteSecondaryEmail';
|
||||
} else if (this.isSignIn()) {
|
||||
brokerMethod = 'afterCompleteSignIn';
|
||||
|
@ -182,10 +186,14 @@ define(function (require, exports, module) {
|
|||
AuthErrors.is(err, 'INVALID_VERIFICATION_CODE') ||
|
||||
AuthErrors.is(err, 'INVALID_PARAMETER')) {
|
||||
|
||||
// When coming from sign-in confirmation verification, show a
|
||||
// verification link expired error instead of damaged verification link.
|
||||
// This error is generated because the link has already been used.
|
||||
if (this.isSignIn()) {
|
||||
if (this.isPrimaryEmail()) {
|
||||
verificationInfo.markUsed();
|
||||
err = AuthErrors.toError('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE');
|
||||
} else if (this.isSignIn()) {
|
||||
// When coming from sign-in confirmation verification, show a
|
||||
// verification link expired error instead of damaged verification link.
|
||||
// This error is generated because the link has already been used.
|
||||
//
|
||||
// Disable resending verification, can only be triggered from new sign-in
|
||||
verificationInfo.markUsed();
|
||||
err = AuthErrors.toError('REUSED_SIGNIN_VERIFICATION_CODE');
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* View mixin to support a user upgrading their session to
|
||||
* be verified. This is useful in situations where a panel
|
||||
* might contain sensitive information or security related
|
||||
* features.
|
||||
*
|
||||
* This mix-in replaces the template loaded by the view with
|
||||
* the upgrade-session template. Once the email has been
|
||||
* verified, the page is re-rendered and the user can see
|
||||
* the gated panel.
|
||||
*
|
||||
* @mixin UpgradeSessionMixin
|
||||
*/
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
const BaseView = require('../base');
|
||||
const { preventDefaultThen } = BaseView;
|
||||
const SettingsPanelMixin = require('../mixins/settings-panel-mixin');
|
||||
const UpgradeSessionTemplate = require('stache!templates/settings/upgrade_session');
|
||||
const t = BaseView.t;
|
||||
|
||||
const showProgressIndicator = require('../decorators/progress_indicator');
|
||||
const EMAIL_REFRESH_SELECTOR = 'button.settings-button.refresh-verification-state';
|
||||
const EMAIL_REFRESH_DELAYMS = 350;
|
||||
|
||||
/**
|
||||
* The UpgradeSessionMixin can be configured to display different titles and captions
|
||||
* depending on what panel is being gated.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {String} [options.caption] - caption describing what the panel is unlocking
|
||||
* @param {String} [options.gatedHref] - location that is redirected after session is verified
|
||||
* @param {String} [options.title] - title name of the panel
|
||||
* @returns {Object} UpgradeSessionMixin
|
||||
*/
|
||||
module.exports = (options = {}) => {
|
||||
return {
|
||||
dependsOn: [SettingsPanelMixin],
|
||||
|
||||
events: {
|
||||
'click .refresh-verification-state': preventDefaultThen('_clickRefreshVerificationState'),
|
||||
'click .send-verification-email': preventDefaultThen('_clickSendVerificationEmail')
|
||||
},
|
||||
|
||||
initialize () {
|
||||
this.gatedTemplate = this.template;
|
||||
},
|
||||
|
||||
_clickRefreshVerificationState: showProgressIndicator(function() {
|
||||
this.model.set({
|
||||
isPanelOpen: true
|
||||
});
|
||||
return this.setupSessionGateIfRequired()
|
||||
.then((verified) => {
|
||||
if (verified) {
|
||||
this.displaySuccess(t('Primary email verified successfully'), {
|
||||
closePanel: false
|
||||
});
|
||||
}
|
||||
return this.render();
|
||||
});
|
||||
}, EMAIL_REFRESH_SELECTOR, EMAIL_REFRESH_DELAYMS),
|
||||
|
||||
_clickSendVerificationEmail () {
|
||||
const account = this.getSignedInAccount();
|
||||
return account.requestVerifySession(this.relier)
|
||||
.then(() => {
|
||||
this.displaySuccess(t('Verification email sent'), {
|
||||
closePanel: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setInitialContext (context) {
|
||||
context.set({
|
||||
caption: this.translate(options.caption),
|
||||
email: this.getSignedInAccount().get('email'),
|
||||
gatedHref: options.gatedHref,
|
||||
isPanelOpen: this.isPanelOpen(),
|
||||
title: this.translate(options.title)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if the current session is verified. If it is,
|
||||
* then it renders the original template, otherwise it renders
|
||||
* the upgrade-session template. This template prompts user
|
||||
* to verify their email address before they can see the original
|
||||
* template.
|
||||
*
|
||||
* @returns {Boolean} sessionVerified
|
||||
*/
|
||||
setupSessionGateIfRequired () {
|
||||
const account = this.getSignedInAccount();
|
||||
return account.sessionVerificationStatus()
|
||||
.then(({sessionVerified}) => {
|
||||
if (! sessionVerified) {
|
||||
this.template = UpgradeSessionTemplate;
|
||||
} else {
|
||||
this.template = this.gatedTemplate;
|
||||
}
|
||||
return sessionVerified;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -45,6 +45,15 @@ define(function (require, exports, module) {
|
|||
return this.model.get('type') === VerificationReasons.SIGN_UP;
|
||||
},
|
||||
|
||||
/**
|
||||
* Is a primary email being verified?
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isPrimaryEmail () {
|
||||
return this.model.get('type') === VerificationReasons.PRIMARY_EMAIL_VERIFIED;
|
||||
},
|
||||
|
||||
/**
|
||||
* Is a secondary email being verified?
|
||||
*
|
||||
|
|
|
@ -40,9 +40,15 @@ define(function (require, exports, module) {
|
|||
headerTitle: t('Password reset'),
|
||||
readyToSyncText: t('Complete set-up by entering the new password on your other Firefox devices.')
|
||||
},
|
||||
SECONDARY_EMAIL_VERIFIED: {
|
||||
PRIMARY_EMAIL_VERIFIED: {
|
||||
emailReadyText: t('You are now ready to make changes to your Firefox Account.'),
|
||||
headerId: 'fxa-sign-up-complete-header',
|
||||
headerTitle: t('Email verified')
|
||||
headerTitle: t('Primary email verified')
|
||||
},
|
||||
SECONDARY_EMAIL_VERIFIED: {
|
||||
emailReadyText: t('Account notifications will now also be sent to %(secondaryEmailVerified)s.'),
|
||||
headerId: 'fxa-sign-up-complete-header',
|
||||
headerTitle: t('Secondary email verified')
|
||||
},
|
||||
// signin_confirmed and signin_verified are only shown to Sync for now.
|
||||
SIGN_IN: {
|
||||
|
@ -69,6 +75,8 @@ define(function (require, exports, module) {
|
|||
|
||||
setInitialContext (context) {
|
||||
context.set({
|
||||
emailVerified: this.getSearchParam('secondary_email_verified') || this.getSearchParam('primary_email_verified'),
|
||||
escapedEmailReadyText: this._getEscapedEmailReadyText(),
|
||||
escapedHeaderTitle: this._getEscapedHeaderTitle(),
|
||||
escapedReadyToSyncText: this._getEscapedReadyToSyncText(),
|
||||
headerId: this._getHeaderId(),
|
||||
|
@ -102,6 +110,17 @@ define(function (require, exports, module) {
|
|||
const readyToSyncText = this._templateInfo.readyToSyncText;
|
||||
// translateInTemplate HTML escapes
|
||||
return this.translateInTemplate(readyToSyncText);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the HTML escaped "Email Ready" text.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
_getEscapedEmailReadyText () {
|
||||
const emailReadyText = this._templateInfo.emailReadyText;
|
||||
// translateInTemplate HTML escapes
|
||||
return this.translateInTemplate(emailReadyText);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ define(function (require, exports, module) {
|
|||
const FormView = require('../form');
|
||||
const preventDefaultThen = require('../base').preventDefaultThen;
|
||||
const SettingsPanelMixin = require('../mixins/settings-panel-mixin');
|
||||
const UpgradeSessionMixin = require('../mixins/upgrade-session-mixin');
|
||||
const SearchParamMixin = require('../../lib/search-param-mixin');
|
||||
const Strings = require('../../lib/strings');
|
||||
const showProgressIndicator = require('../decorators/progress_indicator');
|
||||
|
@ -37,7 +38,16 @@ define(function (require, exports, module) {
|
|||
'click .set-primary': preventDefaultThen('setPrimary')
|
||||
},
|
||||
|
||||
initialize (options) {
|
||||
beforeRender () {
|
||||
return this.setupSessionGateIfRequired()
|
||||
.then((isEnabled) => {
|
||||
if (isEnabled) {
|
||||
return this._fetchEmails();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
initialize (options = {}) {
|
||||
if (options.emails) {
|
||||
this._emails = options.emails;
|
||||
} else {
|
||||
|
@ -53,15 +63,10 @@ define(function (require, exports, module) {
|
|||
hasSecondaryEmail: this._hasSecondaryEmail(),
|
||||
hasSecondaryVerifiedEmail: this._hasSecondaryVerifiedEmail(),
|
||||
isPanelOpen: this.isPanelOpen(),
|
||||
newEmail: this.newEmail
|
||||
newEmail: this.newEmail,
|
||||
});
|
||||
},
|
||||
|
||||
beforeRender () {
|
||||
// Only show this view on verified session
|
||||
return this._isSecondaryEmailEnabled();
|
||||
},
|
||||
|
||||
afterRender () {
|
||||
// Panel should remain open if there are any unverified secondary emails
|
||||
if (this._hasSecondaryEmail() && ! this._hasSecondaryVerifiedEmail()) {
|
||||
|
@ -77,25 +82,6 @@ define(function (require, exports, module) {
|
|||
return false;
|
||||
},
|
||||
|
||||
_isSecondaryEmailEnabled () {
|
||||
// Only show secondary email panel if the user is in a verified session and feature is enabled.
|
||||
const account = this.getSignedInAccount();
|
||||
|
||||
return account.recoveryEmailSecondaryEmailEnabled()
|
||||
.then((isEnabled) => {
|
||||
if (! isEnabled) {
|
||||
return this.remove();
|
||||
}
|
||||
|
||||
// If we fail to fetch emails, then this user does not have this feature enabled
|
||||
// and we should not display this panel.
|
||||
return this._fetchEmails()
|
||||
.fail(() => {
|
||||
return this.remove();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_hasSecondaryEmail () {
|
||||
return this._emails.length > 1;
|
||||
},
|
||||
|
@ -177,6 +163,11 @@ define(function (require, exports, module) {
|
|||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
UpgradeSessionMixin({
|
||||
caption: t('A secondary email is an additional address for receiving security notices and confirming new Sync devices'),
|
||||
gatedHref: 'settings/emails',
|
||||
title: t('Secondary email')
|
||||
}),
|
||||
AvatarMixin,
|
||||
SettingsPanelMixin,
|
||||
FloatingPlaceholderMixin,
|
||||
|
|
|
@ -224,6 +224,17 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('afterCompletePrimaryEmail', () => {
|
||||
it('unpersists VerificationData, returns the expected behavior', function () {
|
||||
sinon.spy(broker, 'unpersistVerificationData');
|
||||
return broker.afterCompletePrimaryEmail(account)
|
||||
.then((behavior) => {
|
||||
assert.isTrue(broker.unpersistVerificationData.calledWith(account));
|
||||
assert.equal(behavior.type, 'settings');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterCompleteSecondaryEmail', function () {
|
||||
it('unpersist VerificationData, returns the expected behavior', function () {
|
||||
sinon.spy(broker, 'unpersistVerificationData');
|
||||
|
|
|
@ -209,7 +209,14 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: null,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: null,
|
||||
service: validService,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -229,7 +236,14 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: null});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: validReminder,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: null,
|
||||
service: null,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -250,7 +264,14 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: validReminder,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: null,
|
||||
service: validService,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -271,7 +292,14 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: 'verified', service: null, type: null});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: null,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: 'verified',
|
||||
service: null,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -292,7 +320,14 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: 'secondary'});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: null,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: null,
|
||||
service: null,
|
||||
type: 'secondary'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -314,7 +349,42 @@ define(function (require, exports, module) {
|
|||
var args = account.verifySignUp.getCall(0).args;
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: 'some@email.com', serverVerificationStatus: null, service: null, type: null});
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: null,
|
||||
reminder: null,
|
||||
secondaryEmailVerified: 'some@email.com',
|
||||
serverVerificationStatus: null,
|
||||
service: null,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if primary_email_verified is in the url', () => {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid +
|
||||
'&primary_email_verified=some@email.com';
|
||||
relier = new Relier({}, {
|
||||
window: windowMock
|
||||
});
|
||||
relier.fetch();
|
||||
initView(account);
|
||||
sinon.stub(view, '_notifyBrokerAndComplete').callsFake(() => p());
|
||||
return view.render();
|
||||
});
|
||||
|
||||
it('attempt to pass secondary_email_verified to verifySignUp', () => {
|
||||
const { args } = account.verifySignUp.getCall(0);
|
||||
assert.isTrue(account.verifySignUp.called);
|
||||
assert.ok(args[0]);
|
||||
assert.deepEqual(args[1], {
|
||||
primaryEmailVerified: 'some@email.com',
|
||||
reminder: null,
|
||||
secondaryEmailVerified: null,
|
||||
serverVerificationStatus: null,
|
||||
service: null,
|
||||
type: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -422,6 +492,34 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE error', () => {
|
||||
beforeEach(function () {
|
||||
verificationError = AuthErrors.toError('INVALID_VERIFICATION_CODE', 'this isn\'t a lottery');
|
||||
|
||||
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid;
|
||||
var model = new Backbone.Model();
|
||||
model.set('type', VerificationReasons.PRIMARY_EMAIL_VERIFIED);
|
||||
|
||||
view = new View({
|
||||
account: account,
|
||||
broker: broker,
|
||||
metrics: metrics,
|
||||
model: model,
|
||||
notifier: notifier,
|
||||
relier: relier,
|
||||
user: user,
|
||||
window: windowMock
|
||||
});
|
||||
|
||||
return view.render();
|
||||
});
|
||||
|
||||
it('displays the verification link expired screen', () => {
|
||||
assert.ok(view.$('#fxa-verification-link-reused-header').length);
|
||||
testErrorLogged(AuthErrors.toError('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('all other server errors', function () {
|
||||
beforeEach(function () {
|
||||
verificationError = AuthErrors.toError('UNEXPECTED_ERROR');
|
||||
|
@ -485,6 +583,12 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
describe('_getBrokerMethod', () => {
|
||||
it('works for primary email', () => {
|
||||
sinon.stub(view, 'isPrimaryEmail').callsFake(() => true);
|
||||
|
||||
assert.equal(view._getBrokerMethod(), 'afterCompletePrimaryEmail');
|
||||
});
|
||||
|
||||
it('works for secondary emails', () => {
|
||||
sinon.stub(view, 'isSecondaryEmail').callsFake(() => true);
|
||||
|
||||
|
|
|
@ -97,37 +97,26 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('feature disabled', () => {
|
||||
describe('for user', () => {
|
||||
describe('feature gated in unverified session', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'sessionVerificationStatus').callsFake(() => {
|
||||
return Promise.resolve({sessionVerified: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shows upgrade session', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'recoveryEmails').callsFake(() => {
|
||||
return p();
|
||||
});
|
||||
|
||||
sinon.stub(account, 'recoveryEmailSecondaryEmailEnabled').callsFake(() => {
|
||||
return p(false);
|
||||
});
|
||||
|
||||
view = new View({
|
||||
broker: broker,
|
||||
emails: emails,
|
||||
metrics: metrics,
|
||||
notifier: notifier,
|
||||
parentView: parentView,
|
||||
translator: translator,
|
||||
user: user,
|
||||
window: windowMock
|
||||
});
|
||||
|
||||
sinon.stub(view, 'remove').callsFake(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
return view.render();
|
||||
emails = [{
|
||||
email: 'primary@email.com',
|
||||
isPrimary: true,
|
||||
verified: true
|
||||
}];
|
||||
return initView();
|
||||
});
|
||||
|
||||
it('should be disabled when feature is disabled for user', () => {
|
||||
assert.equal(view.remove.callCount, 1);
|
||||
it('has upgrade session panel', () => {
|
||||
assert.ok(view.$('.email-address .address').length, 1);
|
||||
assert.equal(view.$('.email-address .address').text(), email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -135,11 +124,11 @@ define(function (require, exports, module) {
|
|||
describe('feature enabled', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'recoveryEmails').callsFake(() => {
|
||||
return p(emails);
|
||||
return Promise.resolve(emails);
|
||||
});
|
||||
|
||||
sinon.stub(account, 'recoveryEmailSecondaryEmailEnabled').callsFake(() => {
|
||||
return p(true);
|
||||
sinon.stub(account, 'sessionVerificationStatus').callsFake(() => {
|
||||
return Promise.resolve({ sessionVerified: true });
|
||||
});
|
||||
|
||||
sinon.stub(account, 'recoveryEmailDestroy').callsFake(() => {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"cocktail": "0.5.9",
|
||||
"easteregg": "https://github.com/mozilla/fxa-easter-egg.git#ab20cd517cf8ae9feee115e48745189d28e13bc3",
|
||||
"fxa-checkbox": "mozilla/fxa-checkbox#7f856afffd394a144f718e28e6fb79092d6ccddd",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.65",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.66",
|
||||
"html5shiv": "3.7.2",
|
||||
"jquery": "3.1.0",
|
||||
"jquery-modal": "https://github.com/shane-tomlinson/jquery-modal.git#0576775d1b4590314b114386019f4c7421c77503",
|
||||
|
|
|
@ -23,6 +23,7 @@ module.exports = function () {
|
|||
'oauth/force_auth',
|
||||
'oauth/signin',
|
||||
'oauth/signup',
|
||||
'primary_email_verified',
|
||||
'report_signin',
|
||||
'reset_password',
|
||||
'reset_password_confirmed',
|
||||
|
@ -54,6 +55,7 @@ module.exports = function () {
|
|||
'sms/sent',
|
||||
'sms/why',
|
||||
'verify_email',
|
||||
'verify_primary_email',
|
||||
'verify_secondary_email'
|
||||
].join('|'); // prepare for use in a RegExp
|
||||
|
||||
|
|
|
@ -90,7 +90,10 @@ define([], function () {
|
|||
SET_PRIMARY_EMAIL_BUTTON: '.email-address .set-primary',
|
||||
SUCCESS: '.success',
|
||||
TOOLTIP: '.tooltip',
|
||||
VERIFIED_LABEL: '.verified'
|
||||
UNLOCK_BUTTON: '.emails .unlock-button',
|
||||
UNLOCK_REFRESH_BUTTON: '.emails .refresh-verification-state',
|
||||
UNLOCK_SEND_BUTTON: '.emails .send-verification-email',
|
||||
VERIFIED_LABEL: '.verified',
|
||||
},
|
||||
ENTER_EMAIL: {
|
||||
EMAIL: 'input[type=email]',
|
||||
|
|
|
@ -49,8 +49,8 @@ define([
|
|||
name: 'settings secondary emails',
|
||||
|
||||
beforeEach: function () {
|
||||
email = TestHelpers.createEmail();
|
||||
secondaryEmail = TestHelpers.createEmail();
|
||||
email = TestHelpers.createEmail('sync{id}');
|
||||
secondaryEmail = TestHelpers.createEmail('sync{id}');
|
||||
client = FunctionalHelpers.getFxaClient();
|
||||
|
||||
return this.remote.then(clearBrowserState({ force: true }));
|
||||
|
@ -60,6 +60,73 @@ define([
|
|||
return this.remote.then(clearBrowserState());
|
||||
},
|
||||
|
||||
'gated in unverified session open verification same tab': function () {
|
||||
return this.remote
|
||||
// when an account is created, the original session is verified
|
||||
// re-login to destroy original session and created an unverified one
|
||||
.then(createUser(email, PASSWORD, { preVerified: true }))
|
||||
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
|
||||
// unlock panel
|
||||
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
|
||||
|
||||
// send and open verification in same tab
|
||||
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
|
||||
.then(openVerificationLinkInSameTab(email, 0))
|
||||
|
||||
// panel becomes verified and opens add secondary panel
|
||||
.then(visibleByQSA(selectors.EMAIL.INPUT));
|
||||
},
|
||||
|
||||
'gated in unverified session open verification new tab': function () {
|
||||
return this.remote
|
||||
// when an account is created, the original session is verified
|
||||
// re-login to destroy original session and created an unverified one
|
||||
.then(createUser(email, PASSWORD, { preVerified: true }))
|
||||
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
|
||||
// unlock panel
|
||||
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
|
||||
|
||||
// send and open verification in same tab
|
||||
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
|
||||
.then(openVerificationLinkInNewTab(email, 0))
|
||||
.then(switchToWindow(1))
|
||||
// panel becomes verified and opens add secondary panel
|
||||
.then(visibleByQSA(selectors.EMAIL.INPUT))
|
||||
.then(closeCurrentWindow())
|
||||
|
||||
.then(switchToWindow(0))
|
||||
.then(click(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
|
||||
.then(visibleByQSA(selectors.EMAIL.INPUT));
|
||||
},
|
||||
|
||||
'gated in unverified session open verification different browser': function () {
|
||||
return this.remote
|
||||
// when an account is created, the original session is verified
|
||||
// re-login to destroy original session and created an unverified one
|
||||
.then(createUser(email, PASSWORD, { preVerified: true }))
|
||||
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
|
||||
// unlock panel
|
||||
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
|
||||
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
|
||||
|
||||
// send and open verification in same tab
|
||||
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
|
||||
.then(openVerificationLinkInDifferentBrowser(email, 0))
|
||||
.then(click(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
|
||||
.then(visibleByQSA(selectors.EMAIL.INPUT));
|
||||
},
|
||||
|
||||
'add and verify secondary email': function () {
|
||||
return this.remote
|
||||
// sign up via the UI, we need a verified session to use secondary email
|
||||
|
@ -204,7 +271,6 @@ define([
|
|||
.then(testElementExists(selectors.SETTINGS.HEADER));
|
||||
},
|
||||
|
||||
|
||||
'signin confirmation is sent to secondary emails': function () {
|
||||
const PAGE_SIGNIN_DESKTOP = `${SIGNIN_URL}?context=fx_desktop_v3&service=sync&forceAboutAccounts=true`;
|
||||
const SETTINGS_URL = `${config.fxaContentRoot}settings?context=fx_desktop_v3&service=sync&forceAboutAccounts=true`;
|
||||
|
|
Загрузка…
Ссылка в новой задаче