From 4c3ac097ca2bdb25d2751bd4a4512356ab1037ef Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 9 Nov 2018 09:59:36 +0100 Subject: [PATCH] wip move login logic from dialog to its own component --- .../application/umblogin.directive.js | 433 ++++++++++++++++++ .../src/common/services/user.service.js | 25 +- .../src/controllers/main.controller.js | 17 +- .../components/application/umb-login.html | 271 +++++++++++ .../Umbraco/Views/Default.cshtml | 5 + 5 files changed, 727 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js new file mode 100644 index 0000000000..64c472c4d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -0,0 +1,433 @@ +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbLogin', { + templateUrl: 'views/components/application/umb-login.html', + controller: UmbLoginController, + controllerAs: 'vm', + bindings: { + isTimedOut: "<", + onLogin: "&" + } + }); + + function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { + + const vm = this; + let twoFactorloginDialog = null; + + vm.invitedUser = null; + + vm.invitedUserPasswordModel = { + password: "", + confirmPassword: "", + buttonState: "", + passwordPolicies: null, + passwordPolicyText: "" + }; + + vm.loginStates = { + submitButton: "init" + }; + + vm.avatarFile = { + filesHolder: null, + uploadStatus: null, + uploadProgress: 0, + maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", + acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), + uploaded: false + }; + + vm.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + vm.errorMsg = ""; + vm.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + vm.externalLoginProviders = externalLoginInfo.providers; + vm.externalLoginInfo = externalLoginInfo; + vm.resetPasswordCodeInfo = resetPasswordCodeInfo; + vm.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; + + vm.$onInit = onInit; + vm.togglePassword = togglePassword; + vm.changeAvatar = changeAvatar; + vm.getStarted = getStarted; + vm.inviteSavePassword = inviteSavePassword; + vm.showLogin = showLogin; + vm.showRequestPasswordReset = showRequestPasswordReset; + vm.showSetPassword = showSetPassword; + vm.loginSubmit = loginSubmit; + vm.requestPasswordResetSubmit = requestPasswordResetSubmit; + + function onInit() { + + // Check if it is a new user + const inviteVal = $location.search().invite; + //1 = enter password, 2 = password set, 3 = invalid token + if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { + + $q.all([ + //get the current invite user + authResource.getCurrentInvitedUser().then(function (data) { + vm.invitedUser = data; + }, + function () { + //it failed so we should remove the search + $location.search('invite', null); + }), + //get the membership provider config for password policies + authResource.getMembershipProviderConfig().then(function (data) { + vm.invitedUserPasswordModel.passwordPolicies = data; + + //localize the text + localizationService.localize("errorHandling_errorInPasswordFormat", [ + vm.invitedUserPasswordModel.passwordPolicies.minPasswordLength, + vm.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars + ]).then(function (data) { + vm.invitedUserPasswordModel.passwordPolicyText = data; + }); + }) + ]).then(function () { + vm.inviteStep = Number(inviteVal); + }); + + } else if (inviteVal && inviteVal === "3") { + vm.inviteStep = Number(inviteVal); + } + + // set the welcome greeting + setGreeting(); + + // show the correct panel + if (vm.resetPasswordCodeInfo.resetCodeModel) { + vm.showSetPassword(); + } + else if (vm.resetPasswordCodeInfo.errors.length > 0) { + vm.view = "password-reset-code-expired"; + } + else { + vm.showLogin(); + } + + } + + function togglePassword() { + var elem = $("form[name='vm.loginForm'] input[name='password']"); + elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); + $(".password-text.show, .password-text.hide").toggle(); + } + + function changeAvatar(files, event) { + if (files && files.length > 0) { + upload(files[0]); + } + } + + function getStarted() { + $location.search('invite', null); + submit(true); + } + + function inviteSavePassword () { + + if (formHelper.submitForm({ scope: $scope })) { + + vm.invitedUserPasswordModel.buttonState = "busy"; + + currentUserResource.performSetInvitedUserPassword(vm.invitedUserPasswordModel.password) + .then(function (data) { + + //success + formHelper.resetForm({ scope: $scope }); + vm.invitedUserPasswordModel.buttonState = "success"; + //set the user and set them as logged in + vm.invitedUser = data; + userService.setAuthenticationSuccessful(data); + + vm.inviteStep = 2; + + }, function (err) { + formHelper.handleError(err); + vm.invitedUserPasswordModel.buttonState = "error"; + }); + } + } + + function showLogin() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "login"; + setFieldFocus("loginForm", "username"); + } + + function showRequestPasswordReset() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "request-password-reset"; + vm.showEmailResetConfirmation = false; + setFieldFocus("requestPasswordResetForm", "email"); + } + + function showSetPassword() { + vm.errorMsg = ""; + resetInputValidation(); + vm.view = "set-password"; + setFieldFocus("setPasswordForm", "password"); + } + + function loginSubmit(login, password) { + + //TODO: Do validation properly like in the invite password update + + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (login && password && login.length > 0 && password.length > 0) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + + if (vm.loginForm.$invalid) { + return; + } + + vm.loginStates.submitButton = "busy"; + + userService.authenticate(login, password) + .then(function (data) { + vm.loginStates.submitButton = "success"; + if(vm.onLogin) { + vm.onLogin(); + } + }, + function (reason) { + + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView, submit); + } + else { + vm.loginStates.submitButton = "error"; + vm.errorMsg = reason.errorMsg; + + //set the form inputs to invalid + vm.loginForm.username.$setValidity("auth", false); + vm.loginForm.password.$setValidity("auth", false); + } + }); + + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + vm.loginForm.username.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + vm.loginForm.password.$viewChangeListeners.push(function () { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } + + function requestPasswordResetSubmit(email) { + + //TODO: Do validation properly like in the invite password update + + if (email && email.length > 0) { + vm.requestPasswordResetForm.email.$setValidity('auth', true); + } + + vm.showEmailResetConfirmation = false; + + if (vm.requestPasswordResetForm.$invalid) { + return; + } + + vm.errorMsg = ""; + + authResource.performRequestPasswordReset(email) + .then(function () { + //remove the email entered + vm.email = ""; + vm.showEmailResetConfirmation = true; + }, function (reason) { + vm.errorMsg = reason.errorMsg; + vm.requestPasswordResetForm.email.$setValidity("auth", false); + }); + + vm.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if (vm.requestPasswordResetForm.email.$invalid) { + vm.requestPasswordResetForm.email.$setValidity('auth', true); + } + }); + } + + function setPasswordSubmit(password, confirmPassword) { + + vm.showSetPasswordConfirmation = false; + + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + vm.setPasswordForm.password.$setValidity('auth', true); + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + + if (vm.setPasswordForm.$invalid) { + return; + } + + //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword + authResource.performSetPassword(vm.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, vm.resetPasswordCodeInfo.resetCodeModel.resetCode) + .then(function () { + vm.showSetPasswordConfirmation = true; + vm.resetComplete = true; + + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + + }, function (reason) { + if (reason.data && reason.data.Message) { + vm.errorMsg = reason.data.Message; + } + else { + vm.errorMsg = reason.errorMsg; + } + vm.setPasswordForm.password.$setValidity("auth", false); + vm.setPasswordForm.confirmPassword.$setValidity("auth", false); + }); + + vm.setPasswordForm.password.$viewChangeListeners.push(function () { + if (vm.setPasswordForm.password.$invalid) { + vm.setPasswordForm.password.$setValidity('auth', true); + } + }); + + vm.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if (vm.setPasswordForm.confirmPassword.$invalid) { + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + }); + } + + + //// + + function setGreeting() { + const date = new Date(); + localizationService.localize("login_greeting" + date.getDay()).then(function (label) { + $scope.greeting = label; + }); + } + + function upload(file) { + + vm.avatarFile.uploadProgress = 0; + + Upload.upload({ + url: umbRequestHelper.getApiUrl("currentUserApiBaseUrl", "PostSetAvatar"), + fields: {}, + file: file + }).progress(function (evt) { + + if (vm.avatarFile.uploadStatus !== "done" && vm.avatarFile.uploadStatus !== "error") { + // set uploading status on file + vm.avatarFile.uploadStatus = "uploading"; + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + vm.avatarFile.uploadProgress = progressPercentage; + } + + }).success(function (data, status, headers, config) { + + vm.avatarFile.uploadProgress = 100; + + // set done status on file + vm.avatarFile.uploadStatus = "done"; + + vm.invitedUser.avatars = data; + + vm.avatarFile.uploaded = true; + + }).error(function (evt, status, headers, config) { + + // set status done + vm.avatarFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.avatarFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + vm.avatarFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + vm.avatarFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + vm.avatarFile.serverErrorMessage = evt.Message; + } + } + }); + } + + function setFieldFocus(form, field) { + $timeout(function () { + $("form[name='" + form + "'] input[name='" + field + "']").focus(); + }); + } + + function show2FALoginDialog(view, callback) { + if (!twoFactorloginDialog) { + twoFactorloginDialog = dialogService.open({ + //very special flag which means that global events cannot close this dialog + manualClose: true, + template: view, + modalClass: "login-overlay", + animation: "slide", + show: true, + callback: callback + }); + } + } + + function resetInputValidation() { + vm.confirmPassword = ""; + vm.password = ""; + vm.login = ""; + if (vm.loginForm) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + if (vm.requestPasswordResetForm) { + vm.requestPasswordResetForm.email.$setValidity("auth", true); + } + if (vm.setPasswordForm) { + vm.setPasswordForm.password.$setValidity('auth', true); + vm.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + } + + + + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 142cbe5e7c..63770e7bce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -3,34 +3,18 @@ angular.module('umbraco.services') var currentUser = null; var lastUserId = null; - var loginDialog = null; //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server // this is used so that we know when to go and get the user's remaining seconds directly. var lastServerTimeoutSet = null; function openLoginDialog(isTimedOut) { - if (!loginDialog) { - loginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - - template: 'views/common/dialogs/login.html', - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: onLoginDialogClose, - dialogData: { - isTimedOut: isTimedOut - } - }); - } + //broadcast a global event that the user is no longer logged in + const args = { isTimedOut: isTimedOut }; + eventsService.emit("app.notAuthenticated", args); } function onLoginDialogClose(success) { - loginDialog = null; - if (success) { requestRetryQueue.retryAll(currentUser.name); } @@ -164,9 +148,6 @@ angular.module('umbraco.services') lastServerTimeoutSet = null; currentUser = null; - //broadcast a global event that the user is no longer logged in - eventsService.emit("app.notAuthenticated"); - openLoginDialog(isLogout === undefined ? true : !isLogout); } diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index a10943c17e..1b76a37a73 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -17,6 +17,7 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.overlay = {}; $scope.drawer = {}; $scope.search = {}; + $scope.login = {}; $scope.removeNotification = function (index) { notificationsService.remove(index); @@ -48,11 +49,19 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function () { + evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; + + // show the login screen + if(data) { + $scope.login.isTimedOut = data.isTimedOut; + } + + $scope.login.show = true; + })); evts.push(eventsService.on("app.userRefresh", function(evt) { @@ -72,6 +81,10 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.authenticated = data.authenticated; $scope.user = data.user; + if($scope.authenticated === true) { + $scope.login.show = false; + } + updateChecker.check().then(function (update) { if (update && update !== "null") { if (update.type !== "None") { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html new file mode 100644 index 0000000000..b589f179ba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -0,0 +1,271 @@ +
+ +
+ + + + + + + + +
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml index 3cdf65ffd8..66e5468eeb 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml @@ -112,6 +112,11 @@ view="ysodOverlay.view"> + + + @Html.BareMinimumServerVariablesScript(Url, Url.Action("ExternalLogin", "BackOffice", new { area = ViewBag.UmbracoPath }), Model.Features, UmbracoConfig.For.GlobalSettings())