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.setCharset("UTF-8");
email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME)); email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME));
email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT)); 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()); email.addTo(recipientUser.getEmail(), recipientUser.getUsername());
ConfigDao configDao = new ConfigDao(); ConfigDao configDao = new ConfigDao();
Config themeConfig = configDao.getById(ConfigType.THEME); Config themeConfig = configDao.getById(ConfigType.THEME);
@ -87,7 +89,7 @@ public class EmailUtil {
} }
email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName); email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName);
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV)); 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")); email.setTextMsg(MessageUtil.getMessage(userLocale, "email.no_html.error"));
// Add automatic parameters // 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';"> <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;"> <tr style="background: #242424; color: #fff;">
<td style="padding: 12px; font-size: 16px; font-weight: bold;"> <td style="padding: 12px; font-size: 16px; font-weight: bold;">
${base_url} ${app_name}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,32 +1,11 @@
package com.sismics.docs.rest.resource; package com.sismics.docs.rest.resource;
import java.io.IOException; import com.google.common.base.Strings;
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.sismics.docs.core.constant.ConfigType; 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.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.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;
@ -36,10 +15,28 @@ import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.log4j.LogCriteria; import com.sismics.util.log4j.LogCriteria;
import com.sismics.util.log4j.LogEntry; import com.sismics.util.log4j.LogEntry;
import com.sismics.util.log4j.MemoryAppender; 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. * General app REST resource.
@ -126,6 +123,7 @@ public class AppResource extends BaseResource {
* @apiParam {String} username SMTP username * @apiParam {String} username SMTP username
* @apiParam {String} password SMTP password * @apiParam {String} password SMTP password
* @apiError (client) ForbiddenError Access denied * @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiPermission admin * @apiPermission admin
* @apiVersion 1.5.0 * @apiVersion 1.5.0
* *
@ -147,18 +145,25 @@ public class AppResource extends BaseResource {
throw new ForbiddenClientException(); throw new ForbiddenClientException();
} }
checkBaseFunction(BaseFunction.ADMIN); checkBaseFunction(BaseFunction.ADMIN);
ValidationUtil.validateRequired(hostname, "hostname"); if (!Strings.isNullOrEmpty(portStr)) {
ValidationUtil.validateInteger(portStr, "port"); ValidationUtil.validateInteger(portStr, "port");
ValidationUtil.validateRequired(from, "from"); }
// Just update the changed configuration
ConfigDao configDao = new ConfigDao(); ConfigDao configDao = new ConfigDao();
configDao.update(ConfigType.SMTP_HOSTNAME, hostname); if (!Strings.isNullOrEmpty(hostname)) {
configDao.update(ConfigType.SMTP_PORT, portStr); configDao.update(ConfigType.SMTP_HOSTNAME, hostname);
configDao.update(ConfigType.SMTP_FROM, from); }
if (username != null) { 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); configDao.update(ConfigType.SMTP_USERNAME, username);
} }
if (password != null) { if (!Strings.isNullOrEmpty(password)) {
configDao.update(ConfigType.SMTP_PASSWORD, 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', { .state('tag', {
url: '/tag', url: '/tag',
abstract: true, abstract: true,

View File

@ -46,22 +46,22 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc
$uibModal.open({ $uibModal.open({
templateUrl: 'partial/docs/passwordlost.html', templateUrl: 'partial/docs/passwordlost.html',
controller: 'LoginModalPasswordLost' controller: 'LoginModalPasswordLost'
}).result.then(function (email) { }).result.then(function (username) {
if (name === null) { if (username === null) {
return; return;
} }
// Send a password lost email // Send a password lost email
Restangular.one('user').post('passwordLost', { Restangular.one('user').post('password_lost', {
email: email username: username
}).then(function () { }).then(function () {
var title = $translate.instant('login.password_lost_sent_title'); 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'}]; var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns); $dialog.messageBox(title, msg, btns);
}, function () { }, function () {
var title = $translate.instant('login.password_lost_error_title'); 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'}]; var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns); $dialog.messageBox(title, msg, btns);
}); });

View File

@ -4,8 +4,8 @@
* Login modal password lost controller. * Login modal password lost controller.
*/ */
angular.module('docs').controller('LoginModalPasswordLost', function ($scope, $uibModalInstance) { angular.module('docs').controller('LoginModalPasswordLost', function ($scope, $uibModalInstance) {
$scope.email = ''; $scope.username = '';
$scope.close = function(name) { $scope.close = function(username) {
$uibModalInstance.close(name); $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

@ -5,29 +5,29 @@
*/ */
angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) { angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) {
// Get the app configuration // Get the app configuration
Restangular.one('app').get().then(function(data) { Restangular.one('app').get().then(function (data) {
$scope.app = data; $scope.app = data;
}); });
// Enable/disable guest login // Enable/disable guest login
$scope.changeGuestLogin = function(enabled) { $scope.changeGuestLogin = function (enabled) {
Restangular.one('app').post('guest_login', { Restangular.one('app').post('guest_login', {
enabled: enabled enabled: enabled
}).then(function() { }).then(function () {
$scope.app.guest_login = enabled; $scope.app.guest_login = enabled;
}); });
}; };
// Fetch the current theme configuration // Fetch the current theme configuration
Restangular.one('theme').get().then(function(data) { Restangular.one('theme').get().then(function (data) {
$scope.theme = data; $scope.theme = data;
$rootScope.appName = $scope.theme.name; $rootScope.appName = $scope.theme.name;
}); });
// Update the theme // Update the theme
$scope.update = function() { $scope.update = function () {
$scope.theme.name = $scope.theme.name.length === 0 ? 'Sismics Docs' : $scope.theme.name; $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]; var stylesheet = $('#theme-stylesheet')[0];
stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime()); stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime());
$rootScope.appName = $scope.theme.name; $rootScope.appName = $scope.theme.name;
@ -36,7 +36,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope,
// Send an image // Send an image
$scope.sendingImage = false; $scope.sendingImage = false;
$scope.sendImage = function(type, image) { $scope.sendImage = function (type, image) {
// Build the payload // Build the payload
var formData = new FormData(); var formData = new FormData();
formData.append('image', image); 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;
});
};
}); });

View File

@ -49,6 +49,7 @@
<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/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/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

@ -12,15 +12,21 @@
"login_failed_message": "Username or password invalid", "login_failed_message": "Username or password invalid",
"password_lost_btn": "Password lost?", "password_lost_btn": "Password lost?",
"password_lost_sent_title": "Password reset email sent", "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_title": "Password reset error",
"password_lost_error_message": "Unable to send a password reset email, please contact your administrator for a manual reset" "password_lost_error_message": "Unable to send a password reset email, please contact your administrator for a manual reset"
}, },
"passwordlost": { "passwordlost": {
"title": "Password lost", "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" "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": { "index": {
"toggle_navigation": "Toggle navigation", "toggle_navigation": "Toggle navigation",
"nav_documents": "Documents", "nav_documents": "Documents",
@ -295,7 +301,14 @@
"custom_css_placeholder": "Custom CSS to add after the main stylesheet", "custom_css_placeholder": "Custom CSS to add after the main stylesheet",
"logo": "Logo (squared size)", "logo": "Logo (squared size)",
"background_image": "Background image", "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": { "log": {
"title": "Server <small>logs</small>", "title": "Server <small>logs</small>",

View File

@ -5,11 +5,11 @@
<div class="modal-body"> <div class="modal-body">
<p> <p>
<label for="share-result">{{ 'passwordlost.message' | translate }}</label> <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> </p>
</div> </div>
<div class="modal-footer"> <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 }} <span class="glyphicon glyphicon-envelope"></span> {{ 'passwordlost.submit' | translate }}
</button> </button>
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | 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

@ -73,4 +73,54 @@
</p> </p>
</div> </div>
</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> </form>