From 65937d6f4c7521cdbb9d7632b59c75eb9ec7f5fa Mon Sep 17 00:00:00 2001 From: bgamard Date: Fri, 17 Nov 2017 17:01:08 +0100 Subject: [PATCH] #161: password recovery by email (wip) --- .../docs/core/constant/ConfigType.java | 11 +- .../docs/rest/resource/AppResource.java | 116 ++++++++---------- .../webapp/src/app/docs/controller/Login.js | 31 ++++- .../docs/controller/LoginModalPasswordLost.js | 11 ++ docs-web/src/main/webapp/src/index.html | 1 + docs-web/src/main/webapp/src/locale/en.json | 12 +- .../main/webapp/src/partial/docs/login.html | 6 +- .../webapp/src/partial/docs/passwordlost.html | 17 +++ docs-web/src/main/webapp/src/style/main.less | 4 + .../sismics/docs/rest/TestAppResource.java | 19 +++ 10 files changed, 159 insertions(+), 69 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js create mode 100644 docs-web/src/main/webapp/src/partial/docs/passwordlost.html diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java index 3a0e7020..5d0525ff 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java @@ -18,5 +18,14 @@ public enum ConfigType { /** * Guest login. */ - GUEST_LOGIN + GUEST_LOGIN, + + /** + * SMTP server configuration. + */ + SMTP_HOSTNAME, + SMTP_PORT, + SMTP_FROM, + SMTP_USERNAME, + SMTP_PASSWORD } 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 3a99c08d..0586d6bd 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 @@ -18,20 +18,15 @@ import javax.ws.rs.*; import javax.ws.rs.core.Response; import com.sismics.docs.core.constant.ConfigType; -import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.*; -import com.sismics.docs.core.dao.jpa.criteria.TagCriteria; -import com.sismics.docs.core.dao.jpa.dto.AclDto; -import com.sismics.docs.core.dao.jpa.dto.TagDto; import com.sismics.docs.core.event.RebuildIndexAsyncEvent; -import com.sismics.docs.core.model.jpa.Acl; +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.context.AppContext; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.ConfigUtil; @@ -119,6 +114,57 @@ public class AppResource extends BaseResource { return Response.ok().build(); } + /** + * Configure the SMTP server. + * + * @api {post} /app/config_smtp Configure the SMTP server + * @apiName PostAppConfigSmtp + * @apiGroup App + * @apiParam {String} hostname SMTP hostname + * @apiParam {Integer} port SMTP port + * @apiParam {String} from From address + * @apiParam {String} username SMTP username + * @apiParam {String} password SMTP password + * @apiError (client) ForbiddenError Access denied + * @apiPermission admin + * @apiVersion 1.5.0 + * + * @param hostname SMTP hostname + * @param portStr SMTP port + * @param from From address + * @param username SMTP username + * @param password SMTP password + * @return Response + */ + @POST + @Path("config_smtp") + public Response configSmtp(@FormParam("hostname") String hostname, + @FormParam("port") String portStr, + @FormParam("from") String from, + @FormParam("username") String username, + @FormParam("password") String password) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + ValidationUtil.validateRequired(hostname, "hostname"); + ValidationUtil.validateInteger(portStr, "port"); + ValidationUtil.validateRequired(from, "from"); + + 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) { + configDao.update(ConfigType.SMTP_USERNAME, username); + } + if (password != null) { + configDao.update(ConfigType.SMTP_PASSWORD, password); + } + + return Response.ok().build(); + } + /** * Retrieve the application logs. * @@ -400,62 +446,4 @@ public class AppResource extends BaseResource { .add("status", "ok"); return Response.ok().entity(response.build()).build(); } - - /** - * Add base ACLs to tags. - * - * @api {post} /app/batch/tag_acls Add base ACL to tags - * @apiDescription This resource must be used after migrating to 1.5. - * It will not do anything if base ACL are already present on tags. - * @apiName PostAppBatchTagAcls - * @apiGroup App - * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied - * @apiPermission admin - * @apiVersion 1.5.0 - * - * @return Response - */ - @POST - @Path("batch/tag_acls") - public Response batchTagAcls() { - if (!authenticate()) { - throw new ForbiddenClientException(); - } - checkBaseFunction(BaseFunction.ADMIN); - - // Get all tags - TagDao tagDao = new TagDao(); - UserDao userDao = new UserDao(); - List tagDtoList = tagDao.findByCriteria(new TagCriteria(), null); - - // Add READ and WRITE ACLs - for (TagDto tagDto : tagDtoList) { - // Remove old ACLs - AclDao aclDao = new AclDao(); - List aclDtoList = aclDao.getBySourceId(tagDto.getId()); - String userId = userDao.getActiveByUsername(tagDto.getCreator()).getId(); - - if (aclDtoList.size() == 0) { - // Create read ACL - Acl acl = new Acl(); - acl.setPerm(PermType.READ); - acl.setSourceId(tagDto.getId()); - acl.setTargetId(userId); - aclDao.create(acl, userId); - - // Create write ACL - acl = new Acl(); - acl.setPerm(PermType.WRITE); - acl.setSourceId(tagDto.getId()); - acl.setTargetId(userId); - aclDao.create(acl, userId); - } - } - - // Always return OK - JsonObjectBuilder response = Json.createObjectBuilder() - .add("status", "ok"); - return Response.ok().entity(response.build()).build(); - } } 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 743d1dcf..7b008144 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 @@ -3,7 +3,7 @@ /** * Login controller. */ -angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User, $translate) { +angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User, $translate, $uibModal) { $scope.codeRequired = false; // Get the app configuration @@ -28,7 +28,7 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc }); $state.go('document.default'); }, function(data) { - if (data.data.type == 'ValidationCodeRequired') { + if (data.data.type === 'ValidationCodeRequired') { // A TOTP validation code is required to login $scope.codeRequired = true; } else { @@ -40,4 +40,31 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc } }); }; + + // Password lost + $scope.openPasswordLost = function () { + $uibModal.open({ + templateUrl: 'partial/docs/passwordlost.html', + controller: 'LoginModalPasswordLost' + }).result.then(function (email) { + if (name === null) { + return; + } + + // Send a password lost email + Restangular.one('user').post('passwordLost', { + email: email + }).then(function () { + var title = $translate.instant('login.password_lost_sent_title'); + var msg = $translate.instant('login.password_lost_sent_message', { email: email }); + 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 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/LoginModalPasswordLost.js b/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js new file mode 100644 index 00000000..534b9f46 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js @@ -0,0 +1,11 @@ +'use strict'; + +/** + * Login modal password lost controller. + */ +angular.module('docs').controller('LoginModalPasswordLost', function ($scope, $uibModalInstance) { + $scope.email = ''; + $scope.close = function(name) { + $uibModalInstance.close(name); + } +}); \ 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 5461de21..fa1ac830 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -48,6 +48,7 @@ + diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 8dc98c4f..11723edb 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -9,7 +9,17 @@ "submit": "Sign in", "login_as_guest": "Login as guest", "login_failed_title": "Login failed", - "login_failed_message": "Username or password invalid" + "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_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", + "submit": "Reset my password" }, "index": { "toggle_navigation": "Toggle navigation", diff --git a/docs-web/src/main/webapp/src/partial/docs/login.html b/docs-web/src/main/webapp/src/partial/docs/login.html index 3a32495b..76d70633 100644 --- a/docs-web/src/main/webapp/src/partial/docs/login.html +++ b/docs-web/src/main/webapp/src/partial/docs/login.html @@ -44,7 +44,11 @@ {{ 'login.submit' | translate }} -

 

+ + +

 

+ + + \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 51038a1c..fdae4870 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -343,6 +343,10 @@ input[readonly].share-link { .help-block, .checkbox { color: white; } + + .btn-password-lost { + padding-bottom: 0; + } } /* Styling for the ngProgress itself */ diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index 41185645..08f56aff 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -189,4 +189,23 @@ public class TestAppResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) .get(JsonObject.class); } + + /** + * Test SMTP configuration changes. + */ + @Test + public void testSmtpConfiguration() { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + + // Change SMTP configuration + target().path("/app/config_smtp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("hostname", "smtp.sismics.com") + .param("port", "1234") + .param("from", "contact@sismics.com") + .param("username", "sismics") + ), JsonObject.class); + } } \ No newline at end of file