mirror of
https://github.com/sismics/docs.git
synced 2024-12-22 11:23:48 +01:00
#161: password recovery by email (wip, server part done)
This commit is contained in:
parent
b8176a9fe9
commit
039d881a07
@ -52,6 +52,21 @@
|
||||
<artifactId>commons-lang</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-email</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
|
@ -44,4 +44,24 @@ public class Constants {
|
||||
* Supported document languages.
|
||||
*/
|
||||
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor");
|
||||
|
||||
/**
|
||||
* Base URL environnement variable.
|
||||
*/
|
||||
public static final String BASE_URL_ENV = "DOCS_BASE_URL";
|
||||
|
||||
/**
|
||||
* Default language environnement variable.
|
||||
*/
|
||||
public static final String DEFAULT_LANGUAGE_ENV = "DOCS_DEFAULT_LANGUAGE";
|
||||
|
||||
/**
|
||||
* Expiration time of the password recovery in hours.
|
||||
*/
|
||||
public static final int PASSWORD_RECOVERY_EXPIRATION_HOUR = 2;
|
||||
|
||||
/**
|
||||
* Email template for password recovery.
|
||||
*/
|
||||
public static final String EMAIL_TEMPLATE_PASSWORD_RECOVERY = "password_recovery";
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
package com.sismics.docs.core.dao.jpa;
|
||||
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.model.jpa.PasswordRecovery;
|
||||
import com.sismics.util.context.ThreadLocalContext;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DurationFieldType;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.Query;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Password recovery DAO.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class PasswordRecoveryDao {
|
||||
/**
|
||||
* Create a new password recovery request.
|
||||
*
|
||||
* @param passwordRecovery Password recovery
|
||||
* @return Unique identifier
|
||||
*/
|
||||
public String create(PasswordRecovery passwordRecovery) {
|
||||
passwordRecovery.setId(UUID.randomUUID().toString());
|
||||
passwordRecovery.setCreateDate(new Date());
|
||||
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
em.persist(passwordRecovery);
|
||||
|
||||
return passwordRecovery.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search an active password recovery by unique identifier.
|
||||
*
|
||||
* @param id Unique identifier
|
||||
* @return Password recovery
|
||||
*/
|
||||
public PasswordRecovery getActiveById(String id) {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
try {
|
||||
Query q = em.createQuery("select r from PasswordRecovery r where r.id = :id and r.createDate > :createDateMin and r.deleteDate is null");
|
||||
q.setParameter("id", id);
|
||||
q.setParameter("createDateMin", new DateTime().withFieldAdded(DurationFieldType.hours(), -1 * Constants.PASSWORD_RECOVERY_EXPIRATION_HOUR).toDate());
|
||||
return (PasswordRecovery) q.getSingleResult();
|
||||
} catch (NoResultException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes active password recovery by username.
|
||||
*
|
||||
* @param username Username
|
||||
*/
|
||||
public void deleteActiveByLogin(String username) {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
Query q = em.createQuery("update PasswordRecovery r set r.deleteDate = :deleteDate where r.username = :username and r.createDate > :createDateMin and r.deleteDate is null");
|
||||
q.setParameter("username", username);
|
||||
q.setParameter("deleteDate", new Date());
|
||||
q.setParameter("createDateMin", new DateTime().withFieldAdded(DurationFieldType.hours(), -1 * Constants.PASSWORD_RECOVERY_EXPIRATION_HOUR).toDate());
|
||||
q.executeUpdate();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.sismics.docs.core.event;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.sismics.docs.core.model.jpa.PasswordRecovery;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
|
||||
/**
|
||||
* Event fired on user's password lost event.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class PasswordLostEvent {
|
||||
/**
|
||||
* User.
|
||||
*/
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* Password recovery request.
|
||||
*/
|
||||
private PasswordRecovery passwordRecovery;
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public PasswordRecovery getPasswordRecovery() {
|
||||
return passwordRecovery;
|
||||
}
|
||||
|
||||
public void setPasswordRecovery(PasswordRecovery passwordRecovery) {
|
||||
this.passwordRecovery = passwordRecovery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("user", user)
|
||||
.add("passwordRecovery", "**hidden**")
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.sismics.docs.core.listener.async;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.event.PasswordLostEvent;
|
||||
import com.sismics.docs.core.model.jpa.PasswordRecovery;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.docs.core.util.TransactionUtil;
|
||||
import com.sismics.util.EmailUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Listener for password recovery requests.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class PasswordLostAsyncListener {
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
private static final Logger log = LoggerFactory.getLogger(PasswordLostAsyncListener.class);
|
||||
|
||||
/**
|
||||
* Handle events.
|
||||
*
|
||||
* @param passwordLostEvent Event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onPasswordLost(final PasswordLostEvent passwordLostEvent) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Password lost event: " + passwordLostEvent.toString());
|
||||
}
|
||||
|
||||
TransactionUtil.handle(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final User user = passwordLostEvent.getUser();
|
||||
final PasswordRecovery passwordRecovery = passwordLostEvent.getPasswordRecovery();
|
||||
|
||||
// Send the password recovery email
|
||||
Map<String, Object> paramRootMap = new HashMap<>();
|
||||
paramRootMap.put("user_name", user.getUsername());
|
||||
paramRootMap.put("password_recovery_key", passwordRecovery.getId());
|
||||
|
||||
EmailUtil.sendEmail(Constants.EMAIL_TEMPLATE_PASSWORD_RECOVERY, user, paramRootMap);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,16 @@
|
||||
package com.sismics.docs.core.model.context;
|
||||
|
||||
import com.google.common.eventbus.AsyncEventBus;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.listener.async.*;
|
||||
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.service.IndexingService;
|
||||
import com.sismics.docs.core.util.PdfUtil;
|
||||
import com.sismics.util.EnvironmentUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -7,19 +18,6 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.eventbus.AsyncEventBus;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.lowagie.text.FontFactory;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent;
|
||||
import com.sismics.docs.core.listener.async.*;
|
||||
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.service.IndexingService;
|
||||
import com.sismics.docs.core.util.PdfUtil;
|
||||
import com.sismics.util.EnvironmentUtil;
|
||||
|
||||
/**
|
||||
* Global application context.
|
||||
*
|
||||
@ -41,6 +39,11 @@ public class AppContext {
|
||||
*/
|
||||
private EventBus asyncEventBus;
|
||||
|
||||
/**
|
||||
* Asynchronous bus for email sending.
|
||||
*/
|
||||
private EventBus mailEventBus;
|
||||
|
||||
/**
|
||||
* Indexing service.
|
||||
*/
|
||||
@ -83,6 +86,9 @@ public class AppContext {
|
||||
asyncEventBus.register(new DocumentDeletedAsyncListener());
|
||||
asyncEventBus.register(new RebuildIndexAsyncListener());
|
||||
asyncEventBus.register(new TemporaryFileCleanupAsyncListener());
|
||||
|
||||
mailEventBus = newAsyncEventBus();
|
||||
mailEventBus.register(new PasswordLostAsyncListener());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,29 +144,18 @@ public class AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of eventBus.
|
||||
*
|
||||
* @return eventBus
|
||||
*/
|
||||
public EventBus getEventBus() {
|
||||
return eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of asyncEventBus.
|
||||
*
|
||||
* @return asyncEventBus
|
||||
*/
|
||||
public EventBus getAsyncEventBus() {
|
||||
return asyncEventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of indexingService.
|
||||
*
|
||||
* @return indexingService
|
||||
*/
|
||||
public EventBus getMailEventBus() {
|
||||
return mailEventBus;
|
||||
}
|
||||
|
||||
public IndexingService getIndexingService() {
|
||||
return indexingService;
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
package com.sismics.docs.core.model.jpa;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Password recovery entity.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "T_PASSWORD_RECOVERY")
|
||||
public class PasswordRecovery {
|
||||
/**
|
||||
* Identifier.
|
||||
*/
|
||||
@Id
|
||||
@Column(name = "PWR_ID_C", length = 36)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* Username.
|
||||
*/
|
||||
@Column(name = "PWR_USERNAME_C", nullable = false, length = 50)
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Creation date.
|
||||
*/
|
||||
@Column(name = "PWR_CREATEDATE_D", nullable = false)
|
||||
private Date createDate;
|
||||
|
||||
/**
|
||||
* Delete date.
|
||||
*/
|
||||
@Column(name = "PWR_DELETEDATE_D")
|
||||
private Date deleteDate;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Date getCreateDate() {
|
||||
return createDate;
|
||||
}
|
||||
|
||||
public void setCreateDate(Date createDate) {
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
public Date getDeleteDate() {
|
||||
return deleteDate;
|
||||
}
|
||||
|
||||
public void setDeleteDate(Date deleteDate) {
|
||||
this.deleteDate = deleteDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("id", id)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -22,6 +22,10 @@ public class AuditLogUtil {
|
||||
* @param userId User ID
|
||||
*/
|
||||
public static void create(Loggable loggable, AuditLogType type, String userId) {
|
||||
if (userId == null) {
|
||||
userId = "admin";
|
||||
}
|
||||
|
||||
// Get the entity ID
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
String entityId = (String) em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(loggable);
|
||||
|
@ -1,20 +0,0 @@
|
||||
package com.sismics.docs.core.util;
|
||||
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
|
||||
/**
|
||||
* Utilitaires sur les utilisateurs.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class UserUtil {
|
||||
/**
|
||||
* Retourne the user's username.
|
||||
*
|
||||
* @param user User
|
||||
* @return User name
|
||||
*/
|
||||
public static String getUserName(User user) {
|
||||
return user.getUsername();
|
||||
}
|
||||
}
|
125
docs-core/src/main/java/com/sismics/util/EmailUtil.java
Normal file
125
docs-core/src/main/java/com/sismics/util/EmailUtil.java
Normal file
@ -0,0 +1,125 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.docs.core.util.ConfigUtil;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||
import freemarker.template.Template;
|
||||
import org.apache.commons.mail.HtmlEmail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObject;
|
||||
import javax.json.JsonReader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Emails utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class EmailUtil {
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
private static final Logger log = LoggerFactory.getLogger(EmailUtil.class);
|
||||
|
||||
/**
|
||||
* Returns an email content as string.
|
||||
* The content is formatted from the given Freemarker template and parameters.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param paramRootMap Map of Freemarker parameters
|
||||
* @param locale Locale
|
||||
* @return Template as string
|
||||
* @throws Exception e
|
||||
*/
|
||||
private static String getFormattedHtml(String templateName, Map<String, Object> paramRootMap, Locale locale) throws Exception {
|
||||
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
|
||||
cfg.setClassForTemplateLoading(EmailUtil.class, "/email_template");
|
||||
cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build());
|
||||
Template template = cfg.getTemplate(templateName + "/template.ftl");
|
||||
paramRootMap.put("messages", new ResourceBundleModel(MessageUtil.getMessage(locale),
|
||||
new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build()));
|
||||
StringWriter sw = new StringWriter();
|
||||
template.process(paramRootMap, sw);
|
||||
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending an email to a user.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param recipientUser Recipient user
|
||||
* @param subject Email subject
|
||||
* @param paramMap Email parameters
|
||||
*/
|
||||
public static void sendEmail(String templateName, User recipientUser, String subject, Map<String, Object> paramMap) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Sending email from template=" + templateName + " to user " + recipientUser);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build email headers
|
||||
HtmlEmail email = new HtmlEmail();
|
||||
email.setCharset("UTF-8");
|
||||
email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME));
|
||||
email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT));
|
||||
email.addTo(recipientUser.getEmail(), recipientUser.getUsername());
|
||||
ConfigDao configDao = new ConfigDao();
|
||||
Config themeConfig = configDao.getById(ConfigType.THEME);
|
||||
String appName = "Sismics Docs";
|
||||
if (themeConfig != null) {
|
||||
try (JsonReader reader = Json.createReader(new StringReader(themeConfig.getValue()))) {
|
||||
JsonObject themeJson = reader.readObject();
|
||||
appName = themeJson.getString("name", "Sismics Docs");
|
||||
}
|
||||
}
|
||||
email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName);
|
||||
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV));
|
||||
email.setSubject(subject);
|
||||
email.setTextMsg(MessageUtil.getMessage(userLocale, "email.no_html.error"));
|
||||
|
||||
// Add automatic parameters
|
||||
String baseUrl = System.getenv(Constants.BASE_URL_ENV);
|
||||
if (Strings.isNullOrEmpty(baseUrl)) {
|
||||
log.error("DOCS_BASE_URL environnement variable needs to be set for proper email links");
|
||||
baseUrl = ""; // At least the mail will be sent...
|
||||
}
|
||||
paramMap.put("base_url", baseUrl);
|
||||
paramMap.put("app_name", appName);
|
||||
|
||||
// Build HTML content from Freemarker template
|
||||
String htmlEmailTemplate = getFormattedHtml(templateName, paramMap, userLocale);
|
||||
email.setHtmlMsg(htmlEmailTemplate);
|
||||
|
||||
// Send the email
|
||||
email.send();
|
||||
} catch (Exception e) {
|
||||
log.error("Error sending email with template=" + templateName + " to user " + recipientUser, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending an email to a user.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param recipientUser Recipient user
|
||||
* @param paramMap Email parameters
|
||||
*/
|
||||
public static void sendEmail(String templateName, User recipientUser, Map<String, Object> paramMap) {
|
||||
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV));
|
||||
String subject = MessageUtil.getMessage(userLocale, "email.template." + templateName + ".subject");
|
||||
sendEmail(templateName, recipientUser, subject, paramMap);
|
||||
}
|
||||
}
|
36
docs-core/src/main/java/com/sismics/util/LocaleUtil.java
Normal file
36
docs-core/src/main/java/com/sismics/util/LocaleUtil.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Locale utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class LocaleUtil {
|
||||
/**
|
||||
* Returns a locale from the language / country / variation code (ex: fr_FR).
|
||||
*
|
||||
* @param localeCode Locale code
|
||||
* @return Locale instance
|
||||
*/
|
||||
public static Locale getLocale(String localeCode) {
|
||||
if (Strings.isNullOrEmpty(localeCode)) {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
|
||||
String[] localeCodeArray = localeCode.split("_");
|
||||
String language = localeCodeArray[0];
|
||||
String country = "";
|
||||
String variant = "";
|
||||
if (localeCodeArray.length >= 2) {
|
||||
country = localeCodeArray[1];
|
||||
}
|
||||
if (localeCodeArray.length >= 3) {
|
||||
variant = localeCodeArray[2];
|
||||
}
|
||||
return new Locale(language, country, variant);
|
||||
}
|
||||
}
|
43
docs-core/src/main/java/com/sismics/util/MessageUtil.java
Normal file
43
docs-core/src/main/java/com/sismics/util/MessageUtil.java
Normal file
@ -0,0 +1,43 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Messages utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class MessageUtil {
|
||||
/**
|
||||
* Returns a localized message in the specified language.
|
||||
* Returns **key** if no message exists for this key.
|
||||
*
|
||||
* @param locale Locale
|
||||
* @param key Message key
|
||||
* @param args Arguments to format
|
||||
* @return Formatted message
|
||||
*/
|
||||
public static String getMessage(Locale locale, String key, Object... args) {
|
||||
ResourceBundle resources = ResourceBundle.getBundle("messages", locale);
|
||||
String message;
|
||||
try {
|
||||
message = resources.getString(key);
|
||||
} catch (MissingResourceException e) {
|
||||
message = "**" + key + "**";
|
||||
}
|
||||
return MessageFormat.format(message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource bundle corresponding to the specified language.
|
||||
*
|
||||
* @param locale Locale
|
||||
* @return Resource bundle
|
||||
*/
|
||||
public static ResourceBundle getMessage(Locale locale) {
|
||||
return ResourceBundle.getBundle("messages", locale);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import freemarker.ext.beans.BeansWrapper;
|
||||
import freemarker.ext.beans.StringModel;
|
||||
import freemarker.template.TemplateModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Override of {@link freemarker.ext.beans.ResourceBundleModel}
|
||||
* to threat single quotes uniformely.
|
||||
*
|
||||
* @author bgamard
|
||||
*/
|
||||
public class ResourceBundleModel extends freemarker.ext.beans.ResourceBundleModel {
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param bundle Resource bundle
|
||||
* @param wrapper Beans wrapper
|
||||
*/
|
||||
public ResourceBundleModel(ResourceBundle bundle, BeansWrapper wrapper) {
|
||||
super(bundle, wrapper);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public Object exec(List arguments) throws TemplateModelException {
|
||||
// Must have at least one argument - the key
|
||||
if (arguments.size() < 1)
|
||||
throw new TemplateModelException("No message key was specified");
|
||||
// Read it
|
||||
Iterator it = arguments.iterator();
|
||||
String key = unwrap((TemplateModel) it.next()).toString();
|
||||
try {
|
||||
// Copy remaining arguments into an Object[]
|
||||
int args = arguments.size() - 1;
|
||||
Object[] params = new Object[args];
|
||||
for (int i = 0; i < args; ++i)
|
||||
params[i] = unwrap((TemplateModel) it.next());
|
||||
|
||||
// Invoke format
|
||||
return new StringModel(format(key, params), wrapper);
|
||||
} catch (MissingResourceException e) {
|
||||
throw new TemplateModelException("No such key: " + key);
|
||||
} catch (Exception e) {
|
||||
throw new TemplateModelException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,6 @@
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
|
||||
version="2.0">
|
||||
<persistence-unit name="transactions-optional" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.hibernate.ejb.HibernatePersistence</provider>
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
</persistence-unit>
|
||||
</persistence>
|
@ -1 +1 @@
|
||||
db.version=12
|
||||
db.version=13
|
@ -0,0 +1,2 @@
|
||||
create cached table T_PASSWORD_RECOVERY ( PWR_ID_C varchar(36) not null, PWR_USERNAME_C varchar(50) not null, PWR_CREATEDATE_D datetime, PWR_DELETEDATE_D datetime, primary key (PWR_ID_C) );
|
||||
update T_CONFIG set CFG_VALUE_C = '13' where CFG_ID_C = 'DB_VERSION';
|
16
docs-core/src/main/resources/email_template/layout.ftl
Normal file
16
docs-core/src/main/resources/email_template/layout.ftl
Normal file
@ -0,0 +1,16 @@
|
||||
<#macro email>
|
||||
<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}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px; padding-top: 10px;">
|
||||
<div style="border: 1px solid #ddd; padding: 10px;">
|
||||
<#nested>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</#macro>
|
@ -0,0 +1,8 @@
|
||||
<#import "../layout.ftl" as layout>
|
||||
<@layout.email>
|
||||
<h2>${app_name} - ${messages['email.template.password_recovery.subject']}</h2>
|
||||
<p>${messages('email.template.password_recovery.hello', user_name)}</p>
|
||||
<p>${messages['email.template.password_recovery.instruction1']}</p>
|
||||
<p>${messages['email.template.password_recovery.instruction2']}</p>
|
||||
<a href="${base_url}/#/passwordreset/${password_recovery_key}">${messages['email.template.password_recovery.click_here']}</a>
|
||||
</@layout.email>
|
5
docs-core/src/main/resources/messages.properties
Normal file
5
docs-core/src/main/resources/messages.properties
Normal file
@ -0,0 +1,5 @@
|
||||
email.template.password_recovery.subject=Please reset your password
|
||||
email.template.password_recovery.hello=Hello {0}.
|
||||
email.template.password_recovery.instruction1=We have received a request to reset your password.<br/>If you did not request help, then feel free to ignore this email.
|
||||
email.template.password_recovery.instruction2=To reset your password, please visit the link below:
|
||||
email.template.password_recovery.click_here=Click here to reset your password
|
5
docs-core/src/main/resources/messages_fr.properties
Normal file
5
docs-core/src/main/resources/messages_fr.properties
Normal file
@ -0,0 +1,5 @@
|
||||
email.template.password_recovery.subject=Réinitialiser votre mot de passe
|
||||
email.template.password_recovery.hello=Bonjour {0}.
|
||||
email.template.password_recovery.instruction1=Nous avons reçu une demande de réinitialisation de mot de passe.<br/>Si vous n''avez rien demandé, vous pouvez ignorer cet mail.
|
||||
email.template.password_recovery.instruction2=Pour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous :
|
||||
email.template.password_recovery.click_here=Cliquez ici pour réinitialiser votre mot de passe.
|
@ -94,6 +94,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<!-- Install test jar -->
|
||||
|
@ -1,11 +1,9 @@
|
||||
package com.sismics.docs.rest;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import javax.ws.rs.core.Application;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import com.sismics.docs.rest.util.ClientUtil;
|
||||
import com.sismics.util.filter.HeaderBasedSecurityFilter;
|
||||
import com.sismics.util.filter.RequestContextFilter;
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
import org.glassfish.grizzly.http.server.HttpServer;
|
||||
import org.glassfish.grizzly.servlet.ServletRegistration;
|
||||
import org.glassfish.grizzly.servlet.WebappContext;
|
||||
@ -17,10 +15,17 @@ import org.glassfish.jersey.test.spi.TestContainerException;
|
||||
import org.glassfish.jersey.test.spi.TestContainerFactory;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.subethamail.wiser.Wiser;
|
||||
import org.subethamail.wiser.WiserMessage;
|
||||
|
||||
import com.sismics.docs.rest.util.ClientUtil;
|
||||
import com.sismics.util.filter.RequestContextFilter;
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.ws.rs.core.Application;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class of integration tests with Jersey.
|
||||
@ -38,6 +43,11 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
*/
|
||||
protected ClientUtil clientUtil;
|
||||
|
||||
/**
|
||||
* Test mail server.
|
||||
*/
|
||||
private Wiser wiser;
|
||||
|
||||
@Override
|
||||
protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
|
||||
return new ExternalTestContainerFactory();
|
||||
@ -67,6 +77,10 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
|
||||
clientUtil = new ClientUtil(target());
|
||||
|
||||
wiser = new Wiser();
|
||||
wiser.setPort(2500);
|
||||
wiser.start();
|
||||
|
||||
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
|
||||
WebappContext context = new WebappContext("GrizzlyContext", "/docs");
|
||||
context.addFilter("requestContextFilter", RequestContextFilter.class)
|
||||
@ -86,6 +100,26 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
httpServer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an email from the list and consume it.
|
||||
*
|
||||
* @return Texte de l'email
|
||||
* @throws MessagingException e
|
||||
* @throws IOException e
|
||||
*/
|
||||
protected String popEmail() throws MessagingException, IOException {
|
||||
List<WiserMessage> wiserMessageList = wiser.getMessages();
|
||||
if (wiserMessageList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
WiserMessage wiserMessage = wiserMessageList.get(wiserMessageList.size() - 1);
|
||||
wiserMessageList.remove(wiserMessageList.size() - 1);
|
||||
MimeMessage message = wiserMessage.getMimeMessage();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
message.writeTo(os);
|
||||
return os.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
|
@ -122,6 +122,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=13
|
@ -11,6 +11,8 @@ import com.sismics.docs.core.dao.jpa.dto.GroupDto;
|
||||
import com.sismics.docs.core.dao.jpa.dto.UserDto;
|
||||
import com.sismics.docs.core.event.DocumentDeletedAsyncEvent;
|
||||
import com.sismics.docs.core.event.FileDeletedAsyncEvent;
|
||||
import com.sismics.docs.core.event.PasswordLostEvent;
|
||||
import com.sismics.docs.core.model.context.AppContext;
|
||||
import com.sismics.docs.core.model.jpa.*;
|
||||
import com.sismics.docs.core.util.ConfigUtil;
|
||||
import com.sismics.docs.core.util.EncryptionUtil;
|
||||
@ -902,6 +904,109 @@ public class UserResource extends BaseResource {
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a key to reset a password and send it by email.
|
||||
*
|
||||
* @api {post} /user/password_lost Create a key to reset a password and send it by email
|
||||
* @apiName PostUserPasswordLost
|
||||
* @apiGroup User
|
||||
* @apiParam {String} username Username
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) UserNotFound The user is not found
|
||||
* @apiError (client) ValidationError Validation error
|
||||
* @apiPermission none
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
* @param username Username
|
||||
* @return Response
|
||||
*/
|
||||
@POST
|
||||
@Path("password_lost")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response passwordLost(@FormParam("username") String username) {
|
||||
authenticate();
|
||||
|
||||
// Validate input data
|
||||
ValidationUtil.validateStringNotBlank("username", username);
|
||||
|
||||
// Check for user existence
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.getActiveByUsername(username);
|
||||
if (user == null) {
|
||||
throw new ClientException("UserNotFound", "User not found: " + username);
|
||||
}
|
||||
|
||||
// Create the password recovery key
|
||||
PasswordRecoveryDao passwordRecoveryDao = new PasswordRecoveryDao();
|
||||
PasswordRecovery passwordRecovery = new PasswordRecovery();
|
||||
passwordRecovery.setUsername(user.getUsername());
|
||||
passwordRecoveryDao.create(passwordRecovery);
|
||||
|
||||
// Fire a password lost event
|
||||
PasswordLostEvent passwordLostEvent = new PasswordLostEvent();
|
||||
passwordLostEvent.setUser(user);
|
||||
passwordLostEvent.setPasswordRecovery(passwordRecovery);
|
||||
AppContext.getInstance().getMailEventBus().post(passwordLostEvent);
|
||||
|
||||
// Always return OK
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("status", "ok");
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the user's password.
|
||||
*
|
||||
* @api {post} /user/password_reset Reset the user's password
|
||||
* @apiName PostUserPasswordReset
|
||||
* @apiGroup User
|
||||
* @apiParam {String} key Password recovery key
|
||||
* @apiParam {String} password New password
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) KeyNotFound Password recovery key not found
|
||||
* @apiError (client) ValidationError Validation error
|
||||
* @apiPermission none
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
* @param passwordResetKey Password reset key
|
||||
* @param password New password
|
||||
* @return Response
|
||||
*/
|
||||
@POST
|
||||
@Path("password_reset")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response passwordReset(
|
||||
@FormParam("key") String passwordResetKey,
|
||||
@FormParam("password") String password) {
|
||||
authenticate();
|
||||
|
||||
// Validate input data
|
||||
ValidationUtil.validateRequired("key", passwordResetKey);
|
||||
password = ValidationUtil.validateLength(password, "password", 8, 50, true);
|
||||
|
||||
// Load the password recovery key
|
||||
PasswordRecoveryDao passwordRecoveryDao = new PasswordRecoveryDao();
|
||||
PasswordRecovery passwordRecovery = passwordRecoveryDao.getActiveById(passwordResetKey);
|
||||
if (passwordRecovery == null) {
|
||||
throw new ClientException("KeyNotFound", "Password recovery key not found");
|
||||
}
|
||||
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.getActiveByUsername(passwordRecovery.getUsername());
|
||||
|
||||
// Change the password
|
||||
user.setPassword(password);
|
||||
user = userDao.updatePassword(user, principal.getId());
|
||||
|
||||
// Deletes password recovery requests
|
||||
passwordRecoveryDao.deleteActiveByLogin(user.getUsername());
|
||||
|
||||
// Always return OK
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("status", "ok");
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication token value.
|
||||
*
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=13
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=13
|
@ -1,5 +1,6 @@
|
||||
package com.sismics.docs.rest;
|
||||
|
||||
import com.sismics.docs.core.model.context.AppContext;
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
import com.sismics.util.totp.GoogleAuthenticator;
|
||||
import org.junit.Assert;
|
||||
@ -13,6 +14,8 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Exhaustive test of the user resource.
|
||||
@ -354,4 +357,79 @@ public class TestUserResource extends BaseJerseyTest {
|
||||
.get(JsonObject.class);
|
||||
Assert.assertFalse(json.getBoolean("totp_enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResetPassword() throws Exception {
|
||||
// Login admin
|
||||
String adminToken = clientUtil.login("admin", "admin", false);
|
||||
|
||||
// Change SMTP configuration to target Wiser
|
||||
target().path("/app/config_smtp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.post(Entity.form(new Form()
|
||||
.param("hostname", "localhost")
|
||||
.param("port", "2500")
|
||||
.param("from", "contact@sismicsdocs.com")
|
||||
), JsonObject.class);
|
||||
|
||||
// Create absent_minded who lost his password
|
||||
clientUtil.createUser("absent_minded");
|
||||
|
||||
// User no_such_user try to recovery its password: invalid user
|
||||
Response response = target().path("/user/password_lost").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("username", "no_such_user")));
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus()));
|
||||
JsonObject json = response.readEntity(JsonObject.class);
|
||||
Assert.assertEquals("UserNotFound", json.getString("type"));
|
||||
|
||||
// User absent_minded try to recovery its password: OK
|
||||
json = target().path("/user/password_lost").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("username", "absent_minded")), JsonObject.class);
|
||||
Assert.assertEquals("ok", json.getString("status"));
|
||||
AppContext.getInstance().waitForAsync();
|
||||
String emailBody = popEmail();
|
||||
Assert.assertNotNull("No email to consume", emailBody);
|
||||
Assert.assertTrue(emailBody.contains("Please reset your password"));
|
||||
Pattern keyPattern = Pattern.compile("/passwordreset/(.+?)\"");
|
||||
Matcher keyMatcher = keyPattern.matcher(emailBody);
|
||||
Assert.assertTrue("Token not found", keyMatcher.find());
|
||||
String key = keyMatcher.group(1).replaceAll("=", "");
|
||||
|
||||
// User absent_minded resets its password: invalid key
|
||||
response = target().path("/user/password_reset").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("key", "no_such_key")
|
||||
.param("password", "87654321")));
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus()));
|
||||
json = response.readEntity(JsonObject.class);
|
||||
Assert.assertEquals("KeyNotFound", json.getString("type"));
|
||||
|
||||
// User absent_minded resets its password: password invalid
|
||||
response = target().path("/user/password_reset").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("key", key)
|
||||
.param("password", " 1 ")));
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus()));
|
||||
json = response.readEntity(JsonObject.class);
|
||||
Assert.assertEquals("ValidationError", json.getString("type"));
|
||||
Assert.assertTrue(json.getString("message"), json.getString("message").contains("password"));
|
||||
|
||||
// User absent_minded resets its password: OK
|
||||
json = target().path("/user/password_reset").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("key", key)
|
||||
.param("password", "87654321")), JsonObject.class);
|
||||
Assert.assertEquals("ok", json.getString("status"));
|
||||
|
||||
// User absent_minded resets its password: expired key
|
||||
response = target().path("/user/password_reset").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("key", key)
|
||||
.param("password", "87654321")));
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus()));
|
||||
json = response.readEntity(JsonObject.class);
|
||||
Assert.assertEquals("KeyNotFound", json.getString("type"));
|
||||
}
|
||||
}
|
28
pom.xml
28
pom.xml
@ -19,6 +19,8 @@
|
||||
<org.apache.commons.commons-compress.version>1.10</org.apache.commons.commons-compress.version>
|
||||
<commons-lang.commons-lang.version>2.6</commons-lang.commons-lang.version>
|
||||
<commons-io.commons-io.version>2.4</commons-io.commons-io.version>
|
||||
<org.apache.commons.commons-email.version>1.5</org.apache.commons.commons-email.version>
|
||||
<org.freemarker.freemarker.version>2.3.23</org.freemarker.freemarker.version>
|
||||
<commons-dbcp.version>1.4</commons-dbcp.version>
|
||||
<com.google.guava.guava.version>19.0</com.google.guava.guava.version>
|
||||
<log4j.log4j.version>1.2.16</log4j.log4j.version>
|
||||
@ -27,6 +29,7 @@
|
||||
<junit.junit.version>4.12</junit.junit.version>
|
||||
<com.h2database.h2.version>1.4.191</com.h2database.h2.version>
|
||||
<org.glassfish.jersey.version>2.22.2</org.glassfish.jersey.version>
|
||||
<org.glassfish.javax.json.version>1.0.4</org.glassfish.javax.json.version>
|
||||
<org.mindrot.jbcrypt>0.3m</org.mindrot.jbcrypt>
|
||||
<org.apache.lucene.version>5.5.0</org.apache.lucene.version>
|
||||
<org.imgscalr.imgscalr-lib.version>4.2</org.imgscalr.imgscalr-lib.version>
|
||||
@ -40,6 +43,7 @@
|
||||
<com.twelvemonkeys.imageio.version>3.2.1</com.twelvemonkeys.imageio.version>
|
||||
<com.levigo.jbig2.levigo-jbig2-imageio.version>1.6.5</com.levigo.jbig2.levigo-jbig2-imageio.version>
|
||||
<com.github.jai-imageio.jai-imageio-core.version>1.3.1</com.github.jai-imageio.jai-imageio-core.version>
|
||||
<org.subethamail.subethasmtp-wiser.version>1.2</org.subethamail.subethasmtp-wiser.version>
|
||||
|
||||
<org.eclipse.jetty.jetty-server.version>9.2.13.v20150730</org.eclipse.jetty.jetty-server.version>
|
||||
<org.eclipse.jetty.jetty-webapp.version>9.2.13.v20150730</org.eclipse.jetty.jetty-webapp.version>
|
||||
@ -194,6 +198,12 @@
|
||||
<version>${commons-io.commons-io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-email</artifactId>
|
||||
<version>${org.apache.commons.commons-email.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
@ -285,6 +295,12 @@
|
||||
<version>${org.glassfish.jersey.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.json</artifactId>
|
||||
<version>${org.glassfish.javax.json.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
@ -315,6 +331,12 @@
|
||||
<version>${commons-dbcp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>${org.freemarker.freemarker.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
@ -375,6 +397,12 @@
|
||||
<version>${fr.opensagres.xdocreport.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<version>${org.subethamail.subethasmtp-wiser.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Servlet listener to register SPI ImageIO plugins -->
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.servlet</groupId>
|
||||
|
Loading…
Reference in New Issue
Block a user