1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-13 01:36:18 +00:00

Merge remote-tracking branch 'origin/master'

This commit is contained in:
bgamard
2017-11-23 15:32:31 +01:00
68 changed files with 1626 additions and 250 deletions

View File

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

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=12
db.version=14

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
'use strict';
/**
* Password reset controller.
*/
angular.module('docs').controller('PasswordReset', function($scope, Restangular, $state, $stateParams, $translate, $dialog) {
$scope.submit = function () {
Restangular.one('user').post('password_reset', {
key: $stateParams.key,
password: $scope.password
}).then(function () {
$state.go('login');
}, function () {
var title = $translate.instant('passwordreset.error_title');
var msg = $translate.instant('passwordreset.error_message');
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
});
};
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,15 +12,21 @@
"login_failed_message": "Username or password invalid",
"password_lost_btn": "Password lost?",
"password_lost_sent_title": "Password reset email sent",
"password_lost_sent_message": "An email has been sent to <strong>{{ email }}</strong> to reset your password",
"password_lost_sent_message": "An email has been sent to <strong>{{ username }}</strong> to reset your password",
"password_lost_error_title": "Password reset error",
"password_lost_error_message": "Unable to send a password reset email, please contact your administrator for a manual reset"
},
"passwordlost": {
"title": "Password lost",
"message": "Please enter your email address to receive a password reset link",
"message": "Please enter your username to receive a password reset link. If you don't remember your username, please contact your administrator",
"submit": "Reset my password"
},
"passwordreset": {
"message": "Please enter a new password",
"submit": "Change my password",
"error_title": "Error changing your password",
"error_message": "Your password recovery request is expired, please ask a new one on the login page"
},
"index": {
"toggle_navigation": "Toggle navigation",
"nav_documents": "Documents",
@@ -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": {

View File

@@ -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": {

View File

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

View File

@@ -67,4 +67,8 @@
</h3>
<audit-log logs="logs" />
</div>
</div>
</div>
<a href class="feedback" ng-click="openFeedback()">
{{ 'document.default.feedback' | translate }}
</a>

View File

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

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

View File

@@ -5,11 +5,11 @@
<div class="modal-body">
<p>
<label for="share-result">{{ 'passwordlost.message' | translate }}</label>
<input name="email" class="form-control" type="email" required id="share-result" ng-model="email" />
<input name="username" class="form-control" type="text" required id="share-result" ng-model="username" />
</p>
</div>
<div class="modal-footer">
<button ng-click="close(email)" class="btn btn-primary" ng-disabled="!form.$valid">
<button ng-click="close(username)" class="btn btn-primary" ng-disabled="!form.$valid">
<span class="glyphicon glyphicon-envelope"></span> {{ 'passwordlost.submit' | translate }}
</button>
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | translate }}</button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=12
db.version=14

View File

@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=12
db.version=14

View File

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

View File

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

View File

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