#161: password recovery by email (wip)

This commit is contained in:
bgamard 2017-11-17 17:01:08 +01:00
parent 590bf74e98
commit 65937d6f4c
10 changed files with 159 additions and 69 deletions

View File

@ -18,5 +18,14 @@ public enum ConfigType {
/** /**
* Guest login. * Guest login.
*/ */
GUEST_LOGIN GUEST_LOGIN,
/**
* SMTP server configuration.
*/
SMTP_HOSTNAME,
SMTP_PORT,
SMTP_FROM,
SMTP_USERNAME,
SMTP_PASSWORD
} }

View File

@ -18,20 +18,15 @@ import javax.ws.rs.*;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import com.sismics.docs.core.constant.ConfigType; 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.*;
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.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.commons.lang.StringUtils;
import org.apache.log4j.Appender; import org.apache.log4j.Appender;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.File;
import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.ConfigUtil;
@ -119,6 +114,57 @@ public class AppResource extends BaseResource {
return Response.ok().build(); 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. * Retrieve the application logs.
* *
@ -400,62 +446,4 @@ public class AppResource extends BaseResource {
.add("status", "ok"); .add("status", "ok");
return Response.ok().entity(response.build()).build(); 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<TagDto> tagDtoList = tagDao.findByCriteria(new TagCriteria(), null);
// Add READ and WRITE ACLs
for (TagDto tagDto : tagDtoList) {
// Remove old ACLs
AclDao aclDao = new AclDao();
List<AclDto> 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();
}
} }

View File

@ -3,7 +3,7 @@
/** /**
* Login controller. * 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; $scope.codeRequired = false;
// Get the app configuration // Get the app configuration
@ -28,7 +28,7 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc
}); });
$state.go('document.default'); $state.go('document.default');
}, function(data) { }, function(data) {
if (data.data.type == 'ValidationCodeRequired') { if (data.data.type === 'ValidationCodeRequired') {
// A TOTP validation code is required to login // A TOTP validation code is required to login
$scope.codeRequired = true; $scope.codeRequired = true;
} else { } 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);
});
});
};
}); });

View File

@ -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);
}
});

View File

@ -48,6 +48,7 @@
<script src="app/docs/app.js" type="text/javascript"></script> <script src="app/docs/app.js" type="text/javascript"></script>
<script src="app/docs/controller/Main.js" type="text/javascript"></script> <script src="app/docs/controller/Main.js" type="text/javascript"></script>
<script src="app/docs/controller/Login.js" type="text/javascript"></script> <script src="app/docs/controller/Login.js" type="text/javascript"></script>
<script src="app/docs/controller/LoginModalPasswordLost.js" type="text/javascript"></script>
<script src="app/docs/controller/Navigation.js" type="text/javascript"></script> <script src="app/docs/controller/Navigation.js" type="text/javascript"></script>
<script src="app/docs/controller/Footer.js" type="text/javascript"></script> <script src="app/docs/controller/Footer.js" type="text/javascript"></script>
<script src="app/docs/controller/document/Document.js" type="text/javascript"></script> <script src="app/docs/controller/document/Document.js" type="text/javascript"></script>

View File

@ -9,7 +9,17 @@
"submit": "Sign in", "submit": "Sign in",
"login_as_guest": "Login as guest", "login_as_guest": "Login as guest",
"login_failed_title": "Login failed", "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 <strong>{{ email }}</strong> 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": { "index": {
"toggle_navigation": "Toggle navigation", "toggle_navigation": "Toggle navigation",

View File

@ -44,7 +44,11 @@
<span class="glyphicon glyphicon-ok"></span> {{ 'login.submit' | translate }} <span class="glyphicon glyphicon-ok"></span> {{ 'login.submit' | translate }}
</button> </button>
<p class="text-center lead" ng-if="app.guest_login">&nbsp;</p> <div class="text-center well-sm btn-password-lost">
<a href ng-click="openPasswordLost()">{{ 'login.password_lost_btn' | translate }}</a>
</div>
<p class="text-center" ng-if="app.guest_login">&nbsp;</p>
<button type="submit" class="btn btn-default btn-block" ng-if="app.guest_login" ng-click="loginAsGuest()"> <button type="submit" class="btn btn-default btn-block" ng-if="app.guest_login" ng-click="loginAsGuest()">
<span class="glyphicon glyphicon-user"></span> {{ 'login.login_as_guest' | translate }} <span class="glyphicon glyphicon-user"></span> {{ 'login.login_as_guest' | translate }}

View File

@ -0,0 +1,17 @@
<form name="form">
<div class="modal-header">
<h3>{{ 'passwordlost.title' | translate }}</h3>
</div>
<div class="modal-body">
<p>
<label for="share-result">{{ 'passwordlost.message' | translate }}</label>
<input name="email" class="form-control" type="email" required id="share-result" ng-model="email" />
</p>
</div>
<div class="modal-footer">
<button ng-click="close(email)" class="btn btn-primary" ng-disabled="!form.$valid">
<span class="glyphicon glyphicon-envelope"></span> {{ 'passwordlost.submit' | translate }}
</button>
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | translate }}</button>
</div>
</form>

View File

@ -343,6 +343,10 @@ input[readonly].share-link {
.help-block, .checkbox { .help-block, .checkbox {
color: white; color: white;
} }
.btn-password-lost {
padding-bottom: 0;
}
} }
/* Styling for the ngProgress itself */ /* Styling for the ngProgress itself */

View File

@ -189,4 +189,23 @@ public class TestAppResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.get(JsonObject.class); .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);
}
} }