Closes #161: password recovery by email

This commit is contained in:
Benjamin Gamard 2017-11-17 23:17:05 +01:00
parent 332fd9d1f6
commit 4cf1f29e0a
13 changed files with 188 additions and 56 deletions

View File

@ -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

View File

@ -2,7 +2,7 @@
<table style="width: 100%; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';">
<tr style="background: #242424; color: #fff;">
<td style="padding: 12px; font-size: 16px; font-weight: bold;">
${base_url}
${app_name}
</td>
</tr>
<tr>

View File

@ -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");
if (!Strings.isNullOrEmpty(portStr)) {
ValidationUtil.validateInteger(portStr, "port");
ValidationUtil.validateRequired(from, "from");
}
// Just update the changed configuration
ConfigDao configDao = new ConfigDao();
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 (username != null) {
}
if (!Strings.isNullOrEmpty(username)) {
configDao.update(ConfigType.SMTP_USERNAME, username);
}
if (password != null) {
if (!Strings.isNullOrEmpty(password)) {
configDao.update(ConfigType.SMTP_PASSWORD, password);
}

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,7 @@
<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/LoginModalPasswordLost.js" type="text/javascript"></script>
<script src="app/docs/controller/PasswordReset.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/document/Document.js" type="text/javascript"></script>

View File

@ -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 <strong>{{ email }}</strong> to reset your password",
"password_lost_sent_message": "An email has been sent to <strong>{{ username }}</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",
"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 <small>configuration</small>",
"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 <small>logs</small>",

View File

@ -5,11 +5,11 @@
<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" />
<input name="username" class="form-control" type="text" required id="share-result" ng-model="username" />
</p>
</div>
<div class="modal-footer">
<button ng-click="close(email)" class="btn btn-primary" ng-disabled="!form.$valid">
<button ng-click="close(username)" 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>

View File

@ -0,0 +1,25 @@
<div class="row">
<div class="col-xs-offset-1 col-xs-10 col-sm-offset-2 col-sm-8">
<form class="form-horizontal" name="form" novalidate>
<div class="form-group" ng-class="{ 'has-error': !form.password.$valid && form.$dirty }">
<label for="inputPassword" class="col-sm-4 control-label">{{ 'passwordreset.message' | translate }}</label>
<div class="col-sm-4">
<input type="password" name="password" ng-model="password" required ng-minlength="8" ng-maxlength="50" class="form-control" id="inputPassword">
</div>
<div class="col-sm-4">
<span class="help-block" ng-show="form.password.$error.required && form.$dirty">{{ 'validation.required' | translate }}</span>
<span class="help-block" ng-show="form.password.$error.minlength && form.$dirty">{{ 'validation.too_short' | translate }}</span>
<span class="help-block" ng-show="form.password.$error.maxlength && form.$dirty">{{ 'validation.too_long' | translate }}</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-5">
<button class="btn btn-primary" ng-disabled="!form.$valid" ng-click="submit()">
<span class="glyphicon glyphicon-lock"></span>
{{ 'passwordreset.submit' | translate }}
</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -74,3 +74,53 @@
</div>
</div>
</form>
<h1 translate="settings.config.title_smtp"></h1>
<div uib-alert ng-class="'alert-success'" ng-show="smtpUpdated">{{ 'settings.config.smtp_updated' | translate }}</div>
<form class="form-horizontal" name="smtpForm" ng-show="!smtpUpdated" novalidate>
<div class="form-group" ng-class="{ 'has-error': !smtpForm.hostname.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpHostname">{{ 'settings.config.smtp_hostname' | translate }}</label>
<div class="col-sm-7">
<input name="hostname" type="text" class="form-control" id="smtpHostname" ng-model="smtp.hostname" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !smtpForm.port.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpPort">{{ 'settings.config.smtp_port' | translate }}</label>
<div class="col-sm-7">
<input name="port" type="number" class="form-control" id="smtpPort" ng-model="smtp.port" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !smtpForm.from.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpFrom">{{ 'settings.config.smtp_from' | translate }}</label>
<div class="col-sm-7">
<input name="from" type="email" class="form-control" id="smtpFrom" ng-model="smtp.from" />
</div>
<div class="col-sm-3">
<span class="help-block" ng-show="smtpForm.from.$error.email && smtpForm.$dirty">{{ 'validation.email' | translate }}</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="smtpUsername">{{ 'settings.config.smtp_username' | translate }}</label>
<div class="col-sm-7">
<input name="username" type="text" class="form-control" id="smtpUsername" ng-model="smtp.username" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="smtpPassword">{{ 'settings.config.smtp_password' | translate }}</label>
<div class="col-sm-7">
<input name="password" type="password" class="form-control" id="smtpPassword" ng-model="smtp.password" />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="editSmtpConfig()" ng-disabled="!smtpForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ 'edit' | translate }}
</button>
</div>
</div>
</form>