From 039d881a07d8f41120d2ae301748df9ec4c5fb5b Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 Nov 2017 22:03:54 +0100 Subject: [PATCH 01/17] #161: password recovery by email (wip, server part done) --- docs-core/pom.xml | 15 +++ .../sismics/docs/core/constant/Constants.java | 20 +++ .../core/dao/jpa/PasswordRecoveryDao.java | 68 ++++++++++ .../docs/core/event/PasswordLostEvent.java | 46 +++++++ .../async/PasswordLostAsyncListener.java | 53 ++++++++ .../docs/core/model/context/AppContext.java | 51 ++++--- .../docs/core/model/jpa/PasswordRecovery.java | 82 ++++++++++++ .../sismics/docs/core/util/AuditLogUtil.java | 4 + .../com/sismics/docs/core/util/UserUtil.java | 20 --- .../main/java/com/sismics/util/EmailUtil.java | 125 ++++++++++++++++++ .../java/com/sismics/util/LocaleUtil.java | 36 +++++ .../java/com/sismics/util/MessageUtil.java | 43 ++++++ .../com/sismics/util/ResourceBundleModel.java | 55 ++++++++ .../main/resources/META-INF/persistence.xml | 2 +- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-013-0.sql | 2 + .../main/resources/email_template/layout.ftl | 16 +++ .../password_recovery/template.ftl | 8 ++ .../src/main/resources/messages.properties | 5 + .../src/main/resources/messages_fr.properties | 5 + docs-web-common/pom.xml | 8 +- .../com/sismics/docs/rest/BaseJerseyTest.java | 52 ++++++-- docs-web/pom.xml | 6 + docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/UserResource.java | 107 ++++++++++++++- docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../sismics/docs/rest/TestUserResource.java | 78 +++++++++++ pom.xml | 32 ++++- 29 files changed, 881 insertions(+), 66 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/PasswordRecoveryDao.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java delete mode 100644 docs-core/src/main/java/com/sismics/docs/core/util/UserUtil.java create mode 100644 docs-core/src/main/java/com/sismics/util/EmailUtil.java create mode 100644 docs-core/src/main/java/com/sismics/util/LocaleUtil.java create mode 100644 docs-core/src/main/java/com/sismics/util/MessageUtil.java create mode 100644 docs-core/src/main/java/com/sismics/util/ResourceBundleModel.java create mode 100644 docs-core/src/main/resources/db/update/dbupdate-013-0.sql create mode 100644 docs-core/src/main/resources/email_template/layout.ftl create mode 100644 docs-core/src/main/resources/email_template/password_recovery/template.ftl create mode 100644 docs-core/src/main/resources/messages.properties create mode 100644 docs-core/src/main/resources/messages_fr.properties diff --git a/docs-core/pom.xml b/docs-core/pom.xml index 85e0267d..cafafa76 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -51,7 +51,22 @@ commons-lang commons-lang + + + org.apache.commons + commons-email + + + org.freemarker + freemarker + + + + org.glassfish + javax.json + + log4j log4j diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index aba7c82d..9ac980b2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -44,4 +44,24 @@ public class Constants { * Supported document languages. */ public static final List 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"; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/PasswordRecoveryDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/PasswordRecoveryDao.java new file mode 100644 index 00000000..1eb5bec0 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/PasswordRecoveryDao.java @@ -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(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java new file mode 100644 index 00000000..78f39d73 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/event/PasswordLostEvent.java @@ -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(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java new file mode 100644 index 00000000..8c4aa48b --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/PasswordLostAsyncListener.java @@ -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 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); + } + }); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index 78a7cb45..b869eee6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -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; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java new file mode 100644 index 00000000..6306c807 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java @@ -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(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java index d2d0913c..60b04820 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java @@ -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); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/UserUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/UserUtil.java deleted file mode 100644 index 7f19c0d1..00000000 --- a/docs-core/src/main/java/com/sismics/docs/core/util/UserUtil.java +++ /dev/null @@ -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(); - } -} diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java new file mode 100644 index 00000000..13ddefbc --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -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 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 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 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); + } +} diff --git a/docs-core/src/main/java/com/sismics/util/LocaleUtil.java b/docs-core/src/main/java/com/sismics/util/LocaleUtil.java new file mode 100644 index 00000000..11ab9c7b --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/LocaleUtil.java @@ -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); + } +} diff --git a/docs-core/src/main/java/com/sismics/util/MessageUtil.java b/docs-core/src/main/java/com/sismics/util/MessageUtil.java new file mode 100644 index 00000000..a2898bd0 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/MessageUtil.java @@ -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); + } +} diff --git a/docs-core/src/main/java/com/sismics/util/ResourceBundleModel.java b/docs-core/src/main/java/com/sismics/util/ResourceBundleModel.java new file mode 100644 index 00000000..cff2a583 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/ResourceBundleModel.java @@ -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()); + } + } +} diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index 7c40e310..b62e7679 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -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"> - org.hibernate.ejb.HibernatePersistence + org.hibernate.jpa.HibernatePersistenceProvider \ No newline at end of file diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index a56b42a4..ca77caa4 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=12 \ No newline at end of file +db.version=13 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-013-0.sql b/docs-core/src/main/resources/db/update/dbupdate-013-0.sql new file mode 100644 index 00000000..1407d548 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-013-0.sql @@ -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'; diff --git a/docs-core/src/main/resources/email_template/layout.ftl b/docs-core/src/main/resources/email_template/layout.ftl new file mode 100644 index 00000000..74a8dd09 --- /dev/null +++ b/docs-core/src/main/resources/email_template/layout.ftl @@ -0,0 +1,16 @@ +<#macro email> + + + + + + + +
+ ${base_url} +
+
+ <#nested> +
+
+ \ No newline at end of file diff --git a/docs-core/src/main/resources/email_template/password_recovery/template.ftl b/docs-core/src/main/resources/email_template/password_recovery/template.ftl new file mode 100644 index 00000000..84c371fa --- /dev/null +++ b/docs-core/src/main/resources/email_template/password_recovery/template.ftl @@ -0,0 +1,8 @@ +<#import "../layout.ftl" as layout> +<@layout.email> +

${app_name} - ${messages['email.template.password_recovery.subject']}

+

${messages('email.template.password_recovery.hello', user_name)}

+

${messages['email.template.password_recovery.instruction1']}

+

${messages['email.template.password_recovery.instruction2']}

+ ${messages['email.template.password_recovery.click_here']} + \ No newline at end of file diff --git a/docs-core/src/main/resources/messages.properties b/docs-core/src/main/resources/messages.properties new file mode 100644 index 00000000..fc8d7a39 --- /dev/null +++ b/docs-core/src/main/resources/messages.properties @@ -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.
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 \ No newline at end of file diff --git a/docs-core/src/main/resources/messages_fr.properties b/docs-core/src/main/resources/messages_fr.properties new file mode 100644 index 00000000..6ddc4b41 --- /dev/null +++ b/docs-core/src/main/resources/messages_fr.properties @@ -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.
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. \ No newline at end of file diff --git a/docs-web-common/pom.xml b/docs-web-common/pom.xml index 5a7e8a30..efa9aced 100644 --- a/docs-web-common/pom.xml +++ b/docs-web-common/pom.xml @@ -93,7 +93,13 @@ jersey-container-grizzly2-servlet test
- + + + org.subethamail + subethasmtp-wiser + test + + diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java index a344443d..0d00e9a4 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java @@ -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. @@ -37,6 +42,11 @@ public abstract class BaseJerseyTest extends JerseyTest { * Utility class for the REST client. */ protected ClientUtil clientUtil; + + /** + * Test mail server. + */ + private Wiser wiser; @Override protected TestContainerFactory getTestContainerFactory() throws TestContainerException { @@ -66,7 +76,11 @@ public abstract class BaseJerseyTest extends JerseyTest { System.setProperty("docs.header_authentication", "true"); 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 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 { diff --git a/docs-web/pom.xml b/docs-web/pom.xml index 3573646e..4d239548 100644 --- a/docs-web/pom.xml +++ b/docs-web/pom.xml @@ -121,6 +121,12 @@ jersey-container-grizzly2-servlet test + + + org.subethamail + subethasmtp-wiser + test + diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 17c44aa4..b41be0b3 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=12 \ No newline at end of file +db.version=13 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index aaca4646..1226fa27 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -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; @@ -901,7 +903,110 @@ public class UserResource extends BaseResource { .add("status", "ok"); 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. * diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 17c44aa4..b41be0b3 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=12 \ No newline at end of file +db.version=13 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 17c44aa4..b41be0b3 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=12 \ No newline at end of file +db.version=13 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 4d5bdfa7..6cddb206 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -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")); + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index b3cadb17..4059a000 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ 1.10 2.6 2.4 + 1.5 + 2.3.23 1.4 19.0 1.2.16 @@ -27,6 +29,7 @@ 4.12 1.4.191 2.22.2 + 1.0.4 0.3m 5.5.0 4.2 @@ -40,6 +43,7 @@ 3.2.1 1.6.5 1.3.1 + 1.2 9.2.13.v20150730 9.2.13.v20150730 @@ -193,6 +197,12 @@ commons-io ${commons-io.commons-io.version} + + + org.apache.commons + commons-email + ${org.apache.commons.commons-email.version} + com.google.guava @@ -284,7 +294,13 @@ jersey-container-grizzly2-servlet ${org.glassfish.jersey.version} - + + + org.glassfish + javax.json + ${org.glassfish.javax.json.version} + + com.h2database h2 @@ -314,7 +330,13 @@ commons-dbcp ${commons-dbcp.version} - + + + org.freemarker + freemarker + ${org.freemarker.freemarker.version} + + joda-time joda-time @@ -375,6 +397,12 @@ ${fr.opensagres.xdocreport.version} + + org.subethamail + subethasmtp-wiser + ${org.subethamail.subethasmtp-wiser.version} + + com.twelvemonkeys.servlet From 332fd9d1f698088cc69dbb0a52aba296339bf319 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 Nov 2017 22:26:20 +0100 Subject: [PATCH 02/17] fix tests --- .../src/test/java/com/sismics/docs/rest/BaseJerseyTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java index 0d00e9a4..a434b227 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java @@ -127,5 +127,8 @@ public abstract class BaseJerseyTest extends JerseyTest { if (httpServer != null) { httpServer.shutdownNow(); } + if (wiser != null) { + wiser.stop(); + } } } From 4cf1f29e0a778fc990a22a966d0884169581b717 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 Nov 2017 23:17:05 +0100 Subject: [PATCH 03/17] Closes #161: password recovery by email --- .../main/java/com/sismics/util/EmailUtil.java | 4 +- .../main/resources/email_template/layout.ftl | 2 +- .../docs/rest/resource/AppResource.java | 71 ++++++++++--------- docs-web/src/main/webapp/src/app/docs/app.js | 9 +++ .../webapp/src/app/docs/controller/Login.js | 12 ++-- .../docs/controller/LoginModalPasswordLost.js | 6 +- .../src/app/docs/controller/PasswordReset.js | 20 ++++++ .../controller/settings/SettingsConfig.js | 21 ++++-- docs-web/src/main/webapp/src/index.html | 1 + docs-web/src/main/webapp/src/locale/en.json | 19 ++++- .../webapp/src/partial/docs/passwordlost.html | 4 +- .../src/partial/docs/passwordreset.html | 25 +++++++ .../src/partial/docs/settings.config.html | 50 +++++++++++++ 13 files changed, 188 insertions(+), 56 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js create mode 100644 docs-web/src/main/webapp/src/partial/docs/passwordreset.html diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index 13ddefbc..af6c88fa 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -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 diff --git a/docs-core/src/main/resources/email_template/layout.ftl b/docs-core/src/main/resources/email_template/layout.ftl index 74a8dd09..0ceae478 100644 --- a/docs-core/src/main/resources/email_template/layout.ftl +++ b/docs-core/src/main/resources/email_template/layout.ftl @@ -2,7 +2,7 @@ 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 0586d6bd..ca422cbe 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 @@ -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"); - ValidationUtil.validateInteger(portStr, "port"); - ValidationUtil.validateRequired(from, "from"); + if (!Strings.isNullOrEmpty(portStr)) { + ValidationUtil.validateInteger(portStr, "port"); + } + // Just update the changed configuration 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) { + 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 (!Strings.isNullOrEmpty(username)) { configDao.update(ConfigType.SMTP_USERNAME, username); } - if (password != null) { + if (!Strings.isNullOrEmpty(password)) { configDao.update(ConfigType.SMTP_PASSWORD, password); } diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 707bef6a..bcaa03b7 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -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, 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 7b008144..32b3a000 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 @@ -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); }); 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 index 534b9f46..16517457 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/LoginModalPasswordLost.js @@ -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); } }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js b/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js new file mode 100644 index 00000000..f525ac92 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/PasswordReset.js @@ -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); + }); + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js index 7efaac1d..32d55dd0 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js @@ -5,29 +5,29 @@ */ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) { // Get the app configuration - Restangular.one('app').get().then(function(data) { + Restangular.one('app').get().then(function (data) { $scope.app = data; }); // Enable/disable guest login - $scope.changeGuestLogin = function(enabled) { + $scope.changeGuestLogin = function (enabled) { Restangular.one('app').post('guest_login', { enabled: enabled - }).then(function() { + }).then(function () { $scope.app.guest_login = enabled; }); }; // Fetch the current theme configuration - Restangular.one('theme').get().then(function(data) { + Restangular.one('theme').get().then(function (data) { $scope.theme = data; $rootScope.appName = $scope.theme.name; }); // Update the theme - $scope.update = function() { + $scope.update = function () { $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]; stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime()); $rootScope.appName = $scope.theme.name; @@ -36,7 +36,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, // Send an image $scope.sendingImage = false; - $scope.sendImage = function(type, image) { + $scope.sendImage = function (type, image) { // Build the payload var formData = new FormData(); 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; + }); + }; }); \ 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 fa1ac830..3d237b6e 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -49,6 +49,7 @@ + diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 11723edb..fc54b550 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -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 {{ email }} to reset your password", + "password_lost_sent_message": "An email has been sent to {{ username }} 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 configuration", + "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 logs", diff --git a/docs-web/src/main/webapp/src/partial/docs/passwordlost.html b/docs-web/src/main/webapp/src/partial/docs/passwordlost.html index 73d042c0..2bb1baac 100644 --- a/docs-web/src/main/webapp/src/partial/docs/passwordlost.html +++ b/docs-web/src/main/webapp/src/partial/docs/passwordlost.html @@ -5,11 +5,11 @@ + + +

+
{{ 'settings.config.smtp_updated' | translate }}
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ {{ 'validation.email' | translate }} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
\ No newline at end of file From fdb95484c1b7a0bc283eb9724cec25ba90bf810d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 Nov 2017 23:47:53 +0100 Subject: [PATCH 04/17] fix sending email to an unauthenticated smtp server --- docs-core/src/main/java/com/sismics/util/EmailUtil.java | 9 ++++++--- docs-core/src/main/resources/messages.properties | 3 ++- docs-core/src/main/resources/messages_fr.properties | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index af6c88fa..51b4b2e9 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -75,10 +75,13 @@ 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 usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME); + Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); + if (usernameConfig != null && passwordConfig != null) { + email.setAuthentication(usernameConfig.getValue(), passwordConfig.getValue()); + } + email.addTo(recipientUser.getEmail(), recipientUser.getUsername()); Config themeConfig = configDao.getById(ConfigType.THEME); String appName = "Sismics Docs"; if (themeConfig != null) { diff --git a/docs-core/src/main/resources/messages.properties b/docs-core/src/main/resources/messages.properties index fc8d7a39..06a43b5a 100644 --- a/docs-core/src/main/resources/messages.properties +++ b/docs-core/src/main/resources/messages.properties @@ -2,4 +2,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.
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 \ No newline at end of file +email.template.password_recovery.click_here=Click here to reset your password +email.no_html.error=Your email client does not support HTML messages \ No newline at end of file diff --git a/docs-core/src/main/resources/messages_fr.properties b/docs-core/src/main/resources/messages_fr.properties index 6ddc4b41..4bfc2450 100644 --- a/docs-core/src/main/resources/messages_fr.properties +++ b/docs-core/src/main/resources/messages_fr.properties @@ -2,4 +2,5 @@ email.template.password_recovery.subject=R 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.
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. \ No newline at end of file +email.template.password_recovery.click_here=Cliquez ici pour réinitialiser votre mot de passe. +email.no_html.error=Votre client mail ne supporte pas les messages au format HTML \ No newline at end of file From df1d013b1ceba14d5edd542fceb521b0a9c6ee44 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 18 Nov 2017 19:34:13 +0100 Subject: [PATCH 05/17] Closes #165: smtp hostname/port/username/password configurables with env --- .../sismics/docs/core/constant/Constants.java | 8 ++ .../main/java/com/sismics/util/EmailUtil.java | 44 ++++++++-- .../docs/rest/resource/AppResource.java | 85 +++++++++++++++++-- .../controller/settings/SettingsConfig.js | 9 +- .../src/partial/docs/settings.config.html | 35 ++++---- .../sismics/docs/rest/TestAppResource.java | 22 ++++- 6 files changed, 168 insertions(+), 35 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index 9ac980b2..50cbcbd4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -55,6 +55,14 @@ public class Constants { */ public static final String DEFAULT_LANGUAGE_ENV = "DOCS_DEFAULT_LANGUAGE"; + /** + * SMTP configuration environnement variables. + */ + public static final String SMTP_HOSTNAME_ENV = "DOCS_SMTP_HOSTNAME"; + public static final String SMTP_PORT_ENV = "DOCS_SMTP_PORT"; + public static final String SMTP_USERNAME_ENV = "DOCS_SMTP_USERNAME"; + public static final String SMTP_PASSWORD_ENV = "DOCS_SMTP_PASSWORD"; + /** * Expiration time of the password recovery in hours. */ diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index 51b4b2e9..88269e0d 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -73,15 +73,41 @@ public class EmailUtil { // 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)); ConfigDao configDao = new ConfigDao(); - Config usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME); - Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); - if (usernameConfig != null && passwordConfig != null) { - email.setAuthentication(usernameConfig.getValue(), passwordConfig.getValue()); + + // Hostname + String envHostname = System.getenv(Constants.SMTP_HOSTNAME_ENV); + if (envHostname == null) { + email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME)); + } else { + email.setHostName(envHostname); } + + // Port + String envPort = System.getenv(Constants.SMTP_PORT_ENV); + if (envPort == null) { + email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT)); + } else { + email.setSmtpPort(Integer.valueOf(envPort)); + } + + // Username and password + String envUsername = System.getenv(Constants.SMTP_USERNAME_ENV); + String envPassword = System.getenv(Constants.SMTP_PASSWORD_ENV); + if (envUsername == null || envPassword == null) { + Config usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME); + Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); + if (usernameConfig != null && passwordConfig != null) { + email.setAuthentication(usernameConfig.getValue(), passwordConfig.getValue()); + } + } else { + email.setAuthentication(envUsername, envPassword); + } + + // Recipient email.addTo(recipientUser.getEmail(), recipientUser.getUsername()); + + // Application name Config themeConfig = configDao.getById(ConfigType.THEME); String appName = "Sismics Docs"; if (themeConfig != null) { @@ -90,8 +116,14 @@ public class EmailUtil { appName = themeJson.getString("name", "Sismics Docs"); } } + + // From email address (defined only by configuration value in the database) email.setFrom(ConfigUtil.getConfigStringValue(ConfigType.SMTP_FROM), appName); + + // Locale (defined only by environment variable) java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV)); + + // Subject and content email.setSubject(appName + " - " + subject); email.setTextMsg(MessageUtil.getMessage(userLocale, "email.no_html.error")); 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 ca422cbe..e190fea9 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 @@ -2,10 +2,12 @@ package com.sismics.docs.rest.resource; 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.dao.jpa.FileDao; import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.event.RebuildIndexAsyncEvent; +import com.sismics.docs.core.model.jpa.Config; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.ConfigUtil; @@ -110,6 +112,75 @@ public class AppResource extends BaseResource { return Response.ok().build(); } + + /** + * Get the SMTP server configuration. + * + * @api {get} /app/config_smtp Get the SMTP server configuration + * @apiName GetAppConfigSmtp + * @apiGroup App + * @apiSuccess {String} hostname SMTP hostname + * @apiSuccess {String} port + * @apiSuccess {String} username + * @apiSuccess {String} password + * @apiSuccess {String} from + * @apiError (client) ForbiddenError Access denied + * @apiPermission admin + * @apiVersion 1.5.0 + * + * @return Response + */ + @GET + @Path("config_smtp") + public Response getConfigSmtp() { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + ConfigDao configDao = new ConfigDao(); + Config hostnameConfig = configDao.getById(ConfigType.SMTP_HOSTNAME); + Config portConfig = configDao.getById(ConfigType.SMTP_PORT); + Config usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME); + Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); + Config fromConfig = configDao.getById(ConfigType.SMTP_FROM); + JsonObjectBuilder response = Json.createObjectBuilder(); + if (System.getenv(Constants.SMTP_HOSTNAME_ENV) == null) { + if (hostnameConfig == null) { + response.addNull("hostname"); + } else { + response.add("hostname", hostnameConfig.getValue()); + } + } + if (System.getenv(Constants.SMTP_PORT_ENV) == null) { + if (portConfig == null) { + response.addNull("port"); + } else { + response.add("port", Integer.valueOf(portConfig.getValue())); + } + } + if (System.getenv(Constants.SMTP_USERNAME_ENV) == null) { + if (usernameConfig == null) { + response.addNull("username"); + } else { + response.add("username", usernameConfig.getValue()); + } + } + if (System.getenv(Constants.SMTP_PASSWORD_ENV) == null) { + if (passwordConfig == null) { + response.addNull("password"); + } else { + response.add("password", passwordConfig.getValue()); + } + } + if (fromConfig == null) { + response.addNull("from"); + } else { + response.add("from", fromConfig.getValue()); + } + + return Response.ok().entity(response.build()).build(); + } /** * Configure the SMTP server. @@ -119,9 +190,9 @@ public class AppResource extends BaseResource { * @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 + * @apiParam {String} from From address * @apiError (client) ForbiddenError Access denied * @apiError (client) ValidationError Validation error * @apiPermission admin @@ -129,18 +200,18 @@ public class AppResource extends BaseResource { * * @param hostname SMTP hostname * @param portStr SMTP port - * @param from From address * @param username SMTP username * @param password SMTP password + * @param from From address * @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) { + @FormParam("password") String password, + @FormParam("from") String from) { if (!authenticate()) { throw new ForbiddenClientException(); } @@ -157,15 +228,15 @@ public class AppResource extends BaseResource { 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); } if (!Strings.isNullOrEmpty(password)) { configDao.update(ConfigType.SMTP_PASSWORD, password); } + if (!Strings.isNullOrEmpty(from)) { + configDao.update(ConfigType.SMTP_FROM, from); + } return Response.ok().build(); } diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js index 32d55dd0..932b41d0 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js @@ -65,10 +65,13 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, }); }; + // Load SMTP config + Restangular.one('app/config_smtp').get().then(function (data) { + $scope.smtp = data; + }); + // Edit SMTP config $scope.editSmtpConfig = function () { - Restangular.one('app').post('config_smtp', $scope.smtp).then(function () { - $scope.smtpUpdated = true; - }); + Restangular.one('app').post('config_smtp', $scope.smtp); }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index b4cb36ac..c47bbab5 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -76,22 +76,35 @@

-
{{ 'settings.config.smtp_updated' | translate }}
-
-
+ +
-
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
@@ -102,20 +115,6 @@
-
- -
- -
-
- -
- -
- -
-
-
-
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.html index 6cc614c8..d0dae4ad 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.html @@ -16,7 +16,8 @@
diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index b41be0b3..743723d1 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=13 \ No newline at end of file +db.version=14 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index b41be0b3..743723d1 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=13 \ No newline at end of file +db.version=14 \ No newline at end of file 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 e5376fea..dab82572 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 @@ -37,6 +37,7 @@ public class TestAppResource extends BaseJerseyTest { Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory); Assert.assertFalse(json.getBoolean("guest_login")); Assert.assertTrue(json.containsKey("global_storage_current")); + Assert.assertTrue(json.getJsonNumber("active_user_count").longValue() > 0); // Rebuild Lucene index Response response = target().path("/app/batch/reindex").request() diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 6cddb206..e9fe73a5 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -57,6 +57,7 @@ public class TestUserResource extends BaseJerseyTest { Assert.assertNotNull(user.getJsonNumber("storage_current")); Assert.assertNotNull(user.getJsonNumber("create_date")); Assert.assertFalse(user.getBoolean("totp_enabled")); + Assert.assertFalse(user.getBoolean("disabled")); // Create a user KO (login length validation) Response response = target().path("/user").request() @@ -262,7 +263,7 @@ public class TestUserResource extends BaseJerseyTest { Assert.assertEquals("newadminemail@docs.com", json.getString("email")); // User admin update admin_user1 information - json = target().path("/user").request() + json = target().path("/user/admin_user1").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .post(Entity.form(new Form() .param("email", " alice2@docs.com ")), JsonObject.class); @@ -276,6 +277,36 @@ public class TestUserResource extends BaseJerseyTest { json = response.readEntity(JsonObject.class); Assert.assertEquals("ForbiddenError", json.getString("type")); + // User admin disable admin_user1 + json = target().path("/user/admin_user1").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("disabled", "true")), JsonObject.class); + Assert.assertEquals("ok", json.getString("status")); + + // User admin_user1 tries to authenticate + response = target().path("/user/login").request() + .post(Entity.form(new Form() + .param("username", "admin_user1") + .param("password", "12345678") + .param("remember", "false"))); + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // User admin enable admin_user1 + json = target().path("/user/admin_user1").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("disabled", "false")), JsonObject.class); + Assert.assertEquals("ok", json.getString("status")); + + // User admin_user1 tries to authenticate + response = target().path("/user/login").request() + .post(Entity.form(new Form() + .param("username", "admin_user1") + .param("password", "12345678") + .param("remember", "false"))); + Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus()); + // User admin deletes user admin_user1 json = target().path("/user/admin_user1").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) From 3f807b3e51df13144b0b89bf7d832c54c5b4d582 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 20 Nov 2017 21:25:52 +0100 Subject: [PATCH 11/17] edit -> save --- docs-web/src/main/webapp/src/partial/docs/settings.account.html | 2 +- docs-web/src/main/webapp/src/partial/docs/settings.config.html | 2 +- .../src/main/webapp/src/partial/docs/settings.group.edit.html | 2 +- .../src/main/webapp/src/partial/docs/settings.user.edit.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.account.html b/docs-web/src/main/webapp/src/partial/docs/settings.account.html index 68e5f4a4..7e81f3c8 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.account.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.account.html @@ -28,7 +28,7 @@
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index da659568..f796e160 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -118,7 +118,7 @@
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html index 9deaec3b..ca6432ae 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html @@ -33,7 +33,7 @@
-
\ No newline at end of file + + + \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/feedback.html b/docs-web/src/main/webapp/src/partial/docs/feedback.html new file mode 100644 index 00000000..7c849c0c --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/feedback.html @@ -0,0 +1,17 @@ + + + + + \ 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 fdae4870..0a582242 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -310,6 +310,35 @@ input[readonly].share-link { padding-bottom: 0; } +// Feedback +.feedback { + display: block; + position: fixed; + right: 0; + top: 200px; + transform: rotate(-90deg) translateY(-100%); + background: #ccc; + padding: 8px; + color: #fff; + font-weight: bold; + transform-origin: 100% 0; + + &:hover { + background: #aaa; + text-decoration: none; + color: #fff; + } + + &:active { + background: #444; + } + + &:active, &:focus { + text-decoration: none; + color: #fff; + } +} + // Vertical alignment .vertical-center { min-height: 100vh; From e4fe1cfa901fc5b5c1ea7004892d1715a0bae056 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 21 Nov 2017 19:37:29 +0100 Subject: [PATCH 14/17] fix active user count --- .../main/java/com/sismics/docs/core/dao/jpa/UserDao.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java index c727bddb..87166a59 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java @@ -312,8 +312,11 @@ public class UserDao { */ public long getActiveUserCount() { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query query = em.createNativeQuery("select count(u.USE_ID_C) from T_USER u where u.USE_DELETEDATE_D is null and (u.USE_DISABLEDATE_D is null or u.USE_DISABLEDATE_D > :date)"); - query.setParameter("date", DateTime.now().minusMonths(1).toDate()); + Query query = em.createNativeQuery("select count(u.USE_ID_C) from T_USER u where u.USE_DELETEDATE_D is null and (u.USE_DISABLEDATE_D is null or u.USE_DISABLEDATE_D >= :fromDate and u.USE_DISABLEDATE_D < :toDate)"); + DateTime fromDate = DateTime.now().minusMonths(1).dayOfMonth().withMinimumValue().withTimeAtStartOfDay(); + DateTime toDate = fromDate.plusMonths(1); + query.setParameter("fromDate", fromDate.toDate()); + query.setParameter("toDate", toDate.toDate()); return ((Number) query.getSingleResult()).longValue(); } } From 7194f9aac0c0a20a06c24d05b2c3a6d7672e9dae Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 21 Nov 2017 20:23:35 +0100 Subject: [PATCH 15/17] update fr translation --- docs-web/src/main/webapp/src/locale/fr.json | 45 ++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index e040c75a..9d93d6e7 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -9,7 +9,23 @@ "submit": "Connexion", "login_as_guest": "Connexion en invité", "login_failed_title": "Echec de connexion", - "login_failed_message": "Nom d'utilisateur ou mot de passe invalide" + "login_failed_message": "Nom d'utilisateur ou mot de passe invalide", + "password_lost_btn": "Mot de passe perdu ?", + "password_lost_sent_title": "Email de réinitialisation de mot de passe envoyé", + "password_lost_sent_message": "Un email a été envoyé à {{ username }} pour réinitialiser votre mot de passe", + "password_lost_error_title": "Erreur lors de la réinitialisation du mot de passe", + "password_lost_error_message": "Impossible d'envoyer un email de changement de mot de passe, veuillez contacter votre administrateur pour une réinitialisation manuelle" + }, + "passwordlost": { + "title": "Mot de passe perdu", + "message": "Veuillez entrer votre nom d'utilisateur pour recevoir un lien de réinitialisation de mot de passe. Si vous ne vous souvenez pas de votre nom d'utilisateur, veuillez contacter votre administrateur", + "submit": "Réinitialiser mon mot de passe" + }, + "passwordreset": { + "message": "Veuillez entrer un nouveau mot de passe", + "submit": "Changer mon mot de passe", + "error_title": "Erreur lors du changement de mot de passe", + "error_message": "Votre demande de changement de mot de passe a expiré, veuillez recommencer depuis la page de connexion" }, "index": { "toggle_navigation": "Afficher/cacher la navigation", @@ -19,7 +35,8 @@ "error_info": "{{ count }} nouvelle{{ count > 1 ? 's' : '' }} erreur{{ count > 1 ? 's' : '' }}", "logged_as": "Connecté en tant que {{ username }}", "nav_settings": "Paramètres", - "logout": "Déconnexion" + "logout": "Déconnexion", + "global_quota_warning": "Attention ! Quota global presque atteint à {{ current | number: 0 }}Mo ({{ percent | number: 1 }}%) utilisé sur {{ total | number: 0 }}Mo" }, "document": { "search_simple": "Recherche simple", @@ -29,7 +46,7 @@ "search_before_date": "Avant cette date", "search_after_date": "Après cette date", "search_tags": "Tags", - "search_clear": "Réinitialiser", + "search_clear": "Vider", "any_language": "Toutes les langues", "add_document": "Ajouter un document", "tags": "Tags", @@ -129,7 +146,8 @@ "add_new_document": "Ajouter à un nouveau document", "latest_activity": "Activité récente", "footer_sismics": "Conçu avec par Sismics", - "api_documentation": "Documentation API" + "api_documentation": "Documentation API", + "feedback": "Donnez-nous votre avis" }, "pdf": { "export_title": "Exporter en PDF", @@ -216,6 +234,7 @@ "add_user": "Ajouter un utilisateur", "username": "Nom d'utilisateur", "create_date": "Date de création", + "totp_enabled": "Authentification en deux étapes activée sur ce compte", "edit": { "delete_user_title": "Supprimer un utilisateur", "delete_user_message": "Etes-vous sûr de vouloir supprimer cet utilisateur ? Tous les documents, fichiers et tags associés seront supprimés", @@ -227,7 +246,8 @@ "storage_quota": "Quota de stockage", "storage_quota_placeholder": "Quota de stockage (en Mo)", "password": "Mot de passe", - "password_confirm": "Mot de passe (confirmation)" + "password_confirm": "Mot de passe (confirmation)", + "disabled": "Utilisateur désactivé" } }, "security": { @@ -284,7 +304,14 @@ "custom_css_placeholder": "CSS personnalisée ajoutée après la feuille de style principale", "logo": "Logo (Taille carrée)", "background_image": "Image de fond", - "uploading_image": "Envoi de l'image..." + "uploading_image": "Envoi de l'image...", + "title_smtp": "Configuration email", + "smtp_hostname": "Hôte SMTP", + "smtp_port": "Port SMTP", + "smtp_from": "Email d'envoi", + "smtp_username": "Nom d'utilisateur SMTP", + "smtp_password": "Mot de passe SMTP", + "smtp_updated": "Configuration SMTP mise à jour avec succès" }, "log": { "title": "Logs serveur", @@ -313,6 +340,12 @@ "new_entry": "Nouvelle entrée" } }, + "feedback": { + "title": "Donnez-nous votre avis", + "message": "Vous avez des suggestions ou des questions à propos de Sismics Docs ? Nous vous écoutons !", + "sent_title": "Avis envoyé", + "sent_message": "Merci pour votre avis ! Cela nous aidera à améliorer Sismics Docs." + }, "app_share": { "main": "Demandez un lien de partage d'un document pour y accéder", "403": { From 6596eba6caa34f4f194d19969731d480baacaf97 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 21 Nov 2017 23:56:32 +0100 Subject: [PATCH 16/17] advertise demo app --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2172015a..85ffffee 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ Docs is an open source, lightweight document management system. Docs is written in Java, and may be run on any operating system with Java support. +Demo +---- + +A demo is available at [demo.sismicsdocs.com](https://demo.sismicsdocs.com) +- Guest login is enabled with read access on all documents +- "admin" login with "admin" password +- "demo" login with "password" password + Features -------- From dc28ebfa504c618748d69ddf4a78726040d509c5 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 23 Nov 2017 01:16:54 +0100 Subject: [PATCH 17/17] fix file modal + fix file link in audit log + high quality thumbs --- .../src/main/java/com/sismics/docs/core/model/jpa/File.java | 2 +- .../src/main/java/com/sismics/docs/core/util/FileUtil.java | 4 ++-- .../main/webapp/src/app/docs/controller/document/FileView.js | 2 +- docs-web/src/main/webapp/src/app/share/controller/FileView.js | 2 +- .../src/main/webapp/src/partial/docs/directive.auditlog.html | 4 ++-- docs-web/src/main/webapp/src/partial/docs/document.html | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index 243e0386..3db2ab7b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -171,7 +171,7 @@ public class File implements Loggable { @Override public String toMessage() { // Attached document ID and name concatenated - return documentId + name; + return (documentId == null ? Strings.repeat(" ", 36) : documentId) + name; } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 6a511bf0..3d67a6a7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -129,8 +129,8 @@ public class FileUtil { if (image != null) { // Generate thumbnails from image - BufferedImage web = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 1280, Scalr.OP_ANTIALIAS); - BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 256, Scalr.OP_ANTIALIAS); + BufferedImage web = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.AUTOMATIC, 1280); + BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.AUTOMATIC, 256); image.flush(); // Write "web" encrypted image diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js index d625314a..e6407e6e 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js @@ -20,7 +20,7 @@ angular.module('docs').controller('FileView', function($uibModal, $state, $state $timeout(function () { // After all router transitions are passed, // if we are still on the file route, go back to the document - if ($state.current.name === 'document.view.content.file') { + if ($state.current.name === 'document.view.content.file' || $state.current.name === 'document.default.file') { $state.go('^', {id: $stateParams.id}); } }); diff --git a/docs-web/src/main/webapp/src/app/share/controller/FileView.js b/docs-web/src/main/webapp/src/app/share/controller/FileView.js index 800a7ceb..7b431a5b 100644 --- a/docs-web/src/main/webapp/src/app/share/controller/FileView.js +++ b/docs-web/src/main/webapp/src/app/share/controller/FileView.js @@ -15,7 +15,7 @@ angular.module('share').controller('FileView', function($uibModal, $state, $stat modal.closed = false; modal.result.then(function() { modal.closed = true; - },function() { + }, function() { modal.closed = true; $timeout(function () { // After all router transitions are passed, diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html index 554a9dc1..072552ae 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -20,8 +20,8 @@ {{ log.message }} - - + + {{ log.message | limitTo: 1000 : 36 }} {{ 'open' | translate }} diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 7b0d2722..e98d9bd8 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -159,7 +159,7 @@ next-text="{{ 'pagination.next' | translate }}" first-text="{{ 'pagination.first' | translate }}" last-text="{{ 'pagination.last' | translate }}" - total-items="totalDocuments" items-per-page="limit" max-size="5" ng-model="$parent.currentPage"> + total-items="totalDocuments" items-per-page="$parent.limit" max-size="5" ng-model="$parent.currentPage">
- ${base_url} + ${app_name}
- {{ user.username }} + {{ user.username }} + {{ user.username }} {{ user.create_date | date: dateFormat }}