From 4cf1f29e0a778fc990a22a966d0884169581b717 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 Nov 2017 23:17:05 +0100 Subject: [PATCH] Closes #161: password recovery by email --- .../main/java/com/sismics/util/EmailUtil.java | 4 +- .../main/resources/email_template/layout.ftl | 2 +- .../docs/rest/resource/AppResource.java | 71 ++++++++++--------- docs-web/src/main/webapp/src/app/docs/app.js | 9 +++ .../webapp/src/app/docs/controller/Login.js | 12 ++-- .../docs/controller/LoginModalPasswordLost.js | 6 +- .../src/app/docs/controller/PasswordReset.js | 20 ++++++ .../controller/settings/SettingsConfig.js | 21 ++++-- docs-web/src/main/webapp/src/index.html | 1 + docs-web/src/main/webapp/src/locale/en.json | 19 ++++- .../webapp/src/partial/docs/passwordlost.html | 4 +- .../src/partial/docs/passwordreset.html | 25 +++++++ .../src/partial/docs/settings.config.html | 50 +++++++++++++ 13 files changed, 188 insertions(+), 56 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js create mode 100644 docs-web/src/main/webapp/src/partial/docs/passwordreset.html diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index 13ddefbc..af6c88fa 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -75,6 +75,8 @@ public class EmailUtil { email.setCharset("UTF-8"); email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME)); email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT)); + email.setAuthentication(ConfigUtil.getConfigStringValue(ConfigType.SMTP_USERNAME), + ConfigUtil.getConfigStringValue(ConfigType.SMTP_PASSWORD)); email.addTo(recipientUser.getEmail(), recipientUser.getUsername()); ConfigDao configDao = new ConfigDao(); Config themeConfig = configDao.getById(ConfigType.THEME); @@ -87,7 +89,7 @@ public class EmailUtil { } email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName); java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV)); - email.setSubject(subject); + email.setSubject(appName + " - " + subject); email.setTextMsg(MessageUtil.getMessage(userLocale, "email.no_html.error")); // Add automatic parameters diff --git a/docs-core/src/main/resources/email_template/layout.ftl b/docs-core/src/main/resources/email_template/layout.ftl index 74a8dd09..0ceae478 100644 --- a/docs-core/src/main/resources/email_template/layout.ftl +++ b/docs-core/src/main/resources/email_template/layout.ftl @@ -2,7 +2,7 @@ diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java index 0586d6bd..ca422cbe 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java @@ -1,32 +1,11 @@ package com.sismics.docs.rest.resource; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.ResourceBundle; - -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.persistence.EntityManager; -import javax.persistence.Query; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; - +import com.google.common.base.Strings; import com.sismics.docs.core.constant.ConfigType; -import com.sismics.docs.core.dao.jpa.*; +import com.sismics.docs.core.dao.jpa.ConfigDao; +import com.sismics.docs.core.dao.jpa.FileDao; +import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.event.RebuildIndexAsyncEvent; -import com.sismics.rest.util.ValidationUtil; -import org.apache.commons.lang.StringUtils; -import org.apache.log4j.Appender; -import org.apache.log4j.Level; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.ConfigUtil; @@ -36,10 +15,28 @@ import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; +import com.sismics.rest.util.ValidationUtil; import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.log4j.LogCriteria; import com.sismics.util.log4j.LogEntry; import com.sismics.util.log4j.MemoryAppender; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Appender; +import org.apache.log4j.Level; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.util.*; /** * General app REST resource. @@ -126,6 +123,7 @@ public class AppResource extends BaseResource { * @apiParam {String} username SMTP username * @apiParam {String} password SMTP password * @apiError (client) ForbiddenError Access denied + * @apiError (client) ValidationError Validation error * @apiPermission admin * @apiVersion 1.5.0 * @@ -147,18 +145,25 @@ public class AppResource extends BaseResource { throw new ForbiddenClientException(); } checkBaseFunction(BaseFunction.ADMIN); - ValidationUtil.validateRequired(hostname, "hostname"); - ValidationUtil.validateInteger(portStr, "port"); - ValidationUtil.validateRequired(from, "from"); + if (!Strings.isNullOrEmpty(portStr)) { + ValidationUtil.validateInteger(portStr, "port"); + } + // Just update the changed configuration ConfigDao configDao = new ConfigDao(); - configDao.update(ConfigType.SMTP_HOSTNAME, hostname); - configDao.update(ConfigType.SMTP_PORT, portStr); - configDao.update(ConfigType.SMTP_FROM, from); - if (username != null) { + if (!Strings.isNullOrEmpty(hostname)) { + configDao.update(ConfigType.SMTP_HOSTNAME, hostname); + } + if (!Strings.isNullOrEmpty(portStr)) { + configDao.update(ConfigType.SMTP_PORT, portStr); + } + if (!Strings.isNullOrEmpty(from)) { + configDao.update(ConfigType.SMTP_FROM, from); + } + if (!Strings.isNullOrEmpty(username)) { configDao.update(ConfigType.SMTP_USERNAME, username); } - if (password != null) { + if (!Strings.isNullOrEmpty(password)) { configDao.update(ConfigType.SMTP_PASSWORD, password); } diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 707bef6a..bcaa03b7 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -28,6 +28,15 @@ angular.module('docs', } } }) + .state('passwordreset', { + url: '/passwordreset/:key', + views: { + 'page': { + templateUrl: 'partial/docs/passwordreset.html', + controller: 'PasswordReset' + } + } + }) .state('tag', { url: '/tag', abstract: true, diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Login.js b/docs-web/src/main/webapp/src/app/docs/controller/Login.js index 7b008144..32b3a000 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/Login.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/Login.js @@ -46,22 +46,22 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc $uibModal.open({ templateUrl: 'partial/docs/passwordlost.html', controller: 'LoginModalPasswordLost' - }).result.then(function (email) { - if (name === null) { + }).result.then(function (username) { + if (username === null) { return; } // Send a password lost email - Restangular.one('user').post('passwordLost', { - email: email + Restangular.one('user').post('password_lost', { + username: username }).then(function () { var title = $translate.instant('login.password_lost_sent_title'); - var msg = $translate.instant('login.password_lost_sent_message', { email: email }); + var msg = $translate.instant('login.password_lost_sent_message', { username: username }); var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}]; $dialog.messageBox(title, msg, btns); }, function () { var title = $translate.instant('login.password_lost_error_title'); - var msg = $translate.instant('login.password_lost_error_message', { email: email }); + var msg = $translate.instant('login.password_lost_error_message'); var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}]; $dialog.messageBox(title, msg, btns); }); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js b/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js index 534b9f46..16517457 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js @@ -4,8 +4,8 @@ * Login modal password lost controller. */ angular.module('docs').controller('LoginModalPasswordLost', function ($scope, $uibModalInstance) { - $scope.email = ''; - $scope.close = function(name) { - $uibModalInstance.close(name); + $scope.username = ''; + $scope.close = function(username) { + $uibModalInstance.close(username); } }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js b/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js new file mode 100644 index 00000000..f525ac92 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js @@ -0,0 +1,20 @@ +'use strict'; + +/** + * Password reset controller. + */ +angular.module('docs').controller('PasswordReset', function($scope, Restangular, $state, $stateParams, $translate, $dialog) { + $scope.submit = function () { + Restangular.one('user').post('password_reset', { + key: $stateParams.key, + password: $scope.password + }).then(function () { + $state.go('login'); + }, function () { + var title = $translate.instant('passwordreset.error_title'); + var msg = $translate.instant('passwordreset.error_message'); + var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}]; + $dialog.messageBox(title, msg, btns); + }); + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js index 7efaac1d..32d55dd0 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js @@ -5,29 +5,29 @@ */ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) { // Get the app configuration - Restangular.one('app').get().then(function(data) { + Restangular.one('app').get().then(function (data) { $scope.app = data; }); // Enable/disable guest login - $scope.changeGuestLogin = function(enabled) { + $scope.changeGuestLogin = function (enabled) { Restangular.one('app').post('guest_login', { enabled: enabled - }).then(function() { + }).then(function () { $scope.app.guest_login = enabled; }); }; // Fetch the current theme configuration - Restangular.one('theme').get().then(function(data) { + Restangular.one('theme').get().then(function (data) { $scope.theme = data; $rootScope.appName = $scope.theme.name; }); // Update the theme - $scope.update = function() { + $scope.update = function () { $scope.theme.name = $scope.theme.name.length === 0 ? 'Sismics Docs' : $scope.theme.name; - Restangular.one('theme').post('', $scope.theme).then(function() { + Restangular.one('theme').post('', $scope.theme).then(function () { var stylesheet = $('#theme-stylesheet')[0]; stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime()); $rootScope.appName = $scope.theme.name; @@ -36,7 +36,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, // Send an image $scope.sendingImage = false; - $scope.sendImage = function(type, image) { + $scope.sendImage = function (type, image) { // Build the payload var formData = new FormData(); formData.append('image', image); @@ -64,4 +64,11 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, } }); }; + + // Edit SMTP config + $scope.editSmtpConfig = function () { + Restangular.one('app').post('config_smtp', $scope.smtp).then(function () { + $scope.smtpUpdated = true; + }); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index fa1ac830..3d237b6e 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -49,6 +49,7 @@ + diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 11723edb..fc54b550 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -12,15 +12,21 @@ "login_failed_message": "Username or password invalid", "password_lost_btn": "Password lost?", "password_lost_sent_title": "Password reset email sent", - "password_lost_sent_message": "An email has been sent to {{ email }} to reset your password", + "password_lost_sent_message": "An email has been sent to {{ username }} to reset your password", "password_lost_error_title": "Password reset error", "password_lost_error_message": "Unable to send a password reset email, please contact your administrator for a manual reset" }, "passwordlost": { "title": "Password lost", - "message": "Please enter your email address to receive a password reset link", + "message": "Please enter your username to receive a password reset link. If you don't remember your username, please contact your administrator", "submit": "Reset my password" }, + "passwordreset": { + "message": "Please enter a new password", + "submit": "Change my password", + "error_title": "Error changing your password", + "error_message": "Your password recovery request is expired, please ask a new one on the login page" + }, "index": { "toggle_navigation": "Toggle navigation", "nav_documents": "Documents", @@ -295,7 +301,14 @@ "custom_css_placeholder": "Custom CSS to add after the main stylesheet", "logo": "Logo (squared size)", "background_image": "Background image", - "uploading_image": "Uploading the image..." + "uploading_image": "Uploading the image...", + "title_smtp": "Email configuration", + "smtp_hostname": "SMTP hostname", + "smtp_port": "SMTP port", + "smtp_from": "Sender e-mail", + "smtp_username": "SMTP username", + "smtp_password": "SMTP password", + "smtp_updated": "SMTP configuration updated successfully" }, "log": { "title": "Server logs", diff --git a/docs-web/src/main/webapp/src/partial/docs/passwordlost.html b/docs-web/src/main/webapp/src/partial/docs/passwordlost.html index 73d042c0..2bb1baac 100644 --- a/docs-web/src/main/webapp/src/partial/docs/passwordlost.html +++ b/docs-web/src/main/webapp/src/partial/docs/passwordlost.html @@ -5,11 +5,11 @@ + + +

+
{{ 'settings.config.smtp_updated' | translate }}
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ {{ 'validation.email' | translate }} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
\ No newline at end of file
- ${base_url} + ${app_name}