'use strict';
/**
* Controller for all customer login storefront processes.
*
* @module controllers/Login
*/
/* API Includes */
var OAuthLoginFlowMgr = require('dw/customer/oauth/OAuthLoginFlowMgr');
var OrderMgr = require('dw/order/OrderMgr');
var Transaction = require('dw/system/Transaction');
var URLUtils = require('dw/web/URLUtils');
var RateLimiter = require('app_storefront_core/cartridge/scripts/util/RateLimiter');
/* Script Modules */
var app = require('~/cartridge/scripts/app');
var guard = require('~/cartridge/scripts/guard');
var Customer = app.getModel('Customer');
var LOGGER = dw.system.Logger.getLogger('login');
/**
* Contains the login page preparation and display, it is called from various
* places implicitly when 'loggedIn' is ensured via the {@link module:guard}.
*/
function show() {
var pageMeta = require('~/cartridge/scripts/meta');
var ContentMgr = dw.content.ContentMgr;
var content = ContentMgr.getContent('myaccount-login');
var loginForm = app.getForm('login');
var oauthLoginForm = app.getForm('oauthlogin');
var orderTrackForm = app.getForm('ordertrack');
var loginView = app.getView('Login',{
RegistrationStatus: false
});
loginForm.clear();
oauthLoginForm.clear();
orderTrackForm.clear();
if (customer.registered) {
loginForm.setValue('username', customer.profile.credentials.login);
loginForm.setValue('rememberme', true);
}
if (content) {
pageMeta.update(content);
}
// Save return URL in session.
if (request.httpParameterMap.original.submitted) {
session.custom.TargetLocation = request.httpParameterMap.original.value;
}
if (request.httpParameterMap.scope.submitted) {
switch (request.httpParameterMap.scope.stringValue) {
case 'wishlist':
loginView.template = 'account/wishlist/wishlistlanding';
break;
case 'giftregistry':
loginView.template = 'account/giftregistry/giftregistrylanding';
break;
default:
}
}
loginView.render();
}
/**
* Internal function that reads the URL that should be redirected to after successful login
* @return {dw.web.Url} The URL to redirect to in case of success
* or {@link module:controllers/Account~Show|Account controller Show function} in case of failure.
*/
function getTargetUrl () {
if (session.custom.TargetLocation) {
var target = session.custom.TargetLocation;
delete session.custom.TargetLocation;
//@TODO make sure only path, no hosts are allowed as redirect target
dw.system.Logger.info('Redirecting to "{0}" after successful login', target);
return decodeURI(target);
} else {
return URLUtils.https('Account-Show');
}
}
/**
* Form handler for the login form. Handles the following actions:
* - __login__ - logs the customer in and renders the login page.
* If login fails, clears the login form and redirects to the original controller that triggered the login process.
* - __register__ - redirects to the {@link module:controllers/Account~startRegister|Account controller StartRegister function}
* - __findorder__ - if the ordertrack form does not contain order number, email, or postal code information, redirects to
* {@link module:controllers/Login~Show|Login controller Show function}. If the order information exists, searches for the order
* using that information. If the order cannot be found, renders the LoginView. Otherwise, renders the order details page
* (account/orderhistory/orderdetails template).
* - __error__ - renders the LoginView.
*/
function handleLoginForm () {
var loginForm = app.getForm('login');
loginForm.handleAction({
login: function () {
// Check to see if the number of attempts has exceeded the session threshold
if (RateLimiter.isOverThreshold('FailedLoginCounter')) {
RateLimiter.showCaptcha();
}
var success = Customer.login(loginForm.getValue('username'), loginForm.getValue('password'), loginForm.getValue('rememberme'));
if (!success) {
loginForm.get('loginsucceeded').invalidate();
app.getView('Login').render();
return;
} else {
loginForm.clear();
}
RateLimiter.hideCaptcha();
// In case of successful login
// Redirects to the original controller that triggered the login process.
response.redirect(getTargetUrl());
return;
},
register: function () {
response.redirect(URLUtils.https('Account-StartRegister'));
return;
},
findorder: function () {
var orderTrackForm = app.getForm('ordertrack');
var orderNumber = orderTrackForm.getValue('orderNumber');
var orderFormEmail = orderTrackForm.getValue('orderEmail');
var orderPostalCode = orderTrackForm.getValue('postalCode');
if (!orderNumber || !orderPostalCode || !orderFormEmail) {
response.redirect(URLUtils.https('Login-Show'));
return;
}
// Check to see if the number of attempts has exceeded the session threshold
if (RateLimiter.isOverThreshold('FailedOrderTrackerCounter')) {
RateLimiter.showCaptcha();
}
var orders = OrderMgr.searchOrders('orderNo={0} AND status!={1}', 'creationDate desc', orderNumber,
dw.order.Order.ORDER_STATUS_REPLACED);
if (empty(orders)) {
app.getView('Login', {
OrderNotFound: true
}).render();
return;
}
var foundOrder = orders.next();
if (foundOrder.billingAddress.postalCode.toUpperCase() !== orderPostalCode.toUpperCase() || foundOrder.customerEmail !== orderFormEmail) {
app.getView('Login', {
OrderNotFound: true
}).render();
return;
}
// Reset the error condition on exceeded attempts
RateLimiter.hideCaptcha();
app.getView({
Order: foundOrder
}).render('account/orderhistory/orderdetails');
},
search: function (form, action) {
var ProductList = require('dw/customer/ProductList');
var ProductListModel = app.getModel('ProductList');
var context = {};
var searchForm, listType, productLists, template;
if (action.htmlName.indexOf('wishlist_search') !== -1) {
searchForm = action.parent;
listType = ProductList.TYPE_WISH_LIST;
template = 'account/wishlist/wishlistresults';
productLists = ProductListModel.search(searchForm, listType);
Transaction.wrap(function () {
session.forms.wishlist.productlists.copyFrom(productLists);
searchForm.clearFormElement();
});
context.SearchFirstName = searchForm.firstname.value;
context.SearchLastName = searchForm.lastname.value;
context.SearchEmail = searchForm.email.value;
} else if (action.htmlName.indexOf('giftregistry_search') !== -1) {
searchForm = action.parent.simple;
listType = ProductList.TYPE_GIFT_REGISTRY;
template = 'account/giftregistry/giftregistryresults';
productLists = ProductListModel.search(searchForm, listType);
context.ProductLists = productLists;
}
app.getView(context).render(template);
},
error: function () {
app.getView('Login').render();
return;
}
});
}
/**
* Form handler for the oauthlogin form. Handles the following actions:
* - __login__ - Starts the process of authentication via an external OAuth2 provider.
* Uses the OAuthProvider property in the httpParameterMap to determine which provider to initiate authentication with.
* Redirects to the provider web page where the customer initiates the actual user authentication.
* If no provider page is available, renders the LoginView.
* - __error__ - renders the LoginView.
*/
function handleOAuthLoginForm() {
var oauthLoginForm = app.getForm('oauthlogin');
oauthLoginForm.handleAction({
login: function () {
if (request.httpParameterMap.OAuthProvider.stringValue) {
session.custom.RememberMe = request.httpParameterMap.rememberme.booleanValue || false;
var OAuthProviderID = request.httpParameterMap.OAuthProvider.stringValue;
var initiateOAuthLoginResult = OAuthLoginFlowMgr.initiateOAuthLogin(OAuthProviderID);
if (!initiateOAuthLoginResult) {
oauthLoginForm.get('loginsucceeded').invalidate();
// Show login page with error.
app.getView('Login').render();
return;
}
response.redirect(initiateOAuthLoginResult.location);
}
return;
},
error: function () {
app.getView('Login').render();
return;
}
});
}
/**
* Determines whether the request has an OAuth provider set. If it does, calls the
* {@link module:controllers/Login~handleOAuthLoginForm|handleOAuthLoginForm} function,
* if not, calls the {@link module:controllers/Login~handleLoginForm|handleLoginForm} function.
*/
function processLoginForm () {
if (request.httpParameterMap.OAuthProvider.stringValue) {
handleOAuthLoginForm();
} else {
handleLoginForm();
}
}
/**
* Invalidates the oauthlogin form.
* Calls the {@link module:controllers/Login~finishOAuthLogin|finishOAuthLogin} function.
*/
function oAuthFailed() {
app.getForm('oauthlogin').get('loginsucceeded').invalidate();
finishOAuthLogin();
}
/**
* Clears the oauthlogin form.
* Calls the {@link module:controllers/Login~finishOAuthLogin|finishOAuthLogin} function.
*/
function oAuthSuccess() {
app.getForm('oauthlogin').clear();
finishOAuthLogin();
}
/**
* This function is called after authentication by an external oauth provider.
* If the user is successfully authenticated, the provider returns an authentication code,
* this function exchanges the code for a token and with that token requests the user information specified by
* the configured scope (id, first/last name, email, etc.) from the provider.
* If the token exchange succeeds, calls the {@link module:controllers/Login~oAuthSuccess|oAuthSuccess} function.
* If the token exchange fails, calls the {@link module:controllers/Login~oAuthFailed|oAuthFailed} function.
* The function also handles multiple error conditions and logs them.
*/
function handleOAuthReentry() {
var finalizeOAuthLoginResult = OAuthLoginFlowMgr.finalizeOAuthLogin();
if (!finalizeOAuthLoginResult) {
oAuthFailed();
return;
}
var responseText = finalizeOAuthLoginResult.userInfoResponse.userInfo;
var oAuthProviderID = finalizeOAuthLoginResult.accessTokenResponse.oauthProviderId;
var accessToken = finalizeOAuthLoginResult.accessTokenResponse.accessToken;
if (!oAuthProviderID) {
LOGGER.warn('OAuth provider id is null.');
oAuthFailed();
return;
}
if (!responseText) {
LOGGER.warn('Response from provider is empty');
oAuthFailed();
return;
}
//whether to drop the rememberMe cookie (preserved in the session before InitiateOAuthLogin)
var rememberMe = session.custom.RememberMe;
delete session.custom.RememberMe;
// LinkedIn returns XML.
var extProfile = {};
if (oAuthProviderID === 'LinkedIn') {
var responseReader = new dw.io.Reader(responseText);
var xmlStreamReader = new dw.io.XMLStreamReader(responseReader);
while (xmlStreamReader.hasNext()) {
if (xmlStreamReader.next() === dw.io.XMLStreamConstants.START_ELEMENT) {
var localElementName = xmlStreamReader.getLocalName();
// Ignore the top level person element and read the rest into a plain object.
if (localElementName !== 'person') {
extProfile[localElementName] = xmlStreamReader.getElementText();
}
}
}
xmlStreamReader.close();
responseReader.close();
} else {
// All other providers return JSON.
extProfile = JSON.parse(responseText);
if (!extProfile) {
LOGGER.warn('Data could not be extracted from the response:\n{0}', responseText);
oAuthFailed();
return;
}
if (oAuthProviderID === 'VKontakte') {
// They use JSON, but thought it would be cool to add some extra top level elements
extProfile = extProfile.response[0];
}
}
// This is always id or uid for all providers.
var userId = extProfile.id || extProfile.uid;
if (!userId) {
LOGGER.warn('Undefined user identifier - make sure you are mapping the correct property from the response.' +
' We are mapping "id" which is not available in the response: \n', extProfile);
oAuthFailed();
return;
}
LOGGER.debug('Parsed UserId "{0}" from response: {1}', userId, JSON.stringify(extProfile));
if (oAuthProviderID === 'SinaWeibo') {
// requires additional requests to get the info
extProfile = getSinaWeiboAccountInfo(accessToken, userId);
}
var profile = dw.customer.CustomerMgr.getExternallyAuthenticatedCustomerProfile(oAuthProviderID, userId);
var customer;
if (!profile) {
Transaction.wrap(function () {
LOGGER.debug('User id: ' + userId + ' not found, creating a new profile.');
customer = dw.customer.CustomerMgr.createExternallyAuthenticatedCustomer(oAuthProviderID, userId);
profile = customer.getProfile();
var firstName, lastName, email;
// Google comes with a 'name' property that holds first and last name.
if (typeof extProfile.name === 'object') {
firstName = extProfile.name.givenName;
lastName = extProfile.name.familyName;
} else {
// The other providers use one of these, GitHub & SinaWeibo have just a 'name'.
firstName = extProfile['first-name'] || extProfile.first_name || extProfile.name;
lastName = extProfile['last-name'] || extProfile.last_name || extProfile.name;
}
// Simple email addresses.
email = extProfile['email-address'] || extProfile.email;
if (!email) {
var emails = extProfile.emails;
// Google comes with an array
if (emails && emails.length) {
//First element of the array is the account email according to Google.
profile.setEmail(extProfile.emails[0].value);
// While MS comes with an object.
} else {
email = emails.preferred || extProfile['emails.account'] || extProfile['emails.personal'] ||
extProfile['emails.business'];
}
}
LOGGER.debug('Updating profile with "{0} {1} - {2}".',firstName, lastName,email);
profile.setFirstName(firstName);
profile.setLastName(lastName);
profile.setEmail(email);
});
} else {
customer = profile.getCustomer();
}
var credentials = profile.getCredentials();
if (credentials.isEnabled()) {
Transaction.wrap(function () {
dw.customer.CustomerMgr.loginExternallyAuthenticatedCustomer(oAuthProviderID, userId, rememberMe);
});
LOGGER.debug('Logged in external customer with id: {0}', userId);
} else {
LOGGER.warn('Customer attempting to login into a disabled profile: {0} with id: {1}',
profile.getCustomer().getCustomerNo(), userId);
oAuthFailed();
return;
}
oAuthSuccess();
}
/**
* Get Sina Weibo account via additional requests.
* Also handles multiple error conditions and logs them.
* @param {String} accessToken The OAuth access token.
* @param {String} userId The OAuth user ID.
* @return {Object} Account information.
* @todo Migrate httpClient calls to dw.svc.*
*/
function getSinaWeiboAccountInfo(accessToken, userId) {
var name, email;
if (null === accessToken) {
LOGGER.warn('Exiting because the AccessToken input parameter is null.');
return null;
}
var accessTokenSuffix = '?access_token=' + accessToken;
var http = new dw.net.HTTPClient();
http.setTimeout(30000); //30 secs
//Obtain the name:
//http://open.weibo.com/wiki/2/users/show/en -> https://api.weibo.com/2/users/show.json
var urlUser = 'https://api.weibo.com/2/users/show.json' + accessTokenSuffix +
'&uid=' + userId;
http.open('GET', urlUser);
http.send();
var resultName = http.getText();
if (200 !== http.statusCode) {
LOGGER.warn('Got an error calling:' + urlUser +
'. The status code is:' + http.statusCode + ' ,the text is:' + resultName +
' and the error text is:' + http.getErrorText());
return null;
} else {
var weiboUser = JSON.parse(resultName);
if (null === weiboUser) {
LOGGER.warn('Name could not be extracted from the response:' + resultName);
return null;
} else {
name = weiboUser.name;
}
}
//Obtain the email:
//http://open.weibo.com/wiki/2/account/profile/email -> https://api.weibo.com/2/account/profile/email.json
var urlEmail = 'https://api.weibo.com/2/account/profile/email.json' + accessTokenSuffix;
http.open('GET', urlEmail);
http.send();
var resultEmail = http.getText();
if (200 !== http.statusCode) {//!
LOGGER.warn('Email could not be retrieved. Got an error calling:' + urlUser +
'. The status code is:' + http.statusCode + ' ,the text is:' + resultEmail +
' and the error text is:' + http.getErrorText() +
'. Make sure your application is authorized by Weibo to request email info (usually need to be successfully audited by them.)');
} else {
var weiboEmail = JSON.parse(resultEmail);// in the format: ('[{"Email": "[email protected]"}]');
if (null === weiboEmail) {
LOGGER.warn('Email could not be extracted from the response:' + resultEmail);
} else {
var emails = weiboEmail;
if (emails && 0 < emails.length) {
//first element of the array would be the account email according to Google:
email = emails[0].Email;
}
}
}
return {name: name, email: email};
}
/**
* Internal helper function to finish the OAuth login.
* Redirects user to the location set in either the
* {@link module:controllers/Login~handleOAuthLoginForm|handleOAuthLoginForm} function
*/
function finishOAuthLogin() {
// To continue to the destination that is already preserved in the session.
var location = getTargetUrl().toString();
response.redirect(location);
}
/**
* Logs the customer out and clears the login and profile forms.
* Calls the {@link module:controllers/Account~Show|Account controller Show function}.
*/
function Logout() {
Customer.logout();
app.getForm('login').clear();
app.getForm('profile').clear();
// TODO: Investigate whether this line should be removed
//Cart.get().calculate();
response.redirect(URLUtils.https('Account-Show'));
}
/*
* Web exposed methods
*/
/** Contains the login page preparation and display.
* @see module:controllers/Login~show */
exports.Show = guard.ensure(['https'], show);
/** Determines whether the request has an OAuth provider set.
* @see module:controllers/Login~processLoginForm */
exports.LoginForm = guard.ensure(['https','post', 'csrf'], processLoginForm);
/** Form handler for the oauthlogin form.
* @see module:controllers/Login~handleOAuthLoginForm */
exports.OAuthLoginForm = guard.ensure(['https','post', 'csrf'], handleOAuthLoginForm);
/** Exchanges a user authentication code for a token and requests user information from an OAUTH provider.
* @see module:controllers/Login~handleOAuthReentry */
exports.OAuthReentry = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryLinkedIn = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryGoogle = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryGooglePlus = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryMicrosoft = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryFacebook = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryGitHub = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentrySinaWeibo = guard.ensure(['https','get'], handleOAuthReentry);
/** @deprecated This is only kept for compatibility reasons, use {@link module:controllers/Login~handleOAuthReentry|handleOAuthReentry} instead */
exports.OAuthReentryVKontakte = guard.ensure(['https','get'], handleOAuthReentry);
/** Contains the login page preparation and display.
* @see module:controllers/Login~show */
exports.Logout = guard.all(Logout);