/*! meta-client/modules/component/login */
/*jslint
browser, long
*/
/*global
*/
/**
* login component.
*
* @module meta-client/modules/component/login
*/
import _ from "underscore";
import i18next from "util-web/modules/i18next";
import keyval from "util-web/modules/keyval";
import ko from "knockout";
import Logger from "js-logger";
import model from "meta-core/modules/model";
import protocol from "meta-core/modules/protocol";
import template from "./login.html";
import ui from "util-web/modules/ui";
import viewmodel from "meta-client/modules/viewmodel";
import webauthn from "meta-client/modules/webauthn";
const LOG = Logger.get("meta-client/component/login");
const TOKEN_STORAGE_KEY_PREFIX = "token-";
export default Object.freeze({
template,
viewModel: {
createViewModel: function ({
client,
clientName,
clientVersion,
customClasses = null,
editUserEnable = false,
help = null,
info = null,
isActive,
login,
logout,
onAuth,
onConnectFailed,
otpRequired = false,
otpTypes = [],
passwordRegex = null,
totpIssuer = null,
webAuthnEnable = false
}) {
const connect = function () {
if (client.isConnected()) {
return Promise.resolve(undefined);
}
return client.connect();
};
const tokenStorageKey = TOKEN_STORAGE_KEY_PREFIX + clientName;
const deleteToken = () => keyval.delete(tokenStorageKey).catch(
(exc) => LOG.warn("deleteToken failed", exc)
).then(() => vm.hasUserFocus(true));
const loadStoredToken = function () {
return keyval.get(tokenStorageKey).then(function (token) {
if (token) {
return vm.loginInternal({token});
}
vm.hasUserFocus(true);
return Promise.resolve(undefined);
}, function (exc) {
LOG.warn("loadStoredToken failed", exc);
}).then(function () {
vm.isLoadingToken(false);
});
};
const vm = {};
if (otpRequired && _.isEmpty(otpTypes)) {
throw new Error("otpTypes required");
}
if (otpTypes.includes(model.metaUserOtpType.TOTP) && _.isEmpty(totpIssuer)) {
throw new Error("totpIssuer required");
}
vm.authRef = protocol.REST_PATH + "/" + protocol.getWebhookAuthPath("exampleAuthRef");
vm.customClasses = customClasses;
vm.isActive = isActive;
vm.metaUserOtpType = model.metaUserOtpType;
vm.otpTypeNone = (
otpRequired
? undefined
: i18next.pureComputedT("no_otp")
);
vm.otpTypes = otpTypes;
vm.otpTypeOptions = Object.keys(model.metaUserOtpType).map(function (key) {
if (otpTypes.includes(key) === false) {
return;
}
return {name: i18next.computedT("otp_type_" + key), type: key};
}).filter(_.isObject);
vm.currentLocale = i18next.currentLocale;
vm.changeLocale = (locale) => i18next.setLocale(locale);
vm.user = ko.observable();
vm.hasUserFocus = ko.observable(false);
vm.password = ko.observable();
vm.unmaskPasswords = ko.observable(false);
vm.editUserEnable = editUserEnable;
vm.webAuthnEnable = webAuthnEnable;
vm.enableHelp = _.isFunction(help);
vm.info = (
_.isEmpty(info)
? undefined
: info
);
vm.connectFailed = ko.observable(false);
vm.hasOtpFocus = ko.observable(false);
vm.isAuthActive = ko.observable(false);
vm.isAuthValid = ko.observable(false);
vm.isLoggingIn = ko.observable(false);
vm.loginFailed = ko.observable(false);
vm.onAuthFailed = ko.observable(false);
vm.otp = ko.observable();
vm.otp.subscribe(() => vm.isAuthValid(true));
vm.togglePasswordMasking = () => vm.unmaskPasswords(!vm.unmaskPasswords());
vm.onAuth = function () {
if (vm.isEditingUser()) {
return vm.loadUser();
}
return client.authToken(protocol.authTokenRequest()).then(function (reply) {
if (reply) {
return keyval.set(tokenStorageKey, reply.token);
}
}).catch(
(exc) => LOG.warn("onAuth authToken failed", exc)
).then(function () {
vm.onAuthFailed(false);
if (_.isFunction(onAuth)) {
onAuth();
}
return Promise.resolve(undefined);
}).catch(function (exc) {
vm.onAuthFailed(true);
LOG.error("onAuth call failed", exc);
});
};
vm.loginInternal = function ({
password = null,
token = null,
user = null
}) {
vm.isLoggingIn(true);
return connect().then(function () {
vm.connectFailed(false);
return client.login(protocol.loginRequest({
client: clientName,
locale: i18next.currentLocale(),
password,
token,
user,
version: clientVersion
}));
}, function (exc) {
LOG.warn("connect failed", exc);
if (_.isFunction(onConnectFailed)) {
onConnectFailed();
}
return Promise.reject(exc);
}).then(function (reply) {
vm.loginFailed(false);
if (reply.isAuth) {
return vm.onAuth();
}
vm.isAuthActive(true);
vm.hasOtpFocus(true);
if (reply.webAuthnAssertionRequest) {
return vm.authInternal(reply.webAuthnAssertionRequest);
}
return Promise.resolve(undefined);
}, function (exc) {
LOG.warn("loginInternal failed", exc);
if (exc && _.has(exc, protocol.REPLY_TYPE_PROPERTY)) {
if (_.isEmpty(token) === false) {
return deleteToken();
}
vm.loginFailed(true);
vm.hasUserFocus(true);
} else {
vm.connectFailed(true);
}
}).then(function () {
vm.password(undefined);
vm.isLoggingIn(false);
});
};
vm.login = function (form) {
if (ui.validateForm(form) === false) {
return;
}
const user = vm.user();
LOG.info(`login, user=${user}`);
return vm.loginInternal({
password: vm.password(),
user
}).then(function () {
ui.resetForm(form);
});
};
if (ko.isWritableObservable(login)) {
login.subscribe(function (doLogin) {
if (doLogin) {
vm.login();
login(false);
}
});
}
vm.isWebAuthnActive = ko.observable(false);
vm.webAuthnFailed = ko.observable(false);
vm.authInternal = function (webAuthnAssertionRequest = null) {
let webAuthnAssertionPromise = Promise.resolve(undefined);
if (webAuthnAssertionRequest) {
vm.isWebAuthnActive(true);
webAuthnAssertionPromise = webauthn.getAssertion(webAuthnAssertionRequest.assertion);
}
LOG.debug(`authInternal, clientId=${client.clientId()}, user=${client.user()}`);
return webAuthnAssertionPromise.then(function (webAuthnResponse) {
const request = (
webAuthnResponse
? protocol.authRequest({
webAuthnAssertionReply: protocol.authRequestWebAuthnAssertionReply({credential: webAuthnResponse})
})
: protocol.authRequest({otp: vm.otp()})
);
return client.auth(protocol.authRequest(request));
}, function (exc) {
LOG.warn("authInternal webAuthn failed", exc);
vm.webAuthnFailed(true);
}).then(function () {
vm.otp(undefined);
vm.isWebAuthnActive(false);
vm.webAuthnFailed(false);
vm.isAuthValid(true);
vm.isAuthActive(false);
return vm.onAuth();
}, function (exc) {
LOG.warn("authInternal failed", exc);
vm.isAuthValid(false);
});
};
vm.isInAuth = ko.observable(false);
vm.auth = function (form) {
if (ui.validateForm(form) === false) {
return;
}
if (client.isConnected() === false || Boolean(client.clientId()) === false) {
vm.isAuthActive(false);
return;
}
LOG.debug("auth");
vm.isInAuth(true);
return vm.authInternal().then(function () {
vm.isInAuth(false);
if (vm.isAuthValid()) {
ui.resetForm(form);
}
});
};
vm.cancelAuth = function () {
ui.resetForm(document.getElementById("auth"));
vm.otp(undefined);
webauthn.abortAssertion();
return vm.logout();
};
vm.logout = function () {
LOG.debug("logout");
vm.user(undefined);
vm.password(undefined);
vm.isLoggingIn(false);
vm.isAuthActive(false);
vm.isAuthValid(true);
vm.isWebAuthnActive(false);
vm.saveUserFailed(false);
vm.saveUserVerifyFailed(false);
vm.webAuthnFailed(false);
return client.logout(protocol.logoutRequest()).catch(
(exc) => LOG.warn("logout failed", exc)
).then(deleteToken);
};
if (ko.isWritableObservable(logout)) {
logout.subscribe(function (doLogout) {
if (doLogout) {
vm.logout();
logout(false);
}
});
}
vm.isEditingUser = ko.observable(false);
vm.editedUser = ko.observable();
vm.newPassword = ko.observable();
vm.newPasswordConfirm = ko.observable();
vm.isNewPasswordValid = viewmodel.newPasswordValidator({
passwordConfirm: vm.newPasswordConfirm,
passwordNew: vm.newPassword,
regexString: passwordRegex,
unmask: vm.unmaskPasswords
});
vm.newOtpType = ko.observable();
vm.newOtpEmail = ko.observable();
vm.newOtpPhone = ko.observable();
vm.hasTotp = ko.observable(false);
vm.doGenerateTotp = ko.observable(false);
vm.newWebhook = ko.observable();
vm.isNewOtpValid = ko.pureComputed(function () {
const type = vm.newOtpType();
if (type) {
switch (type) {
case model.metaUserOtpType.EMAIL:
return _.isEmpty(vm.newOtpEmail()) === false;
case model.metaUserOtpType.PHONE:
return _.isEmpty(vm.newOtpPhone()) === false;
case model.metaUserOtpType.WEBHOOK:
return _.isEmpty(vm.newWebhook()) === false;
}
}
return true;
});
vm.webAuthnCredentials = ko.observableArray();
vm.newWebAuthnDisplayName = ko.observable();
vm.isSavingUser = ko.observable(false);
vm.isRegisteringWebAuthn = ko.observable(false);
vm.hasSavedUser = ko.observable(false);
vm.saveUserFailed = ko.observable(false);
vm.saveUserVerifyFailed = ko.observable(false);
vm.editUser = function () {
LOG.debug("editUser");
vm.isEditingUser(true);
vm.hasUserFocus(true);
};
vm.cancelEditUser = function () {
ui.resetForm(document.getElementById("saveUser"));
vm.editedUser(undefined);
vm.newPassword(undefined);
vm.newPasswordConfirm(undefined);
vm.newOtpType(undefined);
vm.newOtpEmail(undefined);
vm.newOtpPhone(undefined);
vm.hasTotp(false);
vm.doGenerateTotp(false);
vm.newWebhook(undefined);
vm.webAuthnCredentials.removeAll();
vm.newWebAuthnDisplayName(undefined);
vm.isEditingUser(false);
return vm.logout();
};
vm.loadUser = function () {
LOG.debug("loadUser");
return client.userSearch(protocol.userSearchRequest({
limit: 1,
query: client.user()
})).then(function (reply) {
const user = _.head(reply.users);
if (Boolean(user) === false) {
return vm.cancelEditUser();
}
vm.editedUser(user);
if (user.otp) {
vm.newOtpType(user.otp.type);
vm.newOtpEmail(user.otp.email);
vm.newOtpPhone(user.otp.phone);
vm.hasTotp(model.metaUserOtpType.TOTP === user.otp.type && Boolean(user.otp.totp));
vm.newWebhook(user.otp.webhook);
}
if (user.webAuthn) {
vm.webAuthnCredentials(user.webAuthn.credentials.map(function (credential) {
return Object.assign({
remove: ko.observable(false),
sanitizedDisplayName: credential.displayName.replace(/\s/g, "")
}, credential);
}));
}
});
};
vm.onVerify = ko.observable();
vm.onVerifyDone = ko.observable();
vm.getSanitizedWebAuthn = function (webAuthn = null) {
if (_.isObject(webAuthn)) {
return Object.assign(
{},
webAuthn,
{
credentials: vm.webAuthnCredentials().filter((credential) => credential.remove() === false).map(function (credential) {
delete credential.remove;
delete credential.sanitizedDisplayName;
return credential;
})
}
);
}
return webAuthn;
};
vm.saveUser = function (form) {
if (ui.validateForm(form) === false) {
return;
}
const user = vm.editedUser();
const otpType = vm.newOtpType();
const newPassword = vm.newPassword();
const updateUser = model.metaUser(Object.assign({}, user, {
otp: (
otpType
? model.metaUserOtp({
email: vm.newOtpEmail(),
phone: vm.newOtpPhone(),
totp: (
vm.doGenerateTotp()
? null
: _.get(user, ["otp", "totp"])
),
type: otpType,
webhook: vm.newWebhook()
})
: null
),
password: (
_.isEmpty(newPassword)
? user.password
: newPassword
),
webAuthn: vm.getSanitizedWebAuthn(user.webAuthn)
}));
LOG.debug("saveUser", updateUser);
const webAuthnRequest = (
_.isEmpty(vm.newWebAuthnDisplayName()) === false
? protocol.userWriteRequestWebAuthnRequest({displayName: vm.newWebAuthnDisplayName()})
: null
);
vm.isSavingUser(true);
vm.saveUserFailed(false);
return client.userWrite(protocol.userWriteRequest({
action: protocol.WRITE_ACTION.UPDATE,
locale: i18next.currentLocale(),
user: updateUser,
webAuthnRequest
})).then(function (reply) {
if (reply.verifyOtp) {
vm.saveUserVerifyFailed(false);
return new Promise(function (resolve, reject) {
const verifySub = vm.onVerifyDone.subscribe(function (result) {
verifySub.dispose();
const otp = _.get(result, "otp");
LOG.debug(`saveUser onVerifyDone, otp=${otp}`);
if (otp) {
resolve(otp);
} else {
reject();
}
});
vm.onVerify({
totpIssuer,
totpKey: reply.totpKey,
user: reply.user.name
});
}).then(function (otp) {
return client.userWrite(protocol.userWriteRequest({ // OTP second request flow
action: protocol.WRITE_ACTION.UPDATE,
locale: i18next.currentLocale(),
otp,
user: updateUser
}));
});
}
if (reply.webAuthnRequest) {
vm.isRegisteringWebAuthn(true);
return webauthn.createCredential(reply.webAuthnRequest.credentialOptions).then(function (response) {
return client.userWrite(protocol.userWriteRequest({
action: protocol.WRITE_ACTION.UPDATE,
locale: i18next.currentLocale(),
user: updateUser,
webAuthnReply: protocol.userWriteRequestWebAuthnReply({credential: response})
}));
});
}
}).then(function () {
vm.hasSavedUser(true);
ui.resetForm(form);
return vm.cancelEditUser();
}).catch(function (exc) {
LOG.warn("saveUser failed", exc);
if (protocol.ERROR_TYPE.AUTH === _.get(exc, "type")) {
vm.saveUserVerifyFailed(true);
} else {
vm.saveUserFailed(true);
}
}).then(function () {
vm.isRegisteringWebAuthn(false);
vm.isSavingUser(false);
});
};
vm.isRecoverUserActive = ko.observable(false);
vm.hasRecoverUserEmailFocus = ko.observable(false);
vm.recoverUserEmail = ko.observable();
vm.recoverUserPhone = ko.observable();
vm.isRecoverUserValid = ko.pureComputed(function () {
const email = vm.recoverUserEmail();
const phone = vm.recoverUserPhone();
return _.isEmpty(email) === false || _.isEmpty(phone) === false;
});
vm.recoverUserSuccess = ko.observable(false);
vm.activateRecoverUser = function () {
vm.isResetPasswordActive(false);
vm.isRecoverUserActive(true);
vm.hasRecoverUserEmailFocus(true);
};
vm.cancelRecoverUser = function () {
vm.isRecoverUserActive(false);
vm.recoverUserEmail(undefined);
vm.recoverUserPhone(undefined);
};
vm.recoverUser = function (form) {
if (ui.validateForm(form) === false) {
return;
}
LOG.debug("recoverUser");
return connect().then(function () {
return client.recoverUser(protocol.recoverUserRequest({
email: vm.recoverUserEmail(),
locale: i18next.currentLocale(),
phone: vm.recoverUserPhone()
}));
}).then(function () {
ui.resetForm(form);
vm.cancelRecoverUser();
vm.recoverUserSuccess(true);
});
};
vm.hasResetPasswordOtpFocus = ko.observable(false);
vm.hasResetPasswordUserFocus = ko.observable(false);
vm.hasSentResetPasswordOtp = ko.observable(false);
vm.isResetPasswordActive = ko.observable(false);
vm.resetPasswordFailed = ko.observable(false);
vm.resetPasswordNew = ko.observable();
vm.resetPasswordNewConfirm = ko.observable();
vm.resetPasswordOtp = ko.observable();
vm.resetPasswordSuccess = ko.observable(false);
vm.resetPasswordUser = ko.observable();
vm.isResetPasswordNewValid = viewmodel.newPasswordValidator({
passwordConfirm: vm.resetPasswordNewConfirm,
passwordNew: vm.resetPasswordNew,
regexString: passwordRegex,
unmask: vm.unmaskPasswords
});
vm.activateResetPassword = function () {
vm.isRecoverUserActive(false);
vm.isResetPasswordActive(true);
vm.hasResetPasswordUserFocus(true);
};
vm.cancelResetPassword = function () {
vm.hasSentResetPasswordOtp(false);
vm.isResetPasswordActive(false);
vm.resetPasswordFailed(false);
vm.resetPasswordNew(undefined);
vm.resetPasswordNewConfirm(undefined);
vm.resetPasswordOtp(undefined);
vm.resetPasswordUser(undefined);
};
vm.resetPasswordStart = function (form) {
if (ui.validateForm(form) === false) {
return;
}
const user = vm.resetPasswordUser();
LOG.info(`resetPasswordStart, user=${user}`);
return connect().then(function () {
return client.resetPassword(protocol.resetPasswordRequest({
locale: i18next.currentLocale(),
user
}));
}).then(function () {
vm.resetPasswordFailed(false);
vm.hasSentResetPasswordOtp(true);
vm.hasResetPasswordOtpFocus(true);
ui.resetForm(form);
});
};
vm.resetPasswordVerify = function (form) {
if (ui.validateForm(form) === false) {
return;
}
const user = vm.resetPasswordUser();
LOG.debug(`resetPasswordVerify, user=${user}`);
return client.resetPassword(protocol.resetPasswordRequest({
locale: i18next.currentLocale(),
newPassword: vm.resetPasswordNew(),
otp: vm.resetPasswordOtp(),
user
})).then(function (reply) {
ui.resetForm(form);
vm.cancelResetPassword();
if (reply.success) {
vm.resetPasswordSuccess(true);
} else {
vm.resetPasswordFailed(true);
}
});
};
vm.activateHelp = function () {
if (vm.enableHelp) {
help();
}
};
vm.isLoadingToken = ko.observable(true);
loadStoredToken();
return Object.freeze(vm);
}
}
});