mirror of
https://github.com/sismics/docs.git
synced 2024-11-22 05:57:57 +01:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
4fc434a222
@ -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
|
||||
--------
|
||||
|
||||
|
@ -51,7 +51,22 @@
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-email</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
|
@ -44,4 +44,37 @@ public class Constants {
|
||||
* Supported document languages.
|
||||
*/
|
||||
public static final List<String> SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor");
|
||||
|
||||
/**
|
||||
* Base URL environment variable.
|
||||
*/
|
||||
public static final String BASE_URL_ENV = "DOCS_BASE_URL";
|
||||
|
||||
/**
|
||||
* Default language environment variable.
|
||||
*/
|
||||
public static final String DEFAULT_LANGUAGE_ENV = "DOCS_DEFAULT_LANGUAGE";
|
||||
|
||||
/**
|
||||
* SMTP configuration environment 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";
|
||||
|
||||
/**
|
||||
* Global quota environment variable.
|
||||
*/
|
||||
public static final String GLOBAL_QUOTA_ENV = "DOCS_GLOBAL_QUOTA";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
@ -1,18 +1,5 @@
|
||||
package com.sismics.docs.core.dao.jpa;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.Query;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.sismics.docs.core.constant.AuditLogType;
|
||||
@ -28,6 +15,12 @@ import com.sismics.docs.core.util.jpa.QueryParam;
|
||||
import com.sismics.docs.core.util.jpa.SortCriteria;
|
||||
import com.sismics.util.context.ThreadLocalContext;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.Query;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Document DAO.
|
||||
*
|
||||
@ -322,4 +315,15 @@ public class DocumentDao {
|
||||
|
||||
return documentFromDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of documents.
|
||||
*
|
||||
* @return Number of documents
|
||||
*/
|
||||
public long getDocumentCount() {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
Query query = em.createNativeQuery("select count(d.DOC_ID_C) from T_DOCUMENT d where d.DOC_DELETEDATE_D is null");
|
||||
return ((Number) query.getSingleResult()).longValue();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,19 +1,5 @@
|
||||
package com.sismics.docs.core.dao.jpa;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.Query;
|
||||
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.sismics.docs.core.constant.AuditLogType;
|
||||
import com.sismics.docs.core.dao.jpa.criteria.UserCriteria;
|
||||
@ -24,6 +10,14 @@ import com.sismics.docs.core.util.jpa.QueryParam;
|
||||
import com.sismics.docs.core.util.jpa.QueryUtil;
|
||||
import com.sismics.docs.core.util.jpa.SortCriteria;
|
||||
import com.sismics.util.context.ThreadLocalContext;
|
||||
import org.joda.time.DateTime;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.Query;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* User DAO.
|
||||
@ -44,7 +38,7 @@ public class UserDao {
|
||||
q.setParameter("username", username);
|
||||
try {
|
||||
User user = (User) q.getSingleResult();
|
||||
if (!BCrypt.checkpw(password, user.getPassword())) {
|
||||
if (!BCrypt.checkpw(password, user.getPassword()) || user.getDisableDate() != null) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
@ -59,7 +53,7 @@ public class UserDao {
|
||||
* @param user User to create
|
||||
* @param userId User ID
|
||||
* @return User ID
|
||||
* @throws Exception
|
||||
* @throws Exception e
|
||||
*/
|
||||
public String create(User user, String userId) throws Exception {
|
||||
// Create the user UUID
|
||||
@ -105,7 +99,8 @@ public class UserDao {
|
||||
userFromDb.setStorageQuota(user.getStorageQuota());
|
||||
userFromDb.setStorageCurrent(user.getStorageCurrent());
|
||||
userFromDb.setTotpKey(user.getTotpKey());
|
||||
|
||||
userFromDb.setDisableDate(user.getDisableDate());
|
||||
|
||||
// Create audit log
|
||||
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);
|
||||
|
||||
@ -116,9 +111,8 @@ public class UserDao {
|
||||
* Updates a user's quota.
|
||||
*
|
||||
* @param user User to update
|
||||
* @return Updated user
|
||||
*/
|
||||
public User updateQuota(User user) {
|
||||
public void updateQuota(User user) {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
|
||||
// Get the user
|
||||
@ -128,8 +122,6 @@ public class UserDao {
|
||||
|
||||
// Update the user
|
||||
userFromDb.setStorageQuota(user.getStorageQuota());
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -256,7 +248,7 @@ public class UserDao {
|
||||
Map<String, Object> parameterMap = new HashMap<>();
|
||||
List<String> criteriaList = new ArrayList<>();
|
||||
|
||||
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5, u.USE_TOTPKEY_C as c6");
|
||||
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5, u.USE_TOTPKEY_C as c6, u.USE_DISABLEDATE_D as c7");
|
||||
sb.append(" from T_USER u ");
|
||||
|
||||
// Add search criterias
|
||||
@ -293,9 +285,38 @@ public class UserDao {
|
||||
userDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
|
||||
userDto.setStorageCurrent(((Number) o[i++]).longValue());
|
||||
userDto.setStorageQuota(((Number) o[i++]).longValue());
|
||||
userDto.setTotpKey((String) o[i]);
|
||||
userDto.setTotpKey((String) o[i++]);
|
||||
if (o[i] != null) {
|
||||
userDto.setDisableTimestamp(((Timestamp) o[i]).getTime());
|
||||
}
|
||||
userDtoList.add(userDto);
|
||||
}
|
||||
return userDtoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global storage used by all users.
|
||||
*
|
||||
* @return Current global storage
|
||||
*/
|
||||
public long getGlobalStorageCurrent() {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
Query query = em.createNativeQuery("select sum(u.USE_STORAGECURRENT_N) from T_USER u where u.USE_DELETEDATE_D is null");
|
||||
return ((Number) query.getSingleResult()).longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of active users.
|
||||
*
|
||||
* @return Number of active users
|
||||
*/
|
||||
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 >= :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();
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ public class UserDto {
|
||||
*/
|
||||
private Long createTimestamp;
|
||||
|
||||
/**
|
||||
* Disable date of this user.
|
||||
*/
|
||||
private Long disableTimestamp;
|
||||
|
||||
/**
|
||||
* Storage quota.
|
||||
*/
|
||||
@ -72,7 +77,16 @@ public class UserDto {
|
||||
public void setCreateTimestamp(Long createTimestamp) {
|
||||
this.createTimestamp = createTimestamp;
|
||||
}
|
||||
|
||||
|
||||
public Long getDisableTimestamp() {
|
||||
return disableTimestamp;
|
||||
}
|
||||
|
||||
public UserDto setDisableTimestamp(Long disableTimestamp) {
|
||||
this.disableTimestamp = disableTimestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getStorageQuota() {
|
||||
return storageQuota;
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.sismics.docs.core.event;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.sismics.docs.core.model.jpa.PasswordRecovery;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
|
||||
/**
|
||||
* Event fired on user's password lost event.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class PasswordLostEvent {
|
||||
/**
|
||||
* User.
|
||||
*/
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* Password recovery request.
|
||||
*/
|
||||
private PasswordRecovery passwordRecovery;
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public PasswordRecovery getPasswordRecovery() {
|
||||
return passwordRecovery;
|
||||
}
|
||||
|
||||
public void setPasswordRecovery(PasswordRecovery passwordRecovery) {
|
||||
this.passwordRecovery = passwordRecovery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("user", user)
|
||||
.add("passwordRecovery", "**hidden**")
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.sismics.docs.core.listener.async;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.event.PasswordLostEvent;
|
||||
import com.sismics.docs.core.model.jpa.PasswordRecovery;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.docs.core.util.TransactionUtil;
|
||||
import com.sismics.util.EmailUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Listener for password recovery requests.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class PasswordLostAsyncListener {
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
private static final Logger log = LoggerFactory.getLogger(PasswordLostAsyncListener.class);
|
||||
|
||||
/**
|
||||
* Handle events.
|
||||
*
|
||||
* @param passwordLostEvent Event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onPasswordLost(final PasswordLostEvent passwordLostEvent) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Password lost event: " + passwordLostEvent.toString());
|
||||
}
|
||||
|
||||
TransactionUtil.handle(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final User user = passwordLostEvent.getUser();
|
||||
final PasswordRecovery passwordRecovery = passwordLostEvent.getPasswordRecovery();
|
||||
|
||||
// Send the password recovery email
|
||||
Map<String, Object> paramRootMap = new HashMap<>();
|
||||
paramRootMap.put("user_name", user.getUsername());
|
||||
paramRootMap.put("password_recovery_key", passwordRecovery.getId());
|
||||
|
||||
EmailUtil.sendEmail(Constants.EMAIL_TEMPLATE_PASSWORD_RECOVERY, user, paramRootMap);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,16 @@
|
||||
package com.sismics.docs.core.model.context;
|
||||
|
||||
import com.google.common.eventbus.AsyncEventBus;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.listener.async.*;
|
||||
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.service.IndexingService;
|
||||
import com.sismics.docs.core.util.PdfUtil;
|
||||
import com.sismics.util.EnvironmentUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -7,19 +18,6 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.eventbus.AsyncEventBus;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.lowagie.text.FontFactory;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent;
|
||||
import com.sismics.docs.core.listener.async.*;
|
||||
import com.sismics.docs.core.listener.sync.DeadEventListener;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.service.IndexingService;
|
||||
import com.sismics.docs.core.util.PdfUtil;
|
||||
import com.sismics.util.EnvironmentUtil;
|
||||
|
||||
/**
|
||||
* Global application context.
|
||||
*
|
||||
@ -41,6 +39,11 @@ public class AppContext {
|
||||
*/
|
||||
private EventBus asyncEventBus;
|
||||
|
||||
/**
|
||||
* Asynchronous bus for email sending.
|
||||
*/
|
||||
private EventBus mailEventBus;
|
||||
|
||||
/**
|
||||
* Indexing service.
|
||||
*/
|
||||
@ -83,6 +86,9 @@ public class AppContext {
|
||||
asyncEventBus.register(new DocumentDeletedAsyncListener());
|
||||
asyncEventBus.register(new RebuildIndexAsyncListener());
|
||||
asyncEventBus.register(new TemporaryFileCleanupAsyncListener());
|
||||
|
||||
mailEventBus = newAsyncEventBus();
|
||||
mailEventBus.register(new PasswordLostAsyncListener());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,29 +144,18 @@ public class AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of eventBus.
|
||||
*
|
||||
* @return eventBus
|
||||
*/
|
||||
public EventBus getEventBus() {
|
||||
return eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of asyncEventBus.
|
||||
*
|
||||
* @return asyncEventBus
|
||||
*/
|
||||
public EventBus getAsyncEventBus() {
|
||||
return asyncEventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter of indexingService.
|
||||
*
|
||||
* @return indexingService
|
||||
*/
|
||||
public EventBus getMailEventBus() {
|
||||
return mailEventBus;
|
||||
}
|
||||
|
||||
public IndexingService getIndexingService() {
|
||||
return indexingService;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
package com.sismics.docs.core.model.jpa;
|
||||
|
||||
import java.util.Date;
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* User entity.
|
||||
@ -84,6 +83,12 @@ public class User implements Loggable {
|
||||
@Column(name = "USE_DELETEDATE_D")
|
||||
private Date deleteDate;
|
||||
|
||||
/**
|
||||
* Disable date.
|
||||
*/
|
||||
@Column(name = "USE_DISABLEDATE_D")
|
||||
private Date disableDate;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@ -147,7 +152,16 @@ public class User implements Loggable {
|
||||
this.deleteDate = deleteDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Date getDisableDate() {
|
||||
return disableDate;
|
||||
}
|
||||
|
||||
public User setDisableDate(Date disableDate) {
|
||||
this.disableDate = disableDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
162
docs-core/src/main/java/com/sismics/util/EmailUtil.java
Normal file
162
docs-core/src/main/java/com/sismics/util/EmailUtil.java
Normal file
@ -0,0 +1,162 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.model.jpa.Config;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.docs.core.util.ConfigUtil;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||
import freemarker.template.Template;
|
||||
import org.apache.commons.mail.HtmlEmail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObject;
|
||||
import javax.json.JsonReader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Emails utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class EmailUtil {
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
private static final Logger log = LoggerFactory.getLogger(EmailUtil.class);
|
||||
|
||||
/**
|
||||
* Returns an email content as string.
|
||||
* The content is formatted from the given Freemarker template and parameters.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param paramRootMap Map of Freemarker parameters
|
||||
* @param locale Locale
|
||||
* @return Template as string
|
||||
* @throws Exception e
|
||||
*/
|
||||
private static String getFormattedHtml(String templateName, Map<String, Object> paramRootMap, Locale locale) throws Exception {
|
||||
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
|
||||
cfg.setClassForTemplateLoading(EmailUtil.class, "/email_template");
|
||||
cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build());
|
||||
Template template = cfg.getTemplate(templateName + "/template.ftl");
|
||||
paramRootMap.put("messages", new ResourceBundleModel(MessageUtil.getMessage(locale),
|
||||
new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23).build()));
|
||||
StringWriter sw = new StringWriter();
|
||||
template.process(paramRootMap, sw);
|
||||
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending an email to a user.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param recipientUser Recipient user
|
||||
* @param subject Email subject
|
||||
* @param paramMap Email parameters
|
||||
*/
|
||||
public static void sendEmail(String templateName, User recipientUser, String subject, Map<String, Object> paramMap) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Sending email from template=" + templateName + " to user " + recipientUser);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build email headers
|
||||
HtmlEmail email = new HtmlEmail();
|
||||
email.setCharset("UTF-8");
|
||||
ConfigDao configDao = new ConfigDao();
|
||||
|
||||
// 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) {
|
||||
try (JsonReader reader = Json.createReader(new StringReader(themeConfig.getValue()))) {
|
||||
JsonObject themeJson = reader.readObject();
|
||||
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"));
|
||||
|
||||
// Add automatic parameters
|
||||
String baseUrl = System.getenv(Constants.BASE_URL_ENV);
|
||||
if (Strings.isNullOrEmpty(baseUrl)) {
|
||||
log.error("DOCS_BASE_URL environnement variable needs to be set for proper email links");
|
||||
baseUrl = ""; // At least the mail will be sent...
|
||||
}
|
||||
paramMap.put("base_url", baseUrl);
|
||||
paramMap.put("app_name", appName);
|
||||
|
||||
// Build HTML content from Freemarker template
|
||||
String htmlEmailTemplate = getFormattedHtml(templateName, paramMap, userLocale);
|
||||
email.setHtmlMsg(htmlEmailTemplate);
|
||||
|
||||
// Send the email
|
||||
email.send();
|
||||
} catch (Exception e) {
|
||||
log.error("Error sending email with template=" + templateName + " to user " + recipientUser, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending an email to a user.
|
||||
*
|
||||
* @param templateName Template name
|
||||
* @param recipientUser Recipient user
|
||||
* @param paramMap Email parameters
|
||||
*/
|
||||
public static void sendEmail(String templateName, User recipientUser, Map<String, Object> paramMap) {
|
||||
java.util.Locale userLocale = LocaleUtil.getLocale(System.getenv(Constants.DEFAULT_LANGUAGE_ENV));
|
||||
String subject = MessageUtil.getMessage(userLocale, "email.template." + templateName + ".subject");
|
||||
sendEmail(templateName, recipientUser, subject, paramMap);
|
||||
}
|
||||
}
|
36
docs-core/src/main/java/com/sismics/util/LocaleUtil.java
Normal file
36
docs-core/src/main/java/com/sismics/util/LocaleUtil.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Locale utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class LocaleUtil {
|
||||
/**
|
||||
* Returns a locale from the language / country / variation code (ex: fr_FR).
|
||||
*
|
||||
* @param localeCode Locale code
|
||||
* @return Locale instance
|
||||
*/
|
||||
public static Locale getLocale(String localeCode) {
|
||||
if (Strings.isNullOrEmpty(localeCode)) {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
|
||||
String[] localeCodeArray = localeCode.split("_");
|
||||
String language = localeCodeArray[0];
|
||||
String country = "";
|
||||
String variant = "";
|
||||
if (localeCodeArray.length >= 2) {
|
||||
country = localeCodeArray[1];
|
||||
}
|
||||
if (localeCodeArray.length >= 3) {
|
||||
variant = localeCodeArray[2];
|
||||
}
|
||||
return new Locale(language, country, variant);
|
||||
}
|
||||
}
|
43
docs-core/src/main/java/com/sismics/util/MessageUtil.java
Normal file
43
docs-core/src/main/java/com/sismics/util/MessageUtil.java
Normal file
@ -0,0 +1,43 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Messages utilities.
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class MessageUtil {
|
||||
/**
|
||||
* Returns a localized message in the specified language.
|
||||
* Returns **key** if no message exists for this key.
|
||||
*
|
||||
* @param locale Locale
|
||||
* @param key Message key
|
||||
* @param args Arguments to format
|
||||
* @return Formatted message
|
||||
*/
|
||||
public static String getMessage(Locale locale, String key, Object... args) {
|
||||
ResourceBundle resources = ResourceBundle.getBundle("messages", locale);
|
||||
String message;
|
||||
try {
|
||||
message = resources.getString(key);
|
||||
} catch (MissingResourceException e) {
|
||||
message = "**" + key + "**";
|
||||
}
|
||||
return MessageFormat.format(message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource bundle corresponding to the specified language.
|
||||
*
|
||||
* @param locale Locale
|
||||
* @return Resource bundle
|
||||
*/
|
||||
public static ResourceBundle getMessage(Locale locale) {
|
||||
return ResourceBundle.getBundle("messages", locale);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.sismics.util;
|
||||
|
||||
import freemarker.ext.beans.BeansWrapper;
|
||||
import freemarker.ext.beans.StringModel;
|
||||
import freemarker.template.TemplateModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Override of {@link freemarker.ext.beans.ResourceBundleModel}
|
||||
* to threat single quotes uniformely.
|
||||
*
|
||||
* @author bgamard
|
||||
*/
|
||||
public class ResourceBundleModel extends freemarker.ext.beans.ResourceBundleModel {
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*
|
||||
* @param bundle Resource bundle
|
||||
* @param wrapper Beans wrapper
|
||||
*/
|
||||
public ResourceBundleModel(ResourceBundle bundle, BeansWrapper wrapper) {
|
||||
super(bundle, wrapper);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public Object exec(List arguments) throws TemplateModelException {
|
||||
// Must have at least one argument - the key
|
||||
if (arguments.size() < 1)
|
||||
throw new TemplateModelException("No message key was specified");
|
||||
// Read it
|
||||
Iterator it = arguments.iterator();
|
||||
String key = unwrap((TemplateModel) it.next()).toString();
|
||||
try {
|
||||
// Copy remaining arguments into an Object[]
|
||||
int args = arguments.size() - 1;
|
||||
Object[] params = new Object[args];
|
||||
for (int i = 0; i < args; ++i)
|
||||
params[i] = unwrap((TemplateModel) it.next());
|
||||
|
||||
// Invoke format
|
||||
return new StringModel(format(key, params), wrapper);
|
||||
} catch (MissingResourceException e) {
|
||||
throw new TemplateModelException("No such key: " + key);
|
||||
} catch (Exception e) {
|
||||
throw new TemplateModelException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,6 @@
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
|
||||
version="2.0">
|
||||
<persistence-unit name="transactions-optional" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.hibernate.ejb.HibernatePersistence</provider>
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
</persistence-unit>
|
||||
</persistence>
|
@ -1 +1 @@
|
||||
db.version=12
|
||||
db.version=14
|
@ -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';
|
@ -0,0 +1,2 @@
|
||||
alter table T_USER add column USE_DISABLEDATE_D datetime;
|
||||
update T_CONFIG set CFG_VALUE_C = '14' where CFG_ID_C = 'DB_VERSION';
|
16
docs-core/src/main/resources/email_template/layout.ftl
Normal file
16
docs-core/src/main/resources/email_template/layout.ftl
Normal file
@ -0,0 +1,16 @@
|
||||
<#macro email>
|
||||
<table style="width: 100%; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';">
|
||||
<tr style="background: #242424; color: #fff;">
|
||||
<td style="padding: 12px; font-size: 16px; font-weight: bold;">
|
||||
${app_name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px; padding-top: 10px;">
|
||||
<div style="border: 1px solid #ddd; padding: 10px;">
|
||||
<#nested>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</#macro>
|
@ -0,0 +1,8 @@
|
||||
<#import "../layout.ftl" as layout>
|
||||
<@layout.email>
|
||||
<h2>${app_name} - ${messages['email.template.password_recovery.subject']}</h2>
|
||||
<p>${messages('email.template.password_recovery.hello', user_name)}</p>
|
||||
<p>${messages['email.template.password_recovery.instruction1']}</p>
|
||||
<p>${messages['email.template.password_recovery.instruction2']}</p>
|
||||
<a href="${base_url}/#/passwordreset/${password_recovery_key}">${messages['email.template.password_recovery.click_here']}</a>
|
||||
</@layout.email>
|
6
docs-core/src/main/resources/messages.properties
Normal file
6
docs-core/src/main/resources/messages.properties
Normal file
@ -0,0 +1,6 @@
|
||||
email.template.password_recovery.subject=Please reset your password
|
||||
email.template.password_recovery.hello=Hello {0}.
|
||||
email.template.password_recovery.instruction1=We have received a request to reset your password.<br/>If you did not request help, then feel free to ignore this email.
|
||||
email.template.password_recovery.instruction2=To reset your password, please visit the link below:
|
||||
email.template.password_recovery.click_here=Click here to reset your password
|
||||
email.no_html.error=Your email client does not support HTML messages
|
6
docs-core/src/main/resources/messages_fr.properties
Normal file
6
docs-core/src/main/resources/messages_fr.properties
Normal file
@ -0,0 +1,6 @@
|
||||
email.template.password_recovery.subject=Réinitialiser votre mot de passe
|
||||
email.template.password_recovery.hello=Bonjour {0}.
|
||||
email.template.password_recovery.instruction1=Nous avons reçu une demande de réinitialisation de mot de passe.<br/>Si vous n'avez rien demandé, vous pouvez ignorer cet mail.
|
||||
email.template.password_recovery.instruction2=Pour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous :
|
||||
email.template.password_recovery.click_here=Cliquez ici pour réinitialiser votre mot de passe.
|
||||
email.no_html.error=Votre client mail ne supporte pas les messages au format HTML
|
@ -5,4 +5,5 @@ log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n
|
||||
log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender
|
||||
log4j.appender.MEMORY.size=1000
|
||||
|
||||
log4j.logger.com.sismics=DEBUG
|
||||
log4j.logger.com.sismics=INFO
|
||||
log4j.logger.org.hibernate=ERROR
|
@ -93,7 +93,13 @@
|
||||
<artifactId>jersey-container-grizzly2-servlet</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<!-- Install test jar -->
|
||||
|
@ -57,7 +57,7 @@ public abstract class SecurityFilter implements Filter {
|
||||
*/
|
||||
private void injectUser(HttpServletRequest request, User user) {
|
||||
// Check if the user is still valid
|
||||
if (user != null && user.getDeleteDate() == null) {
|
||||
if (user != null && user.getDeleteDate() == null && user.getDisableDate() == null) {
|
||||
injectAuthenticatedUser(request, user);
|
||||
} else {
|
||||
injectAnonymousUser(request);
|
||||
|
@ -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 {
|
||||
@ -45,10 +55,10 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
|
||||
@Override
|
||||
protected Application configure() {
|
||||
enable(TestProperties.LOG_TRAFFIC);
|
||||
String travisEnv = System.getenv("TRAVIS");
|
||||
if (travisEnv == null || !travisEnv.equals("true")) {
|
||||
// Travis don't like entity dumped in the logs
|
||||
// Travis doesn't like big logs
|
||||
enable(TestProperties.LOG_TRAFFIC);
|
||||
enable(TestProperties.DUMP_ENTITY);
|
||||
}
|
||||
return new Application();
|
||||
@ -66,7 +76,7 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
System.setProperty("docs.header_authentication", "true");
|
||||
|
||||
clientUtil = new ClientUtil(target());
|
||||
|
||||
|
||||
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
|
||||
WebappContext context = new WebappContext("GrizzlyContext", "/docs");
|
||||
context.addFilter("requestContextFilter", RequestContextFilter.class)
|
||||
@ -84,12 +94,39 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
reg.setAsyncSupported(true);
|
||||
context.deploy(httpServer);
|
||||
httpServer.start();
|
||||
|
||||
wiser = new Wiser();
|
||||
wiser.setPort(2500);
|
||||
wiser.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an email from the list and consume it.
|
||||
*
|
||||
* @return Email content
|
||||
* @throws MessagingException e
|
||||
* @throws IOException e
|
||||
*/
|
||||
protected String popEmail() throws MessagingException, IOException {
|
||||
List<WiserMessage> wiserMessageList = wiser.getMessages();
|
||||
if (wiserMessageList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
WiserMessage wiserMessage = wiserMessageList.get(wiserMessageList.size() - 1);
|
||||
wiserMessageList.remove(wiserMessageList.size() - 1);
|
||||
MimeMessage message = wiserMessage.getMimeMessage();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
message.writeTo(os);
|
||||
return os.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
if (wiser != null) {
|
||||
wiser.stop();
|
||||
}
|
||||
if (httpServer != null) {
|
||||
httpServer.shutdownNow();
|
||||
}
|
||||
|
@ -121,6 +121,12 @@
|
||||
<artifactId>jersey-container-grizzly2-servlet</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=14
|
@ -1,32 +1,14 @@
|
||||
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.constant.Constants;
|
||||
import com.sismics.docs.core.dao.jpa.ConfigDao;
|
||||
import com.sismics.docs.core.dao.jpa.DocumentDao;
|
||||
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.Config;
|
||||
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 +18,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.
|
||||
@ -64,6 +64,10 @@ public class AppResource extends BaseResource {
|
||||
* @apiSuccess {Boolean} guest_login True if guest login is enabled
|
||||
* @apiSuccess {String} total_memory Allocated JVM memory (in bytes)
|
||||
* @apiSuccess {String} free_memory Free JVM memory (in bytes)
|
||||
* @apiSuccess {String} document_count Number of documents
|
||||
* @apiSuccess {String} active_user_count Number of active users
|
||||
* @apiSuccess {String} global_storage_current Global storage currently used (in bytes)
|
||||
* @apiSuccess {String} global_storage_quota Maximum global storage (in bytes)
|
||||
* @apiPermission none
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
@ -75,14 +79,27 @@ public class AppResource extends BaseResource {
|
||||
String currentVersion = configBundle.getString("api.current_version");
|
||||
String minVersion = configBundle.getString("api.min_version");
|
||||
Boolean guestLogin = ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN);
|
||||
UserDao userDao = new UserDao();
|
||||
DocumentDao documentDao = new DocumentDao();
|
||||
String globalQuotaStr = System.getenv(Constants.GLOBAL_QUOTA_ENV);
|
||||
long globalQuota = 0;
|
||||
if (!Strings.isNullOrEmpty(globalQuotaStr)) {
|
||||
globalQuota = Long.valueOf(globalQuotaStr);
|
||||
}
|
||||
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("current_version", currentVersion.replace("-SNAPSHOT", ""))
|
||||
.add("min_version", minVersion)
|
||||
.add("guest_login", guestLogin)
|
||||
.add("total_memory", Runtime.getRuntime().totalMemory())
|
||||
.add("free_memory", Runtime.getRuntime().freeMemory());
|
||||
|
||||
.add("free_memory", Runtime.getRuntime().freeMemory())
|
||||
.add("document_count", documentDao.getDocumentCount())
|
||||
.add("active_user_count", userDao.getActiveUserCount())
|
||||
.add("global_storage_current", userDao.getGlobalStorageCurrent());
|
||||
if (globalQuota > 0) {
|
||||
response.add("global_storage_quota", globalQuota);
|
||||
}
|
||||
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
@ -113,6 +130,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.
|
||||
@ -122,45 +208,53 @@ 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
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
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(username)) {
|
||||
configDao.update(ConfigType.SMTP_USERNAME, username);
|
||||
}
|
||||
if (password != null) {
|
||||
if (!Strings.isNullOrEmpty(password)) {
|
||||
configDao.update(ConfigType.SMTP_PASSWORD, password);
|
||||
}
|
||||
if (!Strings.isNullOrEmpty(from)) {
|
||||
configDao.update(ConfigType.SMTP_FROM, from);
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.sismics.docs.rest.resource;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.constant.PermType;
|
||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||
import com.sismics.docs.core.dao.jpa.DocumentDao;
|
||||
@ -135,11 +136,21 @@ public class FileResource extends BaseResource {
|
||||
throw new ServerException("ErrorGuessMime", "Error guessing mime type", e);
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
// Validate user quota
|
||||
if (user.getStorageCurrent() + fileSize > user.getStorageQuota()) {
|
||||
throw new ClientException("QuotaReached", "Quota limit reached");
|
||||
}
|
||||
|
||||
|
||||
// Validate global quota
|
||||
String globalStorageQuotaStr = System.getenv(Constants.GLOBAL_QUOTA_ENV);
|
||||
if (!Strings.isNullOrEmpty(globalStorageQuotaStr)) {
|
||||
long globalStorageQuota = Long.valueOf(globalStorageQuotaStr);
|
||||
long globalStorageCurrent = userDao.getGlobalStorageCurrent();
|
||||
if (globalStorageCurrent + fileSize > globalStorageQuota) {
|
||||
throw new ClientException("QuotaReached", "Global quota limit reached");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Get files of this document
|
||||
FileDao fileDao = new FileDao();
|
||||
|
@ -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;
|
||||
@ -184,6 +186,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiParam {String{8..50}} password Password
|
||||
* @apiParam {String{1..100}} email E-mail
|
||||
* @apiParam {Number} storage_quota Storage quota (in bytes)
|
||||
* @apiParam {Boolean} disabled Disabled status
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) ValidationError Validation error
|
||||
@ -202,7 +205,8 @@ public class UserResource extends BaseResource {
|
||||
@PathParam("username") String username,
|
||||
@FormParam("password") String password,
|
||||
@FormParam("email") String email,
|
||||
@FormParam("storage_quota") String storageQuotaStr) {
|
||||
@FormParam("storage_quota") String storageQuotaStr,
|
||||
@FormParam("disabled") Boolean disabled) {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
@ -216,7 +220,7 @@ public class UserResource extends BaseResource {
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.getActiveByUsername(username);
|
||||
if (user == null) {
|
||||
throw new ClientException("UserNotFound", "The user doesn't exist");
|
||||
throw new ClientException("UserNotFound", "The user does not exist");
|
||||
}
|
||||
|
||||
// Update the user
|
||||
@ -227,6 +231,22 @@ public class UserResource extends BaseResource {
|
||||
Long storageQuota = ValidationUtil.validateLong(storageQuotaStr, "storage_quota");
|
||||
user.setStorageQuota(storageQuota);
|
||||
}
|
||||
if (disabled != null) {
|
||||
// Cannot disable the admin user or the guest user
|
||||
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
|
||||
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(Sets.newHashSet(user.getRoleId()));
|
||||
if (Constants.GUEST_USER_ID.equals(username) || baseFunctionSet.contains(BaseFunction.ADMIN.name())) {
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
if (disabled && user.getDisableDate() == null) {
|
||||
// Recording the disabled date
|
||||
user.setDisableDate(new Date());
|
||||
} else if (!disabled && user.getDisableDate() != null) {
|
||||
// Emptying the disabled date
|
||||
user.setDisableDate(null);
|
||||
}
|
||||
}
|
||||
user = userDao.update(user, principal.getId());
|
||||
|
||||
// Change the password
|
||||
@ -629,6 +649,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiSuccess {Number} storage_quota Storage quota (in bytes)
|
||||
* @apiSuccess {Number} storage_current Quota used (in bytes)
|
||||
* @apiSuccess {String[]} groups Groups
|
||||
* @apiSuccess {Boolean} disabled True if the user is disabled
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) UserNotFound The user does not exist
|
||||
* @apiPermission user
|
||||
@ -666,7 +687,8 @@ public class UserResource extends BaseResource {
|
||||
.add("groups", groups)
|
||||
.add("email", user.getEmail())
|
||||
.add("storage_quota", user.getStorageQuota())
|
||||
.add("storage_current", user.getStorageCurrent());
|
||||
.add("storage_current", user.getStorageCurrent())
|
||||
.add("disabled", user.getDisableDate() != null);
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
@ -686,6 +708,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiSuccess {Number} users.storage_quota Storage quota (in bytes)
|
||||
* @apiSuccess {Number} users.storage_current Quota used (in bytes)
|
||||
* @apiSuccess {Number} users.create_date Create date (timestamp)
|
||||
* @apiSuccess {Number} users.disabled True if the user is disabled
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
@ -728,7 +751,8 @@ public class UserResource extends BaseResource {
|
||||
.add("email", userDto.getEmail())
|
||||
.add("storage_quota", userDto.getStorageQuota())
|
||||
.add("storage_current", userDto.getStorageCurrent())
|
||||
.add("create_date", userDto.getCreateTimestamp()));
|
||||
.add("create_date", userDto.getCreateTimestamp())
|
||||
.add("disabled", userDto.getDisableTimestamp() != null));
|
||||
}
|
||||
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
@ -901,7 +925,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.
|
||||
*
|
||||
|
@ -13,7 +13,7 @@ angular.module('docs',
|
||||
/**
|
||||
* Configuring modules.
|
||||
*/
|
||||
.config(function($locationProvider, $urlRouterProvider, $stateProvider, $httpProvider,
|
||||
.config(function($locationProvider, $urlRouterProvider, $stateProvider, $httpProvider, $qProvider,
|
||||
RestangularProvider, $translateProvider, timeAgoSettings, tmhDynamicLocaleProvider) {
|
||||
$locationProvider.hashPrefix('');
|
||||
|
||||
@ -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,
|
||||
@ -408,6 +417,9 @@ angular.module('docs',
|
||||
|
||||
return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;
|
||||
}];
|
||||
|
||||
// Silence unhandled rejections
|
||||
$qProvider.errorOnUnhandledRejections(false);
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -45,23 +45,23 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc
|
||||
$scope.openPasswordLost = function () {
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/docs/passwordlost.html',
|
||||
controller: 'LoginModalPasswordLost'
|
||||
}).result.then(function (email) {
|
||||
if (name === null) {
|
||||
controller: 'ModalPasswordLost'
|
||||
}).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);
|
||||
});
|
||||
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Login modal password lost controller.
|
||||
*/
|
||||
angular.module('docs').controller('LoginModalPasswordLost', function ($scope, $uibModalInstance) {
|
||||
$scope.email = '';
|
||||
$scope.close = function(name) {
|
||||
$uibModalInstance.close(name);
|
||||
}
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Modal feedback controller.
|
||||
*/
|
||||
angular.module('docs').controller('ModalFeedback', function ($scope, $uibModalInstance) {
|
||||
$scope.content = '';
|
||||
$scope.close = function(content) {
|
||||
$uibModalInstance.close(content);
|
||||
}
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Modal password lost controller.
|
||||
*/
|
||||
angular.module('docs').controller('ModalPasswordLost', function ($scope, $uibModalInstance) {
|
||||
$scope.username = '';
|
||||
$scope.close = function(username) {
|
||||
$uibModalInstance.close(username);
|
||||
}
|
||||
});
|
@ -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);
|
||||
});
|
||||
};
|
||||
});
|
@ -3,16 +3,14 @@
|
||||
/**
|
||||
* Document default controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, Upload, $translate) {
|
||||
angular.module('docs').controller('DocumentDefault', function ($scope, $rootScope, $state, Restangular, Upload, $translate, $uibModal, $dialog) {
|
||||
// Load user audit log
|
||||
Restangular.one('auditlog').get().then(function(data) {
|
||||
Restangular.one('auditlog').get().then(function (data) {
|
||||
$scope.logs = data.logs;
|
||||
});
|
||||
|
||||
/**
|
||||
* Load unlinked files.
|
||||
*/
|
||||
$scope.loadFiles = function() {
|
||||
// Load unlinked files
|
||||
$scope.loadFiles = function () {
|
||||
Restangular.one('file/list').get().then(function (data) {
|
||||
$scope.files = data.files;
|
||||
// TODO Keep currently uploading files
|
||||
@ -20,15 +18,12 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
};
|
||||
$scope.loadFiles();
|
||||
|
||||
/**
|
||||
* File has been drag & dropped.
|
||||
* @param files
|
||||
*/
|
||||
$scope.fileDropped = function(files) {
|
||||
// File has been drag & dropped
|
||||
$scope.fileDropped = function (files) {
|
||||
if (files && files.length) {
|
||||
// Adding files to the UI
|
||||
var newfiles = [];
|
||||
_.each(files, function(file) {
|
||||
_.each(files, function (file) {
|
||||
var newfile = {
|
||||
progress: 0,
|
||||
name: file.name,
|
||||
@ -42,7 +37,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
|
||||
// Uploading files sequentially
|
||||
var key = 0;
|
||||
var then = function() {
|
||||
var then = function () {
|
||||
if (files[key]) {
|
||||
$scope.uploadFile(files[key], newfiles[key++]).then(then);
|
||||
}
|
||||
@ -51,12 +46,8 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
* @param file
|
||||
* @param newfile
|
||||
*/
|
||||
$scope.uploadFile = function(file, newfile) {
|
||||
// Upload a file
|
||||
$scope.uploadFile = function (file, newfile) {
|
||||
// Upload the file
|
||||
newfile.status = $translate.instant('document.default.upload_progress');
|
||||
return Upload.upload({
|
||||
@ -77,26 +68,22 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
})
|
||||
.error(function (data) {
|
||||
newfile.status = $translate.instant('document.default.upload_error');
|
||||
if (data.type == 'QuotaReached') {
|
||||
if (data.type === 'QuotaReached') {
|
||||
newfile.status += ' - ' + $translate.instant('document.default.upload_error_quota');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to the selected file.
|
||||
*/
|
||||
//Navigate to the selected file
|
||||
$scope.openFile = function (file) {
|
||||
$state.go('document.default.file', { fileId: file.id })
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*/
|
||||
// Delete a file
|
||||
$scope.deleteFile = function ($event, file) {
|
||||
$event.stopPropagation();
|
||||
|
||||
Restangular.one('file', file.id).remove().then(function() {
|
||||
Restangular.one('file', file.id).remove().then(function () {
|
||||
// File deleted, decrease used quota
|
||||
$rootScope.userInfo.storage_current -= file.size;
|
||||
|
||||
@ -106,17 +93,36 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns checked files.
|
||||
*/
|
||||
$scope.checkedFiles = function() {
|
||||
// Returns checked files
|
||||
$scope.checkedFiles = function () {
|
||||
return _.where($scope.files, { checked: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a document with checked files.
|
||||
*/
|
||||
$scope.addDocument = function() {
|
||||
// Add a document with checked files
|
||||
$scope.addDocument = function () {
|
||||
$state.go('document.add', { files: _.pluck($scope.checkedFiles(), 'id') });
|
||||
};
|
||||
|
||||
// Open the feedback modal
|
||||
$scope.openFeedback = function () {
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/docs/feedback.html',
|
||||
controller: 'ModalFeedback'
|
||||
}).result.then(function (content) {
|
||||
if (content === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Restangular.withConfig(function (RestangularConfigurer) {
|
||||
RestangularConfigurer.setBaseUrl('https://api.sismicsdocs.com');
|
||||
}).one('api').post('feedback', {
|
||||
content: content
|
||||
}).then(function () {
|
||||
var title = $translate.instant('feedback.sent_title');
|
||||
var msg = $translate.instant('feedback.sent_message');
|
||||
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
|
||||
$dialog.messageBox(title, msg, btns);
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
@ -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});
|
||||
}
|
||||
});
|
||||
|
@ -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,14 @@ 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);
|
||||
};
|
||||
});
|
@ -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,
|
||||
|
@ -48,7 +48,9 @@
|
||||
<script src="app/docs/app.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Main.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Login.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/LoginModalPasswordLost.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/ModalPasswordLost.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/ModalFeedback.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/PasswordReset.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Navigation.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Footer.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/document/Document.js" type="text/javascript"></script>
|
||||
@ -156,6 +158,10 @@
|
||||
|
||||
<div class="row" ng-controller="Footer">
|
||||
<div class="col-md-12 footer text-center text-muted">
|
||||
<div class="alert alert-danger" ng-show="app.global_storage_quota && app.global_storage_quota * 0.8 < app.global_storage_current"
|
||||
translate="index.global_quota_warning"
|
||||
translate-values="{ current: app.global_storage_current / 1000000, percent: app.global_storage_current / app.global_storage_quota * 100, total: app.global_storage_quota / 1000000 }">
|
||||
</div>
|
||||
<ul class="list-inline">
|
||||
<li uib-dropdown class="dropdown">
|
||||
<a href uib-dropdown-toggle>
|
||||
|
@ -12,15 +12,21 @@
|
||||
"login_failed_message": "Username or password invalid",
|
||||
"password_lost_btn": "Password lost?",
|
||||
"password_lost_sent_title": "Password reset email sent",
|
||||
"password_lost_sent_message": "An email has been sent to <strong>{{ email }}</strong> to reset your password",
|
||||
"password_lost_sent_message": "An email has been sent to <strong>{{ username }}</strong> to reset your password",
|
||||
"password_lost_error_title": "Password reset error",
|
||||
"password_lost_error_message": "Unable to send a password reset email, please contact your administrator for a manual reset"
|
||||
},
|
||||
"passwordlost": {
|
||||
"title": "Password lost",
|
||||
"message": "Please enter your email address to receive a password reset link",
|
||||
"message": "Please enter your username to receive a password reset link. If you don't remember your username, please contact your administrator",
|
||||
"submit": "Reset my password"
|
||||
},
|
||||
"passwordreset": {
|
||||
"message": "Please enter a new password",
|
||||
"submit": "Change my password",
|
||||
"error_title": "Error changing your password",
|
||||
"error_message": "Your password recovery request is expired, please ask a new one on the login page"
|
||||
},
|
||||
"index": {
|
||||
"toggle_navigation": "Toggle navigation",
|
||||
"nav_documents": "Documents",
|
||||
@ -29,7 +35,8 @@
|
||||
"error_info": "{{ count }} new error{{ count > 1 ? 's' : '' }}",
|
||||
"logged_as": "Logged in as {{ username }}",
|
||||
"nav_settings": "Settings",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"global_quota_warning": "<strong>Warning!</strong> Global quota almost reached at {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) used on {{ total | number: 0 }}MB"
|
||||
},
|
||||
"document": {
|
||||
"search_simple": "Simple search",
|
||||
@ -139,7 +146,8 @@
|
||||
"add_new_document": "Add to new document",
|
||||
"latest_activity": "Latest activity",
|
||||
"footer_sismics": "Crafted with <span class=\"glyphicon glyphicon-heart\"></span> by <a href=\"https://www.sismics.com\" target=\"_blank\">Sismics</a>",
|
||||
"api_documentation": "API Documentation"
|
||||
"api_documentation": "API Documentation",
|
||||
"feedback": "Give us a feedback"
|
||||
},
|
||||
"pdf": {
|
||||
"export_title": "Export to PDF",
|
||||
@ -238,7 +246,8 @@
|
||||
"storage_quota": "Storage quota",
|
||||
"storage_quota_placeholder": "Storage quota (in MB)",
|
||||
"password": "Password",
|
||||
"password_confirm": "Password (confirm)"
|
||||
"password_confirm": "Password (confirm)",
|
||||
"disabled": "Disabled user"
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
@ -295,7 +304,14 @@
|
||||
"custom_css_placeholder": "Custom CSS to add after the main stylesheet",
|
||||
"logo": "Logo (squared size)",
|
||||
"background_image": "Background image",
|
||||
"uploading_image": "Uploading the image..."
|
||||
"uploading_image": "Uploading the image...",
|
||||
"title_smtp": "Email <small>configuration</small>",
|
||||
"smtp_hostname": "SMTP hostname",
|
||||
"smtp_port": "SMTP port",
|
||||
"smtp_from": "Sender e-mail",
|
||||
"smtp_username": "SMTP username",
|
||||
"smtp_password": "SMTP password",
|
||||
"smtp_updated": "SMTP configuration updated successfully"
|
||||
},
|
||||
"log": {
|
||||
"title": "Server <small>logs</small>",
|
||||
@ -324,6 +340,12 @@
|
||||
"new_entry": "New entry"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"title": "Give us a feedback",
|
||||
"message": "Any suggestion or question about Sismics Docs? We listen to you!",
|
||||
"sent_title": "Feedback sent",
|
||||
"sent_message": "Thank you for your feedback! It will help us make Sismics Docs even better."
|
||||
},
|
||||
"app_share": {
|
||||
"main": "Ask a shared document link to access it",
|
||||
"403": {
|
||||
|
@ -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é à <strong>{{ username }}</strong> 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": "<strong>Attention !</strong> 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 <span class=\"glyphicon glyphicon-heart\"></span> par <a href=\"https://www.sismics.com\" target=\"_blank\">Sismics</a>",
|
||||
"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": "<small>Configuration</small> 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": "<small>Logs</small> 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": {
|
||||
|
@ -20,8 +20,8 @@
|
||||
<span ng-switch-when="Document">
|
||||
<a ng-href="#/document/view/{{ log.target }}">{{ log.message }}</a>
|
||||
</span>
|
||||
<span ng-switch-when="File">
|
||||
<a ng-if="log.message" ng-href="#/document/view/{{ log.message | limitTo: 36 }}/content/file/{{ log.target }}">
|
||||
<span ng-switch-when="File" ng-init="hasDocument = log.message.substring(0, 36).trim().length > 0">
|
||||
<a ng-if="log.message" ng-href="#/document/{{ hasDocument ? 'view/' + log.message.substring(0, 36) + '/content/' : '' }}file/{{ log.target }}">
|
||||
<span ng-if="log.message.length > 36">{{ log.message | limitTo: 1000 : 36 }}</span>
|
||||
<span ng-if="log.message.length == 36">{{ 'open' | translate }}</span>
|
||||
</a>
|
||||
|
@ -67,4 +67,8 @@
|
||||
</h3>
|
||||
<audit-log logs="logs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href class="feedback" ng-click="openFeedback()">
|
||||
{{ 'document.default.feedback' | translate }}
|
||||
</a>
|
@ -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"></ul>
|
||||
total-items="totalDocuments" items-per-page="$parent.limit" max-size="5" ng-model="$parent.currentPage"></ul>
|
||||
<label class="sr-only" for="pagesizeSelect">{{ 'document.page_size' | translate }}</label>
|
||||
<select ng-model="limit" id="pagesizeSelect" class="form-control">
|
||||
<option value="10">{{ 'document.page_size_10' | translate }}</option>
|
||||
|
17
docs-web/src/main/webapp/src/partial/docs/feedback.html
Normal file
17
docs-web/src/main/webapp/src/partial/docs/feedback.html
Normal file
@ -0,0 +1,17 @@
|
||||
<form name="form">
|
||||
<div class="modal-header">
|
||||
<h3>{{ 'feedback.title' | translate }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<label for="inputContent">{{ 'feedback.message' | translate }}</label>
|
||||
<textarea name="content" class="form-control" required id="inputContent" ng-model="content"></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="close(content)" class="btn btn-primary" ng-disabled="!form.$valid">
|
||||
<span class="glyphicon glyphicon-send"></span> {{ 'send' | translate }}
|
||||
</button>
|
||||
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
@ -5,11 +5,11 @@
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<label for="share-result">{{ 'passwordlost.message' | translate }}</label>
|
||||
<input name="email" class="form-control" type="email" required id="share-result" ng-model="email" />
|
||||
<input name="username" class="form-control" type="text" required id="share-result" ng-model="username" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="close(email)" class="btn btn-primary" ng-disabled="!form.$valid">
|
||||
<button ng-click="close(username)" class="btn btn-primary" ng-disabled="!form.$valid">
|
||||
<span class="glyphicon glyphicon-envelope"></span> {{ 'passwordlost.submit' | translate }}
|
||||
</button>
|
||||
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | translate }}</button>
|
||||
|
25
docs-web/src/main/webapp/src/partial/docs/passwordreset.html
Normal file
25
docs-web/src/main/webapp/src/partial/docs/passwordreset.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-offset-1 col-xs-10 col-sm-offset-2 col-sm-8">
|
||||
<form class="form-horizontal" name="form" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': !form.password.$valid && form.$dirty }">
|
||||
<label for="inputPassword" class="col-sm-4 control-label">{{ 'passwordreset.message' | translate }}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" name="password" ng-model="password" required ng-minlength="8" ng-maxlength="50" class="form-control" id="inputPassword">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<span class="help-block" ng-show="form.password.$error.required && form.$dirty">{{ 'validation.required' | translate }}</span>
|
||||
<span class="help-block" ng-show="form.password.$error.minlength && form.$dirty">{{ 'validation.too_short' | translate }}</span>
|
||||
<span class="help-block" ng-show="form.password.$error.maxlength && form.$dirty">{{ 'validation.too_long' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-4 col-sm-5">
|
||||
<button class="btn btn-primary" ng-disabled="!form.$valid" ng-click="submit()">
|
||||
<span class="glyphicon glyphicon-lock"></span>
|
||||
{{ 'passwordreset.submit' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -28,7 +28,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" ng-click="editUser()" ng-disabled="!editUserForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ 'edit' | translate }}
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ 'save' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,4 +73,53 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h1 translate="settings.config.title_smtp"></h1>
|
||||
<form class="form-horizontal" name="smtpForm" novalidate>
|
||||
<div class="form-group" ng-show="smtp.hasOwnProperty('hostname')" ng-class="{ 'has-error': !smtpForm.hostname.$valid && smtpForm.$dirty }">
|
||||
<label class="col-sm-2 control-label" for="smtpHostname">{{ 'settings.config.smtp_hostname' | translate }}</label>
|
||||
<div class="col-sm-7">
|
||||
<input name="hostname" type="text" ng-disabled="!smtp.hasOwnProperty('hostname')" class="form-control" id="smtpHostname" ng-model="smtp.hostname" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="smtp.hasOwnProperty('port')" ng-class="{ 'has-error': !smtpForm.port.$valid && smtpForm.$dirty }">
|
||||
<label class="col-sm-2 control-label" for="smtpPort">{{ 'settings.config.smtp_port' | translate }}</label>
|
||||
<div class="col-sm-7">
|
||||
<input name="port" type="number" ng-disabled="!smtp.hasOwnProperty('port')" class="form-control" id="smtpPort" ng-model="smtp.port" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="smtp.hasOwnProperty('username')">
|
||||
<label class="col-sm-2 control-label" for="smtpUsername">{{ 'settings.config.smtp_username' | translate }}</label>
|
||||
<div class="col-sm-7">
|
||||
<input name="username" type="text" ng-disabled="!smtp.hasOwnProperty('username')" class="form-control" id="smtpUsername" ng-model="smtp.username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="smtp.hasOwnProperty('password')">
|
||||
<label class="col-sm-2 control-label" for="smtpPassword">{{ 'settings.config.smtp_password' | translate }}</label>
|
||||
<div class="col-sm-7">
|
||||
<input name="password" type="password" ng-disabled="!smtp.hasOwnProperty('password')" class="form-control" id="smtpPassword" ng-model="smtp.password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': !smtpForm.from.$valid && smtpForm.$dirty }">
|
||||
<label class="col-sm-2 control-label" for="smtpFrom">{{ 'settings.config.smtp_from' | translate }}</label>
|
||||
<div class="col-sm-7">
|
||||
<input name="from" type="email" class="form-control" id="smtpFrom" ng-model="smtp.from" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<span class="help-block" ng-show="smtpForm.from.$error.email && smtpForm.$dirty">{{ 'validation.email' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" ng-click="editSmtpConfig()" ng-disabled="!smtpForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ 'save' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -33,7 +33,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editGroupForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'edit' : 'add' | translate }}
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'save' : 'add' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
|
||||
<span class="glyphicon glyphicon-trash"></span> {{ 'delete' | translate }}
|
||||
|
@ -88,12 +88,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="isEdit() && user.username != 'admin' && user.username != 'guest'">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox text-danger">
|
||||
<label>
|
||||
<input name="disabled" type="checkbox" ng-model="user.disabled" />
|
||||
<strong>{{ 'settings.user.edit.disabled' | translate }}</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'edit' : 'add' | translate }}
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'save' : 'add' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'guest'">
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'admin' && user.username != 'guest'">
|
||||
<span class="glyphicon glyphicon-trash"></span> {{ 'delete' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -16,7 +16,8 @@
|
||||
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)"
|
||||
ng-class="{ active: $stateParams.username == user.username }">
|
||||
<td>
|
||||
{{ user.username }}
|
||||
<span ng-if="!user.disabled">{{ user.username }}</span>
|
||||
<s ng-if="user.disabled">{{ user.username }}</s>
|
||||
<span class="glyphicon glyphicon-lock" ng-show="user.totp_enabled" uib-tooltip="{{ 'settings.user.totp_enabled' | translate }}"></span>
|
||||
</td>
|
||||
<td>{{ user.create_date | date: dateFormat }}</td>
|
||||
|
@ -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;
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=14
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=12
|
||||
db.version=14
|
@ -36,6 +36,8 @@ public class TestAppResource extends BaseJerseyTest {
|
||||
Long totalMemory = json.getJsonNumber("total_memory").longValue();
|
||||
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()
|
||||
@ -163,14 +165,34 @@ public class TestAppResource extends BaseJerseyTest {
|
||||
// Login admin
|
||||
String adminToken = clientUtil.login("admin", "admin", false);
|
||||
|
||||
// Get SMTP configuration
|
||||
JsonObject json = target().path("/app/config_smtp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.get(JsonObject.class);
|
||||
Assert.assertTrue(json.isNull("hostname"));
|
||||
Assert.assertTrue(json.isNull("port"));
|
||||
Assert.assertTrue(json.isNull("username"));
|
||||
Assert.assertTrue(json.isNull("password"));
|
||||
Assert.assertTrue(json.isNull("from"));
|
||||
|
||||
// Change SMTP configuration
|
||||
target().path("/app/config_smtp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.post(Entity.form(new Form()
|
||||
.param("hostname", "smtp.sismics.com")
|
||||
.param("port", "1234")
|
||||
.param("from", "contact@sismics.com")
|
||||
.param("username", "sismics")
|
||||
.param("from", "contact@sismics.com")
|
||||
), JsonObject.class);
|
||||
|
||||
// Get SMTP configuration
|
||||
json = target().path("/app/config_smtp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.get(JsonObject.class);
|
||||
Assert.assertEquals("smtp.sismics.com", json.getString("hostname"));
|
||||
Assert.assertEquals(1234, json.getInt("port"));
|
||||
Assert.assertEquals("sismics", json.getString("username"));
|
||||
Assert.assertTrue(json.isNull("password"));
|
||||
Assert.assertEquals("contact@sismics.com", json.getString("from"));
|
||||
}
|
||||
}
|
@ -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.
|
||||
@ -54,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()
|
||||
@ -259,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);
|
||||
@ -273,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)
|
||||
@ -354,4 +388,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"));
|
||||
}
|
||||
}
|
@ -6,5 +6,7 @@ log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender
|
||||
log4j.appender.MEMORY.size=1000
|
||||
|
||||
log4j.logger.com.sismics=INFO
|
||||
log4j.logger.org.hibernate=INFO
|
||||
log4j.logger.org.apache.pdfbox=INFO
|
||||
log4j.logger.com.sismics.util.jpa=ERROR
|
||||
log4j.logger.org.hibernate=ERROR
|
||||
log4j.logger.org.apache.pdfbox=INFO
|
||||
log4j.logger.com.mchange=ERROR
|
||||
|
36
pom.xml
36
pom.xml
@ -19,6 +19,8 @@
|
||||
<org.apache.commons.commons-compress.version>1.10</org.apache.commons.commons-compress.version>
|
||||
<commons-lang.commons-lang.version>2.6</commons-lang.commons-lang.version>
|
||||
<commons-io.commons-io.version>2.4</commons-io.commons-io.version>
|
||||
<org.apache.commons.commons-email.version>1.5</org.apache.commons.commons-email.version>
|
||||
<org.freemarker.freemarker.version>2.3.23</org.freemarker.freemarker.version>
|
||||
<commons-dbcp.version>1.4</commons-dbcp.version>
|
||||
<com.google.guava.guava.version>19.0</com.google.guava.guava.version>
|
||||
<log4j.log4j.version>1.2.16</log4j.log4j.version>
|
||||
@ -27,6 +29,7 @@
|
||||
<junit.junit.version>4.12</junit.junit.version>
|
||||
<com.h2database.h2.version>1.4.191</com.h2database.h2.version>
|
||||
<org.glassfish.jersey.version>2.22.2</org.glassfish.jersey.version>
|
||||
<org.glassfish.javax.json.version>1.0.4</org.glassfish.javax.json.version>
|
||||
<org.mindrot.jbcrypt>0.3m</org.mindrot.jbcrypt>
|
||||
<org.apache.lucene.version>5.5.0</org.apache.lucene.version>
|
||||
<org.imgscalr.imgscalr-lib.version>4.2</org.imgscalr.imgscalr-lib.version>
|
||||
@ -40,6 +43,7 @@
|
||||
<com.twelvemonkeys.imageio.version>3.2.1</com.twelvemonkeys.imageio.version>
|
||||
<com.levigo.jbig2.levigo-jbig2-imageio.version>1.6.5</com.levigo.jbig2.levigo-jbig2-imageio.version>
|
||||
<com.github.jai-imageio.jai-imageio-core.version>1.3.1</com.github.jai-imageio.jai-imageio-core.version>
|
||||
<org.subethamail.subethasmtp-wiser.version>1.2</org.subethamail.subethasmtp-wiser.version>
|
||||
|
||||
<org.eclipse.jetty.jetty-server.version>9.2.13.v20150730</org.eclipse.jetty.jetty-server.version>
|
||||
<org.eclipse.jetty.jetty-webapp.version>9.2.13.v20150730</org.eclipse.jetty.jetty-webapp.version>
|
||||
@ -109,6 +113,10 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${org.apache.maven.plugins.maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<forkCount>1</forkCount>
|
||||
<reuseForks>false</reuseForks>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
@ -193,6 +201,12 @@
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.commons-io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-email</artifactId>
|
||||
<version>${org.apache.commons.commons-email.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
@ -284,7 +298,13 @@
|
||||
<artifactId>jersey-container-grizzly2-servlet</artifactId>
|
||||
<version>${org.glassfish.jersey.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.json</artifactId>
|
||||
<version>${org.glassfish.javax.json.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
@ -314,7 +334,13 @@
|
||||
<artifactId>commons-dbcp</artifactId>
|
||||
<version>${commons-dbcp.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>${org.freemarker.freemarker.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
@ -375,6 +401,12 @@
|
||||
<version>${fr.opensagres.xdocreport.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp-wiser</artifactId>
|
||||
<version>${org.subethamail.subethasmtp-wiser.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Servlet listener to register SPI ImageIO plugins -->
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.servlet</groupId>
|
||||
|
Loading…
Reference in New Issue
Block a user