mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-07-15 21:34:53 +00:00
feat(password-recovery): Add password recovery with secret question or secondary email
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -20,6 +20,8 @@
|
||||
PolicyPasswordTooShort: 6,
|
||||
PolicyPasswordTooYoung: 7,
|
||||
PolicyPasswordInHistory: 8,
|
||||
PolicyPasswordRecoveryFailed: 9,
|
||||
PolicyPasswordRecoveryInvalidToken: 10,
|
||||
PolicyNoError: 65535
|
||||
})
|
||||
|
||||
@@ -168,7 +170,7 @@
|
||||
return d.promise;
|
||||
}, // login: function(data) { ...
|
||||
|
||||
changePassword: function(userName, domain, newPassword, oldPassword) {
|
||||
changePassword: function(userName, domain, newPassword, oldPassword, token) {
|
||||
var d = $q.defer(),
|
||||
xsrfCookie = $cookies.get('XSRF-TOKEN');
|
||||
|
||||
@@ -180,7 +182,7 @@
|
||||
headers: {
|
||||
'X-XSRF-TOKEN' : xsrfCookie
|
||||
},
|
||||
data: { userName: userName, newPassword: newPassword, oldPassword: oldPassword }
|
||||
data: { userName: userName, newPassword: newPassword, oldPassword: oldPassword, token: token }
|
||||
}).then(function() {
|
||||
d.resolve({url: redirectUrl(userName, domain)});
|
||||
}, function(response) {
|
||||
@@ -205,6 +207,8 @@
|
||||
error = l("Password change failed - Password is too young");
|
||||
} else if (perr == passwordPolicyConfig.PolicyPasswordInHistory) {
|
||||
error = l("Password change failed - Password is in history");
|
||||
} else if (perr == passwordPolicyConfig.PolicyPasswordRecoveryInvalidToken) {
|
||||
error = l("Invalid token. Could not change password");
|
||||
} else {
|
||||
error = l("Unhandled policy error: %{0}").formatted(perr);
|
||||
perr = passwordPolicyConfig.PolicyPasswordUnknown;
|
||||
@@ -215,6 +219,113 @@
|
||||
d.reject(error);
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
passwordRecovery: function (userName, domain) {
|
||||
const self = this;
|
||||
|
||||
var d = $q.defer(),
|
||||
xsrfCookie = $cookies.get('XSRF-TOKEN');
|
||||
|
||||
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
|
||||
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/SOGo/so/passwordRecovery',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': xsrfCookie
|
||||
},
|
||||
data: { userName: userName, domain: domain }
|
||||
}).then(function (response) {
|
||||
d.resolve(Object.assign(
|
||||
{ url: redirectUrl(userName, domain) },
|
||||
response.data,
|
||||
'SecretQuestion' === response.data.mode ? { secretQuestionLabel: l('passwordRecovery_' + response.data.secretQuestion) } : {},
|
||||
'SecondaryEmail' === response.data.mode ? { obfuscatedRecoveryEmail: response.data.obfuscatedSecondaryEmail } : {}
|
||||
));
|
||||
}, function () {
|
||||
// Restore the cookie
|
||||
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
|
||||
d.reject(l("Unhandled policy error: %{0}").formatted(passwordPolicyConfig.PolicyPasswordRecoveryFailed));
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
|
||||
passwordRecoveryEmail: function (userName, domain, mode, mailDomain) {
|
||||
const self = this;
|
||||
|
||||
var d = $q.defer(),
|
||||
xsrfCookie = $cookies.get('XSRF-TOKEN');
|
||||
|
||||
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
|
||||
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/SOGo/so/passwordRecoveryEmail',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': xsrfCookie
|
||||
},
|
||||
data: { userName: userName, domain: domain, mode: mode, mailDomain: mailDomain }
|
||||
}).then(function (response) {
|
||||
d.resolve(response.data.jwt);
|
||||
}, function (response) {
|
||||
// Restore the cookie
|
||||
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
|
||||
d.reject(l(response.data));
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
|
||||
passwordRecoveryCheck: function (userName, domain, mode, question, answer, mailDomain) {
|
||||
const self = this;
|
||||
|
||||
var d = $q.defer(),
|
||||
xsrfCookie = $cookies.get('XSRF-TOKEN');
|
||||
|
||||
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
|
||||
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/SOGo/so/passwordRecoveryCheck',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': xsrfCookie
|
||||
},
|
||||
data: { userName: userName, domain: domain, mode: mode, question: question, answer: answer, mailDomain: mailDomain }
|
||||
}).then(function (response) {
|
||||
d.resolve(response.data.jwt);
|
||||
}, function (response) {
|
||||
// Restore the cookie
|
||||
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
|
||||
d.reject(l(response.data));
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
passwordRecoveryEnabled: function (userName, domain) {
|
||||
const self = this;
|
||||
|
||||
var d = $q.defer(),
|
||||
xsrfCookie = $cookies.get('XSRF-TOKEN');
|
||||
|
||||
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
|
||||
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/SOGo/so/passwordRecoveryEnabled',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': xsrfCookie
|
||||
},
|
||||
data: { userName: userName, domain: domain }
|
||||
}).then(function (response) {
|
||||
d.resolve(response.data.domain);
|
||||
}, function () {
|
||||
// Restore the cookie
|
||||
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
|
||||
d.reject();
|
||||
});
|
||||
return d.promise;
|
||||
}
|
||||
};
|
||||
return service;
|
||||
|
||||
@@ -20,14 +20,43 @@
|
||||
</md-dialog>
|
||||
|
||||
*/
|
||||
sgRippleClick.$inject = ['$log', '$timeout'];
|
||||
function sgRippleClick($log, $timeout) {
|
||||
sgRippleClick.$inject = ['$log', '$timeout', '$rootScope'];
|
||||
function sgRippleClick($log, $timeout, scope) {
|
||||
|
||||
function rippleEffect(element, coordinates, container, content) {
|
||||
// Show ripple
|
||||
angular.element(container).css({ 'overflow': 'hidden', 'position': 'relative' });
|
||||
angular.element(content).css({ top: container.scrollTop + 'px' });
|
||||
|
||||
element.css({
|
||||
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
|
||||
'left': (coordinates.left - container.offsetLeft) + 'px',
|
||||
'height': '400vmin',
|
||||
'width': '400vmin'
|
||||
});
|
||||
|
||||
// Show ripple content
|
||||
content.classList.remove('ng-hide');
|
||||
}
|
||||
|
||||
scope.$on('sgRippleDo', function (e, containerName) {
|
||||
const container = document.getElementById(containerName);
|
||||
container.classList.remove('ng-hide');
|
||||
rippleEffect(
|
||||
angular.element(document.querySelector('sg-ripple'))
|
||||
, { left: (window.innerWidth / 2), top: (window.innerHeight / 2) }
|
||||
, container
|
||||
, document.querySelector('sg-ripple-content')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: compile
|
||||
};
|
||||
|
||||
|
||||
|
||||
function compile(tElement, tAttrs) {
|
||||
|
||||
return function postLink(scope, element, attr) {
|
||||
@@ -90,26 +119,16 @@
|
||||
}
|
||||
|
||||
if (content.classList.contains('ng-hide')) {
|
||||
// Show ripple
|
||||
angular.element(container).css({ 'overflow': 'hidden', 'position': 'relative' });
|
||||
angular.element(content).css({ top: container.scrollTop + 'px' });
|
||||
$timeout(function() {
|
||||
// Wait until next digest for CSS animation to work
|
||||
ripple.css({
|
||||
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
|
||||
'left': (coordinates.left - container.offsetLeft) + 'px',
|
||||
'height': '400vmin',
|
||||
'width': '400vmin'
|
||||
});
|
||||
// Show ripple content
|
||||
content.classList.remove('ng-hide');
|
||||
rippleEffect(ripple, coordinates, container, content);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Hide ripple layer
|
||||
ripple.css({
|
||||
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
|
||||
'left': (coordinates.left - container.offsetLeft) + 'px',
|
||||
'left': (coordinates.left - container.offsetLeft) + 'px',
|
||||
'height': '0px',
|
||||
'width': '0px'
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* sgFocus - A service to set the focus on the element associated to a specific string
|
||||
* @memberof SOGo.Common
|
||||
* @param {string} name - the string identifier of the element
|
||||
* @see {@link SOGo.Common.sgRippleClick}
|
||||
* @ngInject
|
||||
*/
|
||||
sgRippleClick.$inject = ['$rootScope', '$timeout'];
|
||||
function sgRippleClick($rootScope, $timeout) {
|
||||
return function (containerName) {
|
||||
$timeout(function() {
|
||||
$rootScope.$broadcast('sgRippleDo', containerName);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
angular
|
||||
.module('SOGo.Common')
|
||||
.factory('sgRippleClick', sgRippleClick);
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,19 +1,19 @@
|
||||
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* JavaScript for MainUI (SOGoRootPage) */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular.module('SOGo.MainUI', ['SOGo.Common', 'SOGo.Authentication']);
|
||||
const PASSWORD_RECOVERY_TIMER_MS = 2000;
|
||||
|
||||
/**
|
||||
* @ngInject
|
||||
*/
|
||||
LoginController.$inject = ['$scope', '$window', '$timeout', 'Dialog', '$mdDialog', 'Authentication', 'sgFocus'];
|
||||
function LoginController($scope, $window, $timeout, Dialog, $mdDialog, Authentication, focus) {
|
||||
LoginController.$inject = ['$scope', '$window', '$timeout', 'Dialog', '$mdDialog', 'Authentication', 'sgFocus', 'sgRippleClick'];
|
||||
function LoginController($scope, $window, $timeout, Dialog, $mdDialog, Authentication, focus, rippleDo) {
|
||||
var vm = this;
|
||||
|
||||
this.$onInit = function() {
|
||||
this.$onInit = function () {
|
||||
this.creds = {
|
||||
username: $window.cookieUsername,
|
||||
password: null,
|
||||
@@ -31,9 +31,53 @@
|
||||
// Password policy - change expired password
|
||||
this.passwords = { newPassword: null, newPasswordConfirmation: null, oldPassword: null };
|
||||
|
||||
// Password recovery
|
||||
this.passwordRecovery = {
|
||||
passwordRecoveryEnabled: false,
|
||||
passwordRecoveryQuestionKey: null,
|
||||
passwordRecoveryQuestion: null,
|
||||
passwordRecoveryMode: null,
|
||||
passwordRecoveryQuestionAnswer: null,
|
||||
passwordRecoveryToken: null,
|
||||
passwordRecoveryLinkTimer: null,
|
||||
passwordRecoverySecondaryEmailText: null,
|
||||
passwordRecoveryMailDomain: null
|
||||
};
|
||||
|
||||
// Show login once everything is initialized
|
||||
this.showLogin = false;
|
||||
$timeout(function() { vm.showLogin = true; }, 100);
|
||||
$timeout(function () {
|
||||
vm.showLogin = true;
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
let token = urlParams.get('token');
|
||||
|
||||
if (0 < window.location.pathname.indexOf("passwordRecoveryEmail") && token) {
|
||||
token = token.replace(/\//g, ''); // remove trailing '/'
|
||||
const tokenArray = token.split(".");
|
||||
|
||||
// Retrieve info from token
|
||||
if (3 === tokenArray.length) {
|
||||
vm.passwordRecovery.passwordRecoveryToken = token;
|
||||
const info = JSON.parse(atob(tokenArray[1]));
|
||||
vm.creds.username = info.username;
|
||||
vm.creds.domain = info.domain;
|
||||
vm.passwordRecovery.passwordRecoveryToken = token;
|
||||
vm.passwordRecovery.passwordRecoveryMode = "SecondaryEmail";
|
||||
vm.passwordRecovery.passwordRecoveryEnabled = true;
|
||||
|
||||
vm.loginState = 'passwordchange';
|
||||
vm.showLogin = false;
|
||||
rippleDo('loginContent');
|
||||
}
|
||||
|
||||
} else {
|
||||
vm.retrievePasswordRecoveryEnabled();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
};
|
||||
|
||||
this.login = function() {
|
||||
@@ -98,7 +142,7 @@
|
||||
vm.errorMessage = l('Your password is going to expire in %{0} %{1}.', value, string);
|
||||
}
|
||||
else if (msg.passwordexpired) {
|
||||
vm.loginState = 'passwordexpired';
|
||||
vm.loginState = 'passwordchange';
|
||||
vm.url = msg.url;
|
||||
}
|
||||
|
||||
@@ -109,6 +153,9 @@
|
||||
this.restoreLogin = function() {
|
||||
vm.loginState = false;
|
||||
delete vm.creds.verificationCode;
|
||||
if (vm.isInPasswordRecoveryMode()) {
|
||||
vm.passwordRecoveryAbort();
|
||||
}
|
||||
};
|
||||
|
||||
this.continueLogin = function() {
|
||||
@@ -138,6 +185,10 @@
|
||||
$window.location.href = ApplicationBaseURL + 'login?language=' + this.creds.language;
|
||||
};
|
||||
|
||||
this.hello = function (form) {
|
||||
return !true;
|
||||
}
|
||||
|
||||
this.canChangePassword = function(form) {
|
||||
if (this.passwords.newPasswordConfirmation && this.passwords.newPasswordConfirmation.length &&
|
||||
this.passwords.newPassword != this.passwords.newPasswordConfirmation) {
|
||||
@@ -150,14 +201,15 @@
|
||||
if (this.passwords.newPassword && this.passwords.newPassword.length > 0 &&
|
||||
this.passwords.newPasswordConfirmation && this.passwords.newPasswordConfirmation.length &&
|
||||
this.passwords.newPassword == this.passwords.newPasswordConfirmation &&
|
||||
this.passwords.oldPassword && this.passwords.oldPassword.length > 0)
|
||||
((this.isInPasswordRecoveryMode()) ||
|
||||
(!this.loginState && this.passwords.oldPassword && this.passwords.oldPassword.length > 0)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.changePassword = function() {
|
||||
Authentication.changePassword(this.creds.username, this.creds.domain, this.passwords.newPassword, this.passwords.oldPassword).then(function(data) {
|
||||
Authentication.changePassword(this.creds.username, this.creds.domain, this.passwords.newPassword, this.passwords.oldPassword, this.passwordRecovery.passwordRecoveryToken).then(function(data) {
|
||||
vm.loginState = 'message';
|
||||
vm.url = data.url;
|
||||
vm.errorMessage = l('The password was changed successfully.');
|
||||
@@ -167,6 +219,95 @@
|
||||
});
|
||||
};
|
||||
|
||||
this.passwordRecoveryInfo = function () {
|
||||
vm.loginState = 'passwordrecovery';
|
||||
Authentication.passwordRecovery(this.creds.username, this.creds.domain).then(function (data) {
|
||||
vm.passwordRecovery.passwordRecoveryMode = data.mode;
|
||||
if ('SecretQuestion' === data.mode) {
|
||||
vm.passwordRecovery.passwordRecoveryQuestion = data.secretQuestionLabel;
|
||||
vm.passwordRecovery.passwordRecoveryQuestionKey = data.secretQuestion;
|
||||
} else if ('SecondaryEmail' === data.mode) {
|
||||
vm.passwordRecovery.passwordRecoverySecondaryEmailText = l("A link will be sent to %{0}", data.obfuscatedRecoveryEmail);
|
||||
} else if ('Disabled' === data.mode) {
|
||||
vm.loginState = 'error';
|
||||
vm.errorMessage = l('No password recovery method has been defined for this user');
|
||||
}
|
||||
}, function (msg) {
|
||||
vm.loginState = 'error';
|
||||
vm.errorMessage = msg;
|
||||
});
|
||||
};
|
||||
|
||||
this.passwordRecoveryEmail = function () {
|
||||
Authentication.passwordRecoveryEmail(this.creds.username, this.creds.domain
|
||||
, this.passwordRecovery.passwordRecoveryMode
|
||||
, this.passwordRecovery.passwordRecoveryMailDomain).then(function () {
|
||||
vm.loginState = 'sendrecoverymail';
|
||||
}, function (msg) {
|
||||
vm.loginState = 'error';
|
||||
vm.errorMessage = msg;
|
||||
});
|
||||
};
|
||||
|
||||
this.passwordRecoveryCheck = function () {
|
||||
Authentication.passwordRecoveryCheck(this.creds.username, this.creds.domain
|
||||
, this.passwordRecovery.passwordRecoveryMode
|
||||
, this.passwordRecovery.passwordRecoveryQuestionKey
|
||||
, this.passwordRecovery.passwordRecoveryQuestionAnswer
|
||||
, this.passwordRecovery.passwordRecoveryMailDomain).then(function (token) {
|
||||
if ("SecretQuestion" == vm.passwordRecovery.passwordRecoveryMode) {
|
||||
vm.passwordRecovery.passwordRecoveryToken = token;
|
||||
vm.loginState = 'passwordchange';
|
||||
} else if ("SecondaryEmail" == vm.passwordRecovery.passwordRecoveryMode) {
|
||||
vm.loginState = 'sendrecoverymail';
|
||||
}
|
||||
}, function (msg) {
|
||||
vm.loginState = 'error';
|
||||
vm.errorMessage = msg;
|
||||
});
|
||||
};
|
||||
|
||||
this.isInPasswordRecoveryMode = function () {
|
||||
return (("SecretQuestion" == this.passwordRecovery.passwordRecoveryMode ||
|
||||
"SecondaryEmail" == this.passwordRecovery.passwordRecoveryMode) &&
|
||||
this.passwordRecovery.passwordRecoveryToken) ? true : false;
|
||||
};
|
||||
|
||||
this.passwordRecoveryAbort = function () {
|
||||
this.passwords = { newPassword: null, newPasswordConfirmation: null, oldPassword: null };
|
||||
this.loginState = false;
|
||||
this.passwordRecovery.passwordRecoveryEnabled = false;
|
||||
this.passwordRecovery.passwordRecoveryQuestion = null;
|
||||
this.passwordRecovery.passwordRecoveryMode = null;
|
||||
this.passwordRecovery.passwordRecoveryQuestionAnswer = null;
|
||||
this.passwordRecovery.passwordRecoveryToken = null;
|
||||
this.passwordRecovery.passwordRecoverySecondaryEmailText = null;
|
||||
this.passwordRecovery.passwordRecoveryMailDomain = null;
|
||||
$window.location.href = vm.url;
|
||||
};
|
||||
|
||||
this.usernameChanged = function () {
|
||||
if (this.passwordRecovery.passwordRecoveryLinkTimer) {
|
||||
clearTimeout(this.passwordRecovery.passwordRecoveryLinkTimer);
|
||||
}
|
||||
|
||||
this.passwordRecovery.passwordRecoveryLinkTimer = setTimeout(() => {
|
||||
vm.retrievePasswordRecoveryEnabled();
|
||||
this.passwordRecovery.passwordRecoveryLinkTimer = null;
|
||||
}, PASSWORD_RECOVERY_TIMER_MS);
|
||||
};
|
||||
|
||||
this.retrievePasswordRecoveryEnabled = function () {
|
||||
if (this.creds.username || this.creds.domain) {
|
||||
Authentication.passwordRecoveryEnabled(this.creds.username, this.creds.domain).then(function (mailDomain) {
|
||||
vm.passwordRecovery.passwordRecoveryMailDomain = mailDomain;
|
||||
vm.passwordRecovery.passwordRecoveryEnabled = true;
|
||||
}, function () {
|
||||
vm.passwordRecovery.passwordRecoveryEnabled = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
angular
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -393,6 +393,26 @@
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function isItemSecretQuestion
|
||||
* @memberof Preferences.prototype
|
||||
* @desc Check if the customer pick the secret question for e-mail recovery
|
||||
* @returns true if secret question is selected
|
||||
*/
|
||||
Preferences.prototype.isItemSecretQuestion = function() {
|
||||
return "SecretQuestion" == this.defaults.SOGoPasswordRecoveryMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function isItemSecondaryEmail
|
||||
* @memberof Preferences.prototype
|
||||
* @desc Check if the customer pick the email for password recovery
|
||||
* @returns true if email is selected
|
||||
*/
|
||||
Preferences.prototype.isItemSecondaryEmail = function () {
|
||||
return "SecondaryEmail" == this.defaults.SOGoPasswordRecoveryMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function authorizeNotifications
|
||||
* @memberof Preferences.prototype
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
this.onDesktopNotificationsChange = function() {
|
||||
if (this.preferences.defaults.SOGoDesktopNotifications)
|
||||
|
||||
Reference in New Issue
Block a user