packages/fxa-content-server/app/scripts/views/force_auth.js (185 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 AccountResetMixin from './mixins/account-reset-mixin';
import AuthErrors from '../lib/auth-errors';
import AvatarMixin from './mixins/avatar-mixin';
import cancelEventThen from './decorators/cancel_event_then';
import Cocktail from 'cocktail';
import FlowBeginMixin from './mixins/flow-begin-mixin';
import FormPrefillMixin from './mixins/form-prefill-mixin';
import FormView from './form';
import NullBehavior from './behaviors/null';
import PasswordMixin from './mixins/password-mixin';
import PasswordResetMixin from './mixins/password-reset-mixin';
import preventDefaultThen from './decorators/prevent_default_then';
import ServiceMixin from './mixins/service-mixin';
import SignInMixin from './mixins/signin-mixin';
import SignedInNotificationMixin from './mixins/signed-in-notification-mixin';
import Template from 'templates/force_auth.mustache';
import Transform from '../lib/transform';
import UserCardMixin from './mixins/user-card-mixin';
import Vat from '../lib/vat';
import BrandMessagingMixin from './mixins/brand-messaging-mixin';
import PocketMigrationMixin from './mixins/pocket-migration-mixin';
import MonitorClientMixin from './mixins/monitor-client-mixin';
const t = (msg) => msg;
var RELIER_DATA_SCHEMA = {
email: Vat.email().required(),
uid: Vat.uid().allow(null),
};
const PASSWORD_SELECTOR = 'input[type=password]';
const View = FormView.extend({
template: Template,
className: 'force-auth',
// used by the signin-mixin to decide which broker method to
// call with which data when signin is successful.
afterSignInBrokerMethod: 'afterForceAuth',
afterSignInNavigateData: { clearQueryParams: true },
_getAndValidateAccountData() {
var fieldsToPick = ['email', 'uid'];
var accountData = {};
var relier = this.relier;
fieldsToPick.forEach(function (fieldName) {
if (relier.has(fieldName)) {
accountData[fieldName] = relier.get(fieldName);
}
});
return Transform.transformUsingSchema(
accountData,
RELIER_DATA_SCHEMA,
AuthErrors
);
},
beforeRender() {
var accountData;
try {
accountData = this._getAndValidateAccountData();
} catch (err) {
// uid query parameter validation errors are not handled here,
// rather they are handled on startup by the relier.
this.fatalError(err);
return false;
}
/**
* If the relier specifies a UID, check whether the UID is still
* registered. If the uid is not registered, the account
* was probably deleted. If the broker supports UID changes,
* the user will still be allowed to signup or in, depending on
* whether the email is registered. If not, show a useful error
* and do not allow the user to continue.
*/
var account = this.user.initAccount({
email: accountData.email,
uid: accountData.uid,
});
if (accountData.uid) {
return Promise.all([
this.user.checkAccountEmailExists(account),
this.user.checkAccountUidExists(account),
]).then(([emailExists, uidExists]) => {
/*
* uidExists: false, emailExists: false
* Let user sign up w/ email.
* uidExists: true, emailExists: false
* Uid exists but doesn't match email, how'd this happen?
* Let the user sign up.
* uidExists: false, emailExists: true
* Sign in w/ new uid.
* uidExists: true, emailExists: true
* Assume for the same account, try to sign in
*/
if (!emailExists) {
return this._signUpIfUidChangeSupported(account);
}
if (!uidExists) {
return this._signInIfUidChangeSupported(account);
}
// email and uid are both registered, continue as normal
});
} else {
// relier did not specify a uid, there's a bit more flexibility.
// If the email no longer exists, sign up the user.
return this.user.checkAccountEmailExists(account).then((emailExists) => {
if (!emailExists) {
return this._navigateToForceSignUp(account);
}
});
}
},
_signUpIfUidChangeSupported(account) {
if (this.broker.hasCapability('allowUidChange')) {
return this._navigateToForceSignUp(account);
} else {
this.model.set('error', AuthErrors.toError('DELETED_ACCOUNT'));
}
},
_signInIfUidChangeSupported(account) {
// if the broker supports a UID change, use force_auth to sign in,
// otherwise print a big error message.
if (!this.broker.hasCapability('allowUidChange')) {
this.model.set('error', AuthErrors.toError('DELETED_ACCOUNT'));
}
},
_navigateToForceSignUp(account) {
// The default behavior of FxDesktop brokers is to halt before
// the signup confirmation poll because about:accounts takes care
// of polling and updating the UI. /force_auth is not opened in
// about:accounts and unless beforeSignUpConfirmationPoll is
// overridden, the user receives no visual feedback in this
// tab once the verification is complete.
this.broker.setBehavior('beforeSignUpConfirmationPoll', new NullBehavior());
return this.navigate('signup', {
error: AuthErrors.toError('DELETED_ACCOUNT'),
// account is for the email-first signup flow, without an account
// the signup page redirects to index
account,
// forceEmail is a hint to the signup page that the user should not
// be given the option to change their address.
forceEmail: account.get('email'),
});
},
_navigateToForceResetPassword() {
return this.navigate('reset_password', {
forceEmail: this.relier.get('email'),
});
},
setInitialContext(context) {
/// submit button
const buttonSignInText = this.translate(t('Sign in'), {
msgctxt: 'submit button',
});
context.set({
buttonSignInText,
email: this.relier.get('email'),
});
},
events: {
...FormView.prototype.events,
'click a[href="/reset_password"]': cancelEventThen(
'_navigateToForceResetPassword'
),
'click #use-different': preventDefaultThen('useDifferentAccount'),
},
useDifferentAccount() {
this.clearInput();
this.navigate('/', { account: this.getAccount() });
},
submit() {
const account = this.getAccount();
const password = this.getElementValue(PASSWORD_SELECTOR);
return this.signIn(account, password).catch((error) =>
this.onSignInError(account, error)
);
},
onSignInError(account, error) {
return Promise.resolve().then(() => {
if (AuthErrors.is(error, 'UNKNOWN_ACCOUNT')) {
if (this.relier.has('uid')) {
if (this.broker.hasCapability('allowUidChange')) {
return this._navigateToForceSignUp(account);
} else {
throw AuthErrors.toError('DELETED_ACCOUNT');
}
} else {
return this._navigateToForceSignUp(account);
}
} else if (AuthErrors.is(error, 'USER_CANCELED_LOGIN')) {
this.logViewEvent('canceled');
// if user canceled login, just stop
return;
} else if (AuthErrors.is(error, 'ACCOUNT_RESET')) {
return this.notifyOfResetAccount(account);
} else if (AuthErrors.is(error, 'INCORRECT_PASSWORD')) {
return this.showValidationError(this.$('#password'), error);
} else {
throw error;
}
});
},
getAccount() {
const email = this.relier.get('email');
const account = this.user.getAccountByEmail(email);
// if no account is in localStorage for the email address,
// the returned account will be the default. Set the email
// so that the user-card displays correctly.
if (account.isDefault()) {
account.set({
email,
uid: this.relier.get('uid'),
});
}
return account;
},
});
Cocktail.mixin(
View,
AccountResetMixin,
AvatarMixin,
FlowBeginMixin,
FormPrefillMixin,
PasswordMixin,
PasswordResetMixin,
ServiceMixin,
SignInMixin,
SignedInNotificationMixin,
UserCardMixin,
BrandMessagingMixin,
PocketMigrationMixin,
MonitorClientMixin
);
export default View;