diff --git a/back/methods/account/change-password.js b/back/methods/account/change-password.js new file mode 100644 index 000000000..25b63b9a8 --- /dev/null +++ b/back/methods/account/change-password.js @@ -0,0 +1,34 @@ + +module.exports = Self => { + Self.remoteMethod('changePassword', { + description: 'Changes the user password', + accepts: [ + { + arg: 'id', + type: 'Number', + description: 'The user id', + http: {source: 'path'} + }, { + arg: 'oldPassword', + type: 'String', + description: 'The old password', + required: true + }, { + arg: 'newPassword', + type: 'String', + description: 'The new password', + required: true + } + ], + http: { + path: `/:id/changePassword`, + verb: 'PATCH' + } + }); + + Self.changePassword = async function(id, oldPassword, newPassword) { + await Self.rawSql(`CALL account.user_changePassword(?, ?, ?)`, + [id, oldPassword, newPassword]); + await Self.app.models.UserAccount.syncById(id, newPassword); + }; +}; diff --git a/back/methods/account/login.js b/back/methods/account/login.js index 075d3669c..340300e23 100644 --- a/back/methods/account/login.js +++ b/back/methods/account/login.js @@ -26,9 +26,9 @@ module.exports = Self => { }); Self.login = async function(user, password) { + let $ = Self.app.models; let token; let usesEmail = user.indexOf('@') !== -1; - let User = Self.app.models.User; let loginInfo = {password}; @@ -38,7 +38,7 @@ module.exports = Self => { loginInfo.username = user; try { - token = await User.login(loginInfo, 'user'); + token = await $.User.login(loginInfo, 'user'); } catch (err) { if (err.code != 'LOGIN_FAILED' || usesEmail) throw err; @@ -49,17 +49,8 @@ module.exports = Self => { if (!instance || instance.password !== md5(password || '')) throw err; - let where = {id: instance.id}; - let userData = { - id: instance.id, - username: user, - password: password, - email: instance.email, - created: instance.created, - updated: instance.updated - }; - await User.upsertWithWhere(where, userData); - token = await User.login(loginInfo, 'user'); + await $.UserAccount.sync(user, password); + token = await $.User.login(loginInfo, 'user'); } return {token: token.id}; diff --git a/back/methods/account/set-password.js b/back/methods/account/set-password.js new file mode 100644 index 000000000..fc54b5abe --- /dev/null +++ b/back/methods/account/set-password.js @@ -0,0 +1,29 @@ + +module.exports = Self => { + Self.remoteMethod('setPassword', { + description: 'Sets the user password', + accepts: [ + { + arg: 'id', + type: 'Number', + description: 'The user id', + http: {source: 'path'} + }, { + arg: 'newPassword', + type: 'String', + description: 'The new password', + required: true + } + ], + http: { + path: `/:id/setPassword`, + verb: 'PATCH' + } + }); + + Self.setPassword = async function(id, newPassword) { + await Self.rawSql(`CALL account.user_setPassword(?, ?)`, + [id, newPassword]); + await Self.app.models.UserAccount.syncById(id, newPassword); + }; +}; diff --git a/back/methods/account/specs/change-password.spec.js b/back/methods/account/specs/change-password.spec.js new file mode 100644 index 000000000..9f1130df5 --- /dev/null +++ b/back/methods/account/specs/change-password.spec.js @@ -0,0 +1,9 @@ +const app = require('vn-loopback/server/server'); + +describe('account changePassword()', () => { + it('should throw an error when old password is wrong', async() => { + let req = app.models.Account.changePassword(null, 1, 'wrongOldPass', 'newPass'); + + await expectAsync(req).toBeRejected(); + }); +}); diff --git a/back/methods/account/specs/set-password.spec.js b/back/methods/account/specs/set-password.spec.js new file mode 100644 index 000000000..c76fd52b8 --- /dev/null +++ b/back/methods/account/specs/set-password.spec.js @@ -0,0 +1,15 @@ +const app = require('vn-loopback/server/server'); + +describe('account changePassword()', () => { + it('should throw an error when password does not meet requirements', async() => { + let req = app.models.Account.setPassword(1, 'insecurePass'); + + await expectAsync(req).toBeRejected(); + }); + + it('should update password when it passes requirements', async() => { + let req = app.models.Account.setPassword(1, 'Very$ecurePa22.'); + + await expectAsync(req).toBeResolved(); + }); +}); diff --git a/back/model-config.json b/back/model-config.json index dc5cde217..22d0e2327 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -38,6 +38,9 @@ "ImageCollectionSize": { "dataSource": "vn" }, + "Language": { + "dataSource": "vn" + }, "Province": { "dataSource": "vn" }, diff --git a/back/models/account.js b/back/models/account.js index a0b08dd57..1c6ac04f8 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -4,6 +4,8 @@ module.exports = Self => { require('../methods/account/login')(Self); require('../methods/account/logout')(Self); require('../methods/account/acl')(Self); + require('../methods/account/change-password')(Self); + require('../methods/account/set-password')(Self); require('../methods/account/validate-token')(Self); // Validations @@ -38,17 +40,9 @@ module.exports = Self => { Self.getCurrentUserData = async function(ctx) { let userId = ctx.req.accessToken.userId; - - let account = await Self.findById(userId, { + return await Self.findById(userId, { fields: ['id', 'name', 'nickname'] }); - - let worker = await Self.app.models.Worker.findOne({ - fields: ['id'], - where: {userFk: userId} - }); - - return Object.assign(account, {workerId: worker.id}); }; /** diff --git a/back/models/account.json b/back/models/account.json index 7186621b4..483973515 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -5,7 +5,7 @@ "mysql": { "table": "account.user" } - }, + }, "properties": { "id": { "type": "number", @@ -24,10 +24,16 @@ "nickname": { "type": "string" }, + "lang": { + "type": "string" + }, "password": { "type": "string", "required": true }, + "bcryptPassword": { + "type": "string" + }, "active": { "type": "boolean" }, @@ -41,44 +47,47 @@ "type": "date" } }, - "relations": { - "role": { - "type": "belongsTo", - "model": "Role", - "foreignKey": "roleFk" - }, - "emailUser": { - "type": "hasOne", - "model": "EmailUser", - "foreignKey": "userFk" + "relations": { + "role": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "roleFk" + }, + "roles": { + "type": "hasMany", + "model": "RoleRole", + "foreignKey": "role" + }, + "emailUser": { + "type": "hasOne", + "model": "EmailUser", + "foreignKey": "userFk" }, "worker": { "type": "hasOne", "model": "Worker", "foreignKey": "userFk" - } - }, - "acls": [ - { + } + }, + "acls": [ + { "property": "login", "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW" - }, - { + }, { "property": "logout", "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$authenticated", "permission": "ALLOW" - }, - { + }, { "property": "validateToken", "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$authenticated", "permission": "ALLOW" - } - ] + } + ] } diff --git a/back/models/language.json b/back/models/language.json new file mode 100644 index 000000000..f4e221464 --- /dev/null +++ b/back/models/language.json @@ -0,0 +1,34 @@ +{ + "name": "Language", + "base": "VnModel", + "options": { + "mysql": { + "table": "hedera.language" + } + }, + "properties": { + "code": { + "type": "string", + "id": true + }, + "name": { + "type": "string", + "required": true + }, + "orgName": { + "type": "string", + "required": true + }, + "isActive": { + "type": "boolean" + } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} \ No newline at end of file diff --git a/db/changes/10221-accountModule/00-account.sql b/db/changes/10221-accountModule/00-account.sql new file mode 100644 index 000000000..d26d61c19 --- /dev/null +++ b/db/changes/10221-accountModule/00-account.sql @@ -0,0 +1,127 @@ + +ALTER TABLE `account`.`role` + MODIFY COLUMN `hasLogin` tinyint(3) unsigned DEFAULT 1 NOT NULL; + +ALTER TABLE `account`.`roleInherit` + ADD UNIQUE( `role`, `inheritsFrom`); + +ALTER TABLE `account`.`roleInherit` + DROP PRIMARY KEY; + +ALTER TABLE `account`.`roleInherit` + ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (`id`); + +ALTER TABLE `account`.`mailAlias` + ADD `description` VARCHAR(255) NULL AFTER `alias`; + +ALTER TABLE `account`.`mailAliasAccount` + ADD UNIQUE( `mailAlias`, `account`); + +ALTER TABLE `account`.`mailAliasAccount` + DROP PRIMARY KEY; + +ALTER TABLE `account`.`mailAliasAccount` + ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (`id`); + +ALTER TABLE account.ldapConfig + ADD groupDn varchar(255) NULL; + +UPDATE account.ldapConfig SET groupDn = 'ou=groups,dc=verdnatura,dc=es'; + +DROP PROCEDURE IF EXISTS account.user_syncPassword; + +ALTER TABLE account.`user` + MODIFY COLUMN sync tinyint(4) DEFAULT 0 NOT NULL COMMENT 'Deprecated'; + +CREATE TABLE account.userSync ( + name varchar(30) NOT NULL, + CONSTRAINT userSync_PK PRIMARY KEY (name) +) +ENGINE=InnoDB +DEFAULT CHARSET=utf8 +COLLATE=utf8_general_ci; + +USE account; + +DELIMITER $$ + +DROP TRIGGER IF EXISTS account.user_beforeUpdate$$ +CREATE DEFINER=`root`@`%` TRIGGER `user_beforeUpdate` + BEFORE UPDATE ON `user` FOR EACH ROW +BEGIN + IF !(NEW.`name` <=> OLD.`name`) THEN + CALL user_checkName (NEW.`name`); + END IF; + + IF !(NEW.`password` <=> OLD.`password`) THEN + SET NEW.bcryptPassword = NULL; + SET NEW.lastPassChange = NOW(); + END IF; +END$$ + +DROP TRIGGER IF EXISTS account.user_afterUpdate$$ +CREATE DEFINER=`root`@`%` TRIGGER `user_afterUpdate` + AFTER UPDATE ON `user` FOR EACH ROW +BEGIN + INSERT IGNORE INTO userSync SET `name` = NEW.`name`; + + IF !(OLD.`name` <=> NEW.`name`) THEN + INSERT IGNORE INTO userSync SET `name` = OLD.`name`; + END IF; + + IF !(NEW.`role` <=> OLD.`role`) + THEN + INSERT INTO vn.mail SET + `sender` = 'jgallego@verdnatura.es', + `replyTo` = 'jgallego@verdnatura.es', + `subject` = 'Rol modificado', + `body` = CONCAT(myUserGetName(), ' ha modificado el rol del usuario ', + NEW.`name`, ' de ', OLD.role, ' a ', NEW.role); + END IF; +END$$ + +CREATE DEFINER=`root`@`%` TRIGGER `user_afterInsert` + AFTER INSERT ON `user` FOR EACH ROW +BEGIN + INSERT IGNORE INTO userSync SET `name` = NEW.`name`; +END$$ + +CREATE DEFINER=`root`@`%` TRIGGER `user_afterDelete` + AFTER DELETE ON `user` FOR EACH ROW +BEGIN + INSERT IGNORE INTO userSync SET `name` = OLD.`name`; +END$$ + +DROP TRIGGER IF EXISTS account.account_afterInsert$$ +CREATE DEFINER=`root`@`%` TRIGGER `account_afterInsert` + AFTER INSERT ON `account` FOR EACH ROW +BEGIN + INSERT IGNORE INTO userSync (`name`) + SELECT `name` FROM `user` WHERE id = NEW.id; +END$$ + +DROP TRIGGER IF EXISTS account.account_afterDelete$$ +CREATE DEFINER=`root`@`%` TRIGGER `account_afterDelete` + AFTER DELETE ON `account` FOR EACH ROW +BEGIN + INSERT IGNORE INTO userSync (`name`) + SELECT `name` FROM `user` WHERE id = OLD.id; +END$$ + +CREATE TRIGGER role_beforeInsert + BEFORE INSERT ON `role` FOR EACH ROW +BEGIN + CALL role_checkName(NEW.`name`); +END$$ + +CREATE TRIGGER role_beforeUpdate + BEFORE UPDATE ON `role` FOR EACH ROW +BEGIN + IF !(NEW.`name` <=> OLD.`name`) THEN + CALL role_checkName (NEW.`name`); + END IF; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10221-accountModule/00-myUserChangePassword.sql b/db/changes/10221-accountModule/00-myUserChangePassword.sql new file mode 100644 index 000000000..94fc02087 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserChangePassword.sql @@ -0,0 +1,13 @@ +DROP PROCEDURE IF EXISTS account.myUserChangePassword; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUserChangePassword`(vOldPassword VARCHAR(255), vPassword VARCHAR(255)) +BEGIN +/** + * @deprecated Use myUser_changePassword() + */ + CALL myUser_changePassword(vOldPassword, vPassword); +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUserChangePassword TO account@localhost; diff --git a/db/changes/10221-accountModule/00-myUserCheckLogin.sql b/db/changes/10221-accountModule/00-myUserCheckLogin.sql new file mode 100644 index 000000000..eaa962b63 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserCheckLogin.sql @@ -0,0 +1,15 @@ +DROP FUNCTION IF EXISTS account.myUserCheckLogin; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserCheckLogin`() RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_checkLogin() + */ + RETURN myUser_checkLogin(); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUserCheckLogin TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUserGetId.sql b/db/changes/10221-accountModule/00-myUserGetId.sql new file mode 100644 index 000000000..f0bb972aa --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserGetId.sql @@ -0,0 +1,15 @@ +DROP FUNCTION IF EXISTS account.myUserGetId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserGetId`() RETURNS int(11) + READS SQL DATA + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_getId() + */ + RETURN myUser_getId(); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUserGetId TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUserGetName.sql b/db/changes/10221-accountModule/00-myUserGetName.sql new file mode 100644 index 000000000..2f758d0c6 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserGetName.sql @@ -0,0 +1,15 @@ +DROP FUNCTION IF EXISTS account.myUserGetName; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserGetName`() RETURNS varchar(30) CHARSET utf8 + NO SQL + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_getName() + */ + RETURN myUser_getName(); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUserGetName TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUserHasRole.sql b/db/changes/10221-accountModule/00-myUserHasRole.sql new file mode 100644 index 000000000..6d2301328 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserHasRole.sql @@ -0,0 +1,14 @@ +DROP FUNCTION IF EXISTS account.myUserHasRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserHasRole`(vRoleName VARCHAR(255)) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_hasRole() + */ + RETURN myUser_hasRole(vRoleName); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUserHasRole TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUserHasRoleId.sql b/db/changes/10221-accountModule/00-myUserHasRoleId.sql new file mode 100644 index 000000000..380bd0641 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUserHasRoleId.sql @@ -0,0 +1,14 @@ +DROP FUNCTION IF EXISTS account.myUserHasRoleId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserHasRoleId`(vRoleId INT) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_hasRoleId() + */ + RETURN myUser_hasRoleId(vRoleId); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUserHasRoleId TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_changePassword.sql b/db/changes/10221-accountModule/00-myUser_changePassword.sql new file mode 100644 index 000000000..3dd86a881 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_changePassword.sql @@ -0,0 +1,17 @@ +DROP PROCEDURE IF EXISTS account.myUser_changePassword; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_changePassword`(vOldPassword VARCHAR(255), vPassword VARCHAR(255)) +BEGIN +/** + * Changes the current user password, if user is in recovery mode ignores the + * current password. + * + * @param vOldPassword The current password + * @param vPassword The new password + */ + CALL user_changePassword(myUser_getId(), vOldPassword, vPassword); +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUser_changePassword TO account@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_checkLogin.sql b/db/changes/10221-accountModule/00-myUser_checkLogin.sql new file mode 100644 index 000000000..843f57fff --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_checkLogin.sql @@ -0,0 +1,29 @@ +DROP FUNCTION IF EXISTS account.myUser_checkLogin; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_checkLogin`() RETURNS tinyint(1) + READS SQL DATA + DETERMINISTIC +BEGIN +/** + * Checks that variables @userId and @userName haven't been altered. + * + * @return %TRUE if they are unaltered or unset, otherwise %FALSE + */ + DECLARE vSignature VARCHAR(128); + DECLARE vKey VARCHAR(255); + + IF @userId IS NOT NULL + AND @userName IS NOT NULL + AND @userSignature IS NOT NULL + THEN + SELECT loginKey INTO vKey FROM userConfig; + SET vSignature = util.hmacSha2(256, CONCAT_WS('/', @userId, @userName), vKey); + RETURN vSignature = @userSignature; + END IF; + + RETURN FALSE; +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUser_checkLogin TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_getId.sql b/db/changes/10221-accountModule/00-myUser_getId.sql new file mode 100644 index 000000000..b3d3f1b28 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_getId.sql @@ -0,0 +1,27 @@ +DROP FUNCTION IF EXISTS account.myUser_getId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_getId`() RETURNS int(11) + READS SQL DATA + DETERMINISTIC +BEGIN +/** + * Returns the current user id. + * + * @return The user id + */ + DECLARE vUser INT DEFAULT NULL; + + IF myUser_checkLogin() + THEN + SET vUser = @userId; + ELSE + SELECT id INTO vUser FROM user + WHERE name = LEFT(USER(), INSTR(USER(), '@') - 1); + END IF; + + RETURN vUser; +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUser_getId TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_getName.sql b/db/changes/10221-accountModule/00-myUser_getName.sql new file mode 100644 index 000000000..b055227d3 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_getName.sql @@ -0,0 +1,27 @@ +DROP FUNCTION IF EXISTS account.myUser_getName; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_getName`() RETURNS varchar(30) CHARSET utf8 + NO SQL + DETERMINISTIC +BEGIN +/** + * Returns the current user name. + * + * @return The user name + */ + DECLARE vUser VARCHAR(30) DEFAULT NULL; + + IF myUser_checkLogin() + THEN + SET vUser = @userName; + ELSE + SET vUser = LEFT(USER(), INSTR(USER(), '@') - 1); + END IF; + + RETURN vUser; +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUser_getName TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_hasRole.sql b/db/changes/10221-accountModule/00-myUser_hasRole.sql new file mode 100644 index 000000000..538b58f08 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_hasRole.sql @@ -0,0 +1,17 @@ +DROP FUNCTION IF EXISTS account.myUser_hasRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_hasRole`(vRoleName VARCHAR(255)) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * Checks if current user has/inherits a role. + * + * @param vRoleName Role to check + * @return %TRUE if it has role, %FALSE otherwise + */ + RETURN user_hasRole(myUser_getName(), vRoleName); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUser_hasRole TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_hasRoleId.sql b/db/changes/10221-accountModule/00-myUser_hasRoleId.sql new file mode 100644 index 000000000..2931443e1 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_hasRoleId.sql @@ -0,0 +1,17 @@ +DROP FUNCTION IF EXISTS account.myUser_hasRoleId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_hasRoleId`(vRoleId INT) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * Checks if current user has/inherits a role. + * + * @param vRoleName Role id to check + * @return %TRUE if it has role, %FALSE otherwise + */ + RETURN user_hasRoleId(myUserGetName(), vRoleId); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.myUser_hasRoleId TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_login.sql b/db/changes/10221-accountModule/00-myUser_login.sql new file mode 100644 index 000000000..9d92828b0 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_login.sql @@ -0,0 +1,29 @@ +DROP PROCEDURE IF EXISTS account.myUser_login; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_login`(vUserName VARCHAR(255), vPassword VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * Logs in using the user credentials. + * + * @param vUserName The user name + * @param vPassword The user password + */ + DECLARE vAuthIsOk BOOLEAN DEFAULT FALSE; + + SELECT COUNT(*) = 1 INTO vAuthIsOk FROM user + WHERE name = vUserName + AND password = MD5(vPassword) + AND active; + + IF vAuthIsOk + THEN + CALL myUser_loginWithName (vUserName); + ELSE + CALL util.throw ('INVALID_CREDENTIALS'); + END IF; +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUser_login TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_loginWithKey.sql b/db/changes/10221-accountModule/00-myUser_loginWithKey.sql new file mode 100644 index 000000000..fc12a79d9 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_loginWithKey.sql @@ -0,0 +1,25 @@ +DROP PROCEDURE IF EXISTS account.myUser_loginWithKey; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_loginWithKey`(vUserName VARCHAR(255), vKey VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * Logs in using the user name and MySQL master key. + * + * @param vUserName The user name + * @param vKey The MySQL master key + */ + DECLARE vLoginKey VARCHAR(255); + + SELECT loginKey INTO vLoginKey FROM userConfig; + + IF vLoginKey = vKey THEN + CALL user_loginWithName(vUserName); + ELSE + CALL util.throw('INVALID_KEY'); + END IF; +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUser_loginWithKey TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-myUser_loginWithName.sql b/db/changes/10221-accountModule/00-myUser_loginWithName.sql new file mode 100644 index 000000000..6b86a37f3 --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_loginWithName.sql @@ -0,0 +1,26 @@ +DROP PROCEDURE IF EXISTS account.myUser_loginWithName; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_loginWithName`(vUserName VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * Logs in using only the user name. This procedure is intended to be executed + * by users with a high level of privileges so that normal users should not have + * execute permissions on it. + * + * @param vUserName The user name + */ + DECLARE vUserId INT DEFAULT NULL; + DECLARE vKey VARCHAR(255); + + SELECT id INTO vUserId FROM user + WHERE name = vUserName; + + SELECT loginKey INTO vKey FROM userConfig; + + SET @userId = vUserId; + SET @userName = vUserName; + SET @userSignature = util.hmacSha2(256, CONCAT_WS('/', vUserId, vUserName), vKey); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-myUser_logout.sql b/db/changes/10221-accountModule/00-myUser_logout.sql new file mode 100644 index 000000000..ffa2c969e --- /dev/null +++ b/db/changes/10221-accountModule/00-myUser_logout.sql @@ -0,0 +1,15 @@ +DROP PROCEDURE IF EXISTS account.myUser_logout; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_logout`() +BEGIN +/** + * Logouts the user. + */ + SET @userId = NULL; + SET @userName = NULL; + SET @userSignature = NULL; +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUser_logout TO account@localhost; diff --git a/db/changes/10221-accountModule/00-passwordGenerate.sql b/db/changes/10221-accountModule/00-passwordGenerate.sql new file mode 100644 index 000000000..46048e24d --- /dev/null +++ b/db/changes/10221-accountModule/00-passwordGenerate.sql @@ -0,0 +1,52 @@ +DROP FUNCTION IF EXISTS account.passwordGenerate; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`passwordGenerate`() RETURNS text CHARSET utf8 +BEGIN +/** + * Generates a random password that meets the minimum requirements. + * + * @return Generated password + */ + DECLARE vMinLength TINYINT; + DECLARE vMinAlpha TINYINT; + DECLARE vMinUpper TINYINT; + DECLARE vMinDigits TINYINT; + DECLARE vMinPunct TINYINT; + DECLARE vAlpha TINYINT DEFAULT 0; + DECLARE vUpper TINYINT DEFAULT 0; + DECLARE vDigits TINYINT DEFAULT 0; + DECLARE vPunct TINYINT DEFAULT 0; + DECLARE vRandIndex INT; + DECLARE vPwd TEXT DEFAULT ''; + + DECLARE vAlphaChars TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyz'; + DECLARE vUpperChars TEXT DEFAULT 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + DECLARE vDigitChars TEXT DEFAULT '1234567890'; + DECLARE vPunctChars TEXT DEFAULT '!$%&()=.'; + + SELECT length, nAlpha, nUpper, nDigits, nPunct + INTO vMinLength, vMinAlpha, vMinUpper, vMinDigits, vMinPunct FROM userPassword; + + WHILE LENGTH(vPwd) < vMinLength OR vAlpha < vMinAlpha + OR vUpper < vMinUpper OR vDigits < vMinDigits OR vPunct < vMinPunct DO + SET vRandIndex = FLOOR((RAND() * 4) + 1); + + CASE + WHEN vRandIndex = 1 THEN + SET vPwd = CONCAT(vPwd, SUBSTRING(vAlphaChars, FLOOR((RAND() * 26) + 1), 1)); + SET vAlpha = vAlpha + 1; + WHEN vRandIndex = 2 THEN + SET vPwd = CONCAT(vPwd, SUBSTRING(vUpperChars, FLOOR((RAND() * 26) + 1), 1)); + SET vUpper = vUpper + 1; + WHEN vRandIndex = 3 THEN + SET vPwd = CONCAT(vPwd, SUBSTRING(vDigitChars, FLOOR((RAND() * 10) + 1), 1)); + SET vDigits = vDigits + 1; + WHEN vRandIndex = 4 THEN + SET vPwd = CONCAT(vPwd, SUBSTRING(vPunctChars, FLOOR((RAND() * LENGTH(vPunctChars)) + 1), 1)); + SET vPunct = vPunct + 1; + END CASE; + END WHILE; + RETURN vPwd; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-role_checkName.sql b/db/changes/10221-accountModule/00-role_checkName.sql new file mode 100644 index 000000000..1e4f31767 --- /dev/null +++ b/db/changes/10221-accountModule/00-role_checkName.sql @@ -0,0 +1,18 @@ +DROP PROCEDURE IF EXISTS account.role_checkName; + +DELIMITER $$ +CREATE PROCEDURE account.role_checkName(vRoleName VARCHAR(255)) +BEGIN +/** + * Checks that role name meets the necessary syntax requirements, otherwise it + * throws an exception. + * Role name must be written in camelCase. + * + * @param vRoleName The role name + */ + IF BINARY vRoleName NOT REGEXP '^[a-z][a-zA-Z]+$' THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'Role name must be written in camelCase'; + END IF; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-role_getDescendents.sql b/db/changes/10221-accountModule/00-role_getDescendents.sql new file mode 100644 index 000000000..9b224f6eb --- /dev/null +++ b/db/changes/10221-accountModule/00-role_getDescendents.sql @@ -0,0 +1,66 @@ +DROP PROCEDURE IF EXISTS account.role_getDescendents; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_getDescendents`(vSelf INT) +BEGIN +/** + * Gets the identifiers of all the subroles implemented by a role (Including + * itself). + * + * @param vSelf The role identifier + * @table tmp.role Subroles implemented by the role + */ + DECLARE vIsRoot BOOL; + + DROP TEMPORARY TABLE IF EXISTS + tmp.role, parents, childs; + + CREATE TEMPORARY TABLE tmp.role + (UNIQUE (id)) + ENGINE = MEMORY + SELECT vSelf AS id; + + CREATE TEMPORARY TABLE parents + ENGINE = MEMORY + SELECT vSelf AS id; + + CREATE TEMPORARY TABLE childs + LIKE parents; + + REPEAT + DELETE FROM childs; + INSERT INTO childs + SELECT DISTINCT r.inheritsFrom id + FROM parents p + JOIN roleInherit r ON r.role = p.id + LEFT JOIN tmp.role t ON t.id = r.inheritsFrom + WHERE t.id IS NULL; + + DELETE FROM parents; + INSERT INTO parents + SELECT * FROM childs; + + INSERT INTO tmp.role + SELECT * FROM childs; + + UNTIL ROW_COUNT() <= 0 + END REPEAT; + + -- If it is root all the roles are added + + SELECT COUNT(*) > 0 INTO vIsRoot + FROM tmp.role t + JOIN role r ON r.id = t.id + WHERE r.`name` = 'root'; + + IF vIsRoot THEN + INSERT IGNORE INTO tmp.role (id) + SELECT id FROM role; + END IF; + + -- Cleaning + + DROP TEMPORARY TABLE + parents, childs; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-role_sync.sql b/db/changes/10221-accountModule/00-role_sync.sql new file mode 100644 index 000000000..8e16ef567 --- /dev/null +++ b/db/changes/10221-accountModule/00-role_sync.sql @@ -0,0 +1,53 @@ +DROP PROCEDURE IF EXISTS account.role_sync; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_sync`() +BEGIN +/** + * Synchronize the @roleRole table with the current role hierarchy. This + * procedure must be called every time the @roleInherit table is modified so + * that the changes made on it are effective. + */ + DECLARE vRoleId INT; + DECLARE vDone BOOL; + + DECLARE cur CURSOR FOR + SELECT id FROM role; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + + DROP TEMPORARY TABLE IF EXISTS tRoleRole; + CREATE TEMPORARY TABLE tRoleRole + ENGINE = MEMORY + SELECT * FROM roleRole LIMIT 0; + + OPEN cur; + + l: LOOP + SET vDone = FALSE; + FETCH cur INTO vRoleId; + + IF vDone THEN + LEAVE l; + END IF; + + CALL role_getDescendents(vRoleId); + + INSERT INTO tRoleRole (role, inheritsFrom) + SELECT vRoleId, id FROM tmp.role; + + DROP TEMPORARY TABLE tmp.role; + END LOOP; + + CLOSE cur; + + START TRANSACTION; + DELETE FROM roleRole; + INSERT INTO roleRole SELECT * FROM tRoleRole; + COMMIT; + + DROP TEMPORARY TABLE tRoleRole; + + CALL role_syncPrivileges; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-role_syncPrivileges.sql b/db/changes/10221-accountModule/00-role_syncPrivileges.sql new file mode 100644 index 000000000..0d6d8975b --- /dev/null +++ b/db/changes/10221-accountModule/00-role_syncPrivileges.sql @@ -0,0 +1,494 @@ +DROP PROCEDURE IF EXISTS account.role_syncPrivileges; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_syncPrivileges`() +BEGIN +/** + * Synchronizes permissions of MySQL role users based on role hierarchy. + * The computed role users of permission mix will be named according to + * pattern z-[role_name]. + * + * If any@localhost user exists, it will be taken as a template for basic + * attributes. + * + * Warning! This procedure should only be called when MySQL privileges + * are modified. If role hierarchy is modified, you must call the role_sync() + * procedure wich calls this internally. + */ + DECLARE vIsMysql BOOL DEFAULT VERSION() NOT LIKE '%MariaDB%'; + DECLARE vVersion INT DEFAULT SUBSTRING_INDEX(VERSION(), '.', 1); + DECLARE vTplUser VARCHAR(255) DEFAULT 'any'; + DECLARE vTplHost VARCHAR(255) DEFAULT '%'; + DECLARE vRoleHost VARCHAR(255) DEFAULT 'localhost'; + DECLARE vAllHost VARCHAR(255) DEFAULT '%'; + DECLARE vPrefix VARCHAR(2) DEFAULT 'z-'; + DECLARE vPrefixedLike VARCHAR(255); + DECLARE vPassword VARCHAR(255) DEFAULT ''; + + -- Deletes computed role users + + SET vPrefixedLike = CONCAT(vPrefix, '%'); + + DELETE FROM mysql.user + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.db + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.tables_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.columns_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.procs_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.proxies_priv + WHERE `Proxied_user` LIKE vPrefixedLike; + + -- Temporary tables + + DROP TEMPORARY TABLE IF EXISTS tRole; + CREATE TEMPORARY TABLE tRole + (INDEX (id)) + ENGINE = MEMORY + SELECT + id, + `name` role, + CONCAT(vPrefix, `name`) prefixedRole + FROM role + WHERE hasLogin; + + DROP TEMPORARY TABLE IF EXISTS tRoleInherit; + CREATE TEMPORARY TABLE tRoleInherit + (INDEX (inheritsFrom)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + ri.`name` inheritsFrom + FROM tRole r + JOIN roleRole rr ON rr.role = r.id + JOIN role ri ON ri.id = rr.inheritsFrom; + + -- Recreate role users + + IF vIsMysql THEN + DROP TEMPORARY TABLE IF EXISTS tUser; + CREATE TEMPORARY TABLE tUser + SELECT + r.prefixedRole `User`, + vTplHost `Host`, + IFNULL(t.`authentication_string`, + '') `authentication_string`, + IFNULL(t.`plugin`, + 'mysql_native_password') `plugin`, + IFNULL(IF('' != u.`ssl_type`, + u.`ssl_type`, t.`ssl_type`), + '') `ssl_type`, + IFNULL(IF('' != u.`ssl_cipher`, + u.`ssl_cipher`, t.`ssl_cipher`), + '') `ssl_cipher`, + IFNULL(IF('' != u.`x509_issuer`, + u.`x509_issuer`, t.`x509_issuer`), + '') `x509_issuer`, + IFNULL(IF('' != u.`x509_subject`, + u.`x509_subject`, t.`x509_subject`), + '') `x509_subject`, + IFNULL(IF(0 != u.`max_questions`, + u.`max_questions`, t.`max_questions`), + 0) `max_questions`, + IFNULL(IF(0 != u.`max_updates`, + u.`max_updates`, t.`max_updates`), + 0) `max_updates`, + IFNULL(IF(0 != u.`max_connections`, + u.`max_connections`, t.`max_connections`), + 0) `max_connections`, + IFNULL(IF(0 != u.`max_user_connections`, + u.`max_user_connections`, t.`max_user_connections`), + 0) `max_user_connections` + FROM tRole r + LEFT JOIN mysql.user t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.user u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + + IF vVersion <= 5 THEN + SELECT `Password` INTO vPassword + FROM mysql.user + WHERE `User` = vTplUser + AND `Host` = vRoleHost; + + INSERT INTO mysql.user ( + `User`, + `Host`, + `Password`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + vPassword, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + ELSE + INSERT INTO mysql.user ( + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + END IF; + + DROP TEMPORARY TABLE IF EXISTS tUser; + ELSE + INSERT INTO mysql.global_priv ( + `User`, + `Host`, + `Priv` + ) + SELECT + r.prefixedRole, + vTplHost, + JSON_MERGE_PATCH( + IFNULL(t.`Priv`, '{}'), + IFNULL(u.`Priv`, '{}') + ) + FROM tRole r + LEFT JOIN mysql.global_priv t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.global_priv u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + END IF; + + INSERT INTO mysql.proxies_priv ( + `User`, + `Host`, + `Proxied_user`, + `Proxied_host`, + `Grantor` + ) + SELECT + '', + vAllHost, + prefixedRole, + vTplHost, + CONCAT(prefixedRole, '@', vTplHost) + FROM tRole; + + -- Copies global privileges + + DROP TEMPORARY TABLE IF EXISTS tUserPriv; + + IF vIsMysql THEN + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + MAX(u.`Select_priv`) `Select_priv`, + MAX(u.`Insert_priv`) `Insert_priv`, + MAX(u.`Update_priv`) `Update_priv`, + MAX(u.`Delete_priv`) `Delete_priv`, + MAX(u.`Create_priv`) `Create_priv`, + MAX(u.`Drop_priv`) `Drop_priv`, + MAX(u.`Reload_priv`) `Reload_priv`, + MAX(u.`Shutdown_priv`) `Shutdown_priv`, + MAX(u.`Process_priv`) `Process_priv`, + MAX(u.`File_priv`) `File_priv`, + MAX(u.`Grant_priv`) `Grant_priv`, + MAX(u.`References_priv`) `References_priv`, + MAX(u.`Index_priv`) `Index_priv`, + MAX(u.`Alter_priv`) `Alter_priv`, + MAX(u.`Show_db_priv`) `Show_db_priv`, + MAX(u.`Super_priv`) `Super_priv`, + MAX(u.`Create_tmp_table_priv`) `Create_tmp_table_priv`, + MAX(u.`Lock_tables_priv`) `Lock_tables_priv`, + MAX(u.`Execute_priv`) `Execute_priv`, + MAX(u.`Repl_slave_priv`) `Repl_slave_priv`, + MAX(u.`Repl_client_priv`) `Repl_client_priv`, + MAX(u.`Create_view_priv`) `Create_view_priv`, + MAX(u.`Show_view_priv`) `Show_view_priv`, + MAX(u.`Create_routine_priv`) `Create_routine_priv`, + MAX(u.`Alter_routine_priv`) `Alter_routine_priv`, + MAX(u.`Create_user_priv`) `Create_user_priv`, + MAX(u.`Event_priv`) `Event_priv`, + MAX(u.`Trigger_priv`) `Trigger_priv`, + MAX(u.`Create_tablespace_priv`) `Create_tablespace_priv` + FROM tRoleInherit r + JOIN mysql.user u + ON u.`User` = r.inheritsFrom + AND u.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.user u + JOIN tUserPriv t + ON u.`User` = t.prefixedRole + AND u.`Host` = vTplHost + SET + u.`Select_priv` + = t.`Select_priv`, + u.`Insert_priv` + = t.`Insert_priv`, + u.`Update_priv` + = t.`Update_priv`, + u.`Delete_priv` + = t.`Delete_priv`, + u.`Create_priv` + = t.`Create_priv`, + u.`Drop_priv` + = t.`Drop_priv`, + u.`Reload_priv` + = t.`Reload_priv`, + u.`Shutdown_priv` + = t.`Shutdown_priv`, + u.`Process_priv` + = t.`Process_priv`, + u.`File_priv` + = t.`File_priv`, + u.`Grant_priv` + = t.`Grant_priv`, + u.`References_priv` + = t.`References_priv`, + u.`Index_priv` + = t.`Index_priv`, + u.`Alter_priv` + = t.`Alter_priv`, + u.`Show_db_priv` + = t.`Show_db_priv`, + u.`Super_priv` + = t.`Super_priv`, + u.`Create_tmp_table_priv` + = t.`Create_tmp_table_priv`, + u.`Lock_tables_priv` + = t.`Lock_tables_priv`, + u.`Execute_priv` + = t.`Execute_priv`, + u.`Repl_slave_priv` + = t.`Repl_slave_priv`, + u.`Repl_client_priv` + = t.`Repl_client_priv`, + u.`Create_view_priv` + = t.`Create_view_priv`, + u.`Show_view_priv` + = t.`Show_view_priv`, + u.`Create_routine_priv` + = t.`Create_routine_priv`, + u.`Alter_routine_priv` + = t.`Alter_routine_priv`, + u.`Create_user_priv` + = t.`Create_user_priv`, + u.`Event_priv` + = t.`Event_priv`, + u.`Trigger_priv` + = t.`Trigger_priv`, + u.`Create_tablespace_priv` + = t.`Create_tablespace_priv`; + ELSE + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + SELECT + r.prefixedRole, + BIT_OR(JSON_VALUE(p.`Priv`, '$.access')) access + FROM tRoleInherit r + JOIN mysql.global_priv p + ON p.`User` = r.inheritsFrom + AND p.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.global_priv p + JOIN tUserPriv t + ON p.`User` = t.prefixedRole + AND p.`Host` = vTplHost + SET + p.`Priv` = JSON_SET(p.`Priv`, '$.access', t.access); + END IF; + + DROP TEMPORARY TABLE tUserPriv; + + -- Copy schema level privileges + + INSERT INTO mysql.db ( + `User`, + `Host`, + `Db`, + `Select_priv`, + `Insert_priv`, + `Update_priv`, + `Delete_priv`, + `Create_priv`, + `Drop_priv`, + `Grant_priv`, + `References_priv`, + `Index_priv`, + `Alter_priv`, + `Create_tmp_table_priv`, + `Lock_tables_priv`, + `Create_view_priv`, + `Show_view_priv`, + `Create_routine_priv`, + `Alter_routine_priv`, + `Execute_priv`, + `Event_priv`, + `Trigger_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + MAX(t.`Select_priv`), + MAX(t.`Insert_priv`), + MAX(t.`Update_priv`), + MAX(t.`Delete_priv`), + MAX(t.`Create_priv`), + MAX(t.`Drop_priv`), + MAX(t.`Grant_priv`), + MAX(t.`References_priv`), + MAX(t.`Index_priv`), + MAX(t.`Alter_priv`), + MAX(t.`Create_tmp_table_priv`), + MAX(t.`Lock_tables_priv`), + MAX(t.`Create_view_priv`), + MAX(t.`Show_view_priv`), + MAX(t.`Create_routine_priv`), + MAX(t.`Alter_routine_priv`), + MAX(t.`Execute_priv`), + MAX(t.`Event_priv`), + MAX(t.`Trigger_priv`) + FROM tRoleInherit r + JOIN mysql.db t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`; + + -- Copy table level privileges + + INSERT INTO mysql.tables_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Grantor`, + `Timestamp`, + `Table_priv`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Grantor`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Table_priv`, '')), ''), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.tables_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`; + + -- Copy column level privileges + + INSERT INTO mysql.columns_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Column_name`, + `Timestamp`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Column_name`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.columns_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`, t.`Column_name`; + + -- Copy routine privileges + + INSERT IGNORE INTO mysql.procs_priv ( + `User`, + `Host`, + `Db`, + `Routine_name`, + `Routine_type`, + `Grantor`, + `Timestamp`, + `Proc_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Routine_name`, + t.`Routine_type`, + t.`Grantor`, + t.`Timestamp`, + t.`Proc_priv` + FROM tRoleInherit r + JOIN mysql.procs_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost; + + -- Free memory + + DROP TEMPORARY TABLE + tRole, + tRoleInherit; + + FLUSH PRIVILEGES; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-userGetId.sql b/db/changes/10221-accountModule/00-userGetId.sql new file mode 100644 index 000000000..219ea680c --- /dev/null +++ b/db/changes/10221-accountModule/00-userGetId.sql @@ -0,0 +1,15 @@ +DROP FUNCTION IF EXISTS account.userGetId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetId`() RETURNS int(11) + READS SQL DATA + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_getId() + */ + RETURN myUser_getId(); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.userGetId TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-userGetMysqlRole.sql b/db/changes/10221-accountModule/00-userGetMysqlRole.sql new file mode 100644 index 000000000..673f1aac9 --- /dev/null +++ b/db/changes/10221-accountModule/00-userGetMysqlRole.sql @@ -0,0 +1,11 @@ +DROP FUNCTION IF EXISTS account.userGetMysqlRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetMysqlRole`(vUserName VARCHAR(255)) RETURNS varchar(255) CHARSET utf8 +BEGIN +/** + * @deprecated Use user_getMysqlRole() + */ + RETURN user_getMysqlRole(); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-userGetName.sql b/db/changes/10221-accountModule/00-userGetName.sql new file mode 100644 index 000000000..f49f2dbef --- /dev/null +++ b/db/changes/10221-accountModule/00-userGetName.sql @@ -0,0 +1,15 @@ +DROP FUNCTION IF EXISTS account.userGetName; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetName`() RETURNS varchar(30) CHARSET utf8 + NO SQL + DETERMINISTIC +BEGIN +/** + * @deprecated Use myUser_getName() + */ + RETURN myUser_getName(); +END$$ +DELIMITER ; + +GRANT EXECUTE ON FUNCTION account.userGetName TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-userGetNameFromId.sql b/db/changes/10221-accountModule/00-userGetNameFromId.sql new file mode 100644 index 000000000..f8e9333cb --- /dev/null +++ b/db/changes/10221-accountModule/00-userGetNameFromId.sql @@ -0,0 +1,11 @@ +DROP FUNCTION IF EXISTS account.userGetNameFromId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetNameFromId`(vSelf INT) RETURNS varchar(30) CHARSET utf8 +BEGIN +/** + * @deprecated Use user_getNameFromId(); + */ + RETURN user_getNameFromId(vSelf); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-userHasRole.sql b/db/changes/10221-accountModule/00-userHasRole.sql new file mode 100644 index 000000000..3e09d27bf --- /dev/null +++ b/db/changes/10221-accountModule/00-userHasRole.sql @@ -0,0 +1,12 @@ +DROP FUNCTION IF EXISTS account.userHasRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userHasRole`(vUserName VARCHAR(255), vRoleName VARCHAR(255)) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * @deprecated Use user_hasRole() + */ + RETURN user_hasRole(vUserName, vRoleName); +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10221-accountModule/00-userHasRoleId.sql b/db/changes/10221-accountModule/00-userHasRoleId.sql new file mode 100644 index 000000000..9fcd9f073 --- /dev/null +++ b/db/changes/10221-accountModule/00-userHasRoleId.sql @@ -0,0 +1,12 @@ +DROP FUNCTION IF EXISTS account.userHasRoleId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`userHasRoleId`(vUser VARCHAR(255), vRoleId INT) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * @deprecated Use user_hasRoleId() + */ + RETURN user_hasRoleId(vUser, vRoleId); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-userLogin.sql b/db/changes/10221-accountModule/00-userLogin.sql new file mode 100644 index 000000000..63a332254 --- /dev/null +++ b/db/changes/10221-accountModule/00-userLogin.sql @@ -0,0 +1,14 @@ +DROP PROCEDURE IF EXISTS account.userLogin; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLogin`(vUserName VARCHAR(255), vPassword VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * @deprecated Use myUser_login() + */ + CALL myUser_login(vUserName, vPassword); +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.userLogin TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-userLoginWithKey.sql b/db/changes/10221-accountModule/00-userLoginWithKey.sql new file mode 100644 index 000000000..45a490c71 --- /dev/null +++ b/db/changes/10221-accountModule/00-userLoginWithKey.sql @@ -0,0 +1,14 @@ +DROP PROCEDURE IF EXISTS account.userLoginWithKey; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLoginWithKey`(vUserName VARCHAR(255), vKey VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * @deprecated Use myUser_loginWithKey() + */ + CALL myUser_loginWithKey(vUserName, vKey); +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.userLoginWithKey TO guest@localhost; diff --git a/db/changes/10221-accountModule/00-userLoginWithName.sql b/db/changes/10221-accountModule/00-userLoginWithName.sql new file mode 100644 index 000000000..4053970e4 --- /dev/null +++ b/db/changes/10221-accountModule/00-userLoginWithName.sql @@ -0,0 +1,12 @@ +DROP PROCEDURE IF EXISTS account.userLoginWithName; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLoginWithName`(vUserName VARCHAR(255)) + READS SQL DATA +BEGIN +/** + * @deprecated Use myUser_loginWithName() + */ + CALL myUser_loginWithName(vUserName); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-userLogout.sql b/db/changes/10221-accountModule/00-userLogout.sql new file mode 100644 index 000000000..7d0d68324 --- /dev/null +++ b/db/changes/10221-accountModule/00-userLogout.sql @@ -0,0 +1,14 @@ +DROP PROCEDURE IF EXISTS account.myUserLogout; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUserLogout`() +BEGIN +/** + * @deprecated Use myUser_Logout() + */ + CALL myUser_logout; +END$$ +DELIMITER ; + +GRANT EXECUTE ON PROCEDURE account.myUserLogout TO account@localhost; diff --git a/db/changes/10221-accountModule/00-userSetPassword.sql b/db/changes/10221-accountModule/00-userSetPassword.sql new file mode 100644 index 000000000..fd3daec53 --- /dev/null +++ b/db/changes/10221-accountModule/00-userSetPassword.sql @@ -0,0 +1,16 @@ +DROP PROCEDURE IF EXISTS account.userSetPassword; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userSetPassword`(vUserName VARCHAR(255), vPassword VARCHAR(255)) +BEGIN +/** + * @deprecated Use user_setPassword() + */ + DECLARE vUserId INT; + + SELECT id INTO vUserId + FROM user WHERE `name` = vUserName; + + CALL user_setPassword(vUserId, vPassword); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_changePassword.sql b/db/changes/10221-accountModule/00-user_changePassword.sql new file mode 100644 index 000000000..c137213e0 --- /dev/null +++ b/db/changes/10221-accountModule/00-user_changePassword.sql @@ -0,0 +1,27 @@ +DROP PROCEDURE IF EXISTS account.user_changePassword; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE account.user_changePassword(vSelf INT, vOldPassword VARCHAR(255), vPassword VARCHAR(255)) +BEGIN +/** + * Changes the user password. + * + * @param vSelf The user id + * @param vOldPassword The current password + * @param vPassword The new password + */ + DECLARE vPasswordOk BOOL; + DECLARE vUserName VARCHAR(255); + + SELECT `password` = MD5(vOldPassword), `name` + INTO vPasswordOk, vUserName + FROM user WHERE id = vSelf; + + IF NOT vPasswordOk THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'Invalid password'; + END IF; + + CALL user_setPassword(vSelf, vPassword); +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_checkName.sql b/db/changes/10221-accountModule/00-user_checkName.sql new file mode 100644 index 000000000..9b54d6175 --- /dev/null +++ b/db/changes/10221-accountModule/00-user_checkName.sql @@ -0,0 +1,17 @@ +DROP PROCEDURE IF EXISTS account.user_checkName; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`user_checkName`(vUserName VARCHAR(255)) +BEGIN +/** + * Checks that username meets the necessary syntax requirements, otherwise it + * throws an exception. + * The user name must only contain lowercase letters or, starting with second + * character, numbers or underscores. + */ + IF vUserName NOT REGEXP '^[a-z0-9_-]*$' THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'INVALID_USER_NAME'; + END IF; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_getMysqlRole.sql b/db/changes/10221-accountModule/00-user_getMysqlRole.sql new file mode 100644 index 000000000..4088ea8a4 --- /dev/null +++ b/db/changes/10221-accountModule/00-user_getMysqlRole.sql @@ -0,0 +1,22 @@ +DROP FUNCTION IF EXISTS account.user_getMysqlRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_getMysqlRole`(vUserName VARCHAR(255)) RETURNS varchar(255) CHARSET utf8 +BEGIN +/** + * From a username, it returns the associated MySQL wich should be used when + * using external authentication systems. + * + * @param vUserName The user name + * @return The associated MySQL role + */ + DECLARE vRole VARCHAR(255); + + SELECT CONCAT(IF(r.hasLogin, 'z-', ''), r.name) INTO vRole + FROM role r + JOIN user u ON u.role = r.id + WHERE u.name = vUserName; + + RETURN vRole; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_getNameFromId.sql b/db/changes/10221-accountModule/00-user_getNameFromId.sql new file mode 100644 index 000000000..ae9ae5941 --- /dev/null +++ b/db/changes/10221-accountModule/00-user_getNameFromId.sql @@ -0,0 +1,20 @@ +DROP FUNCTION IF EXISTS account.user_getNameFromId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_getNameFromId`(vSelf INT) RETURNS varchar(30) CHARSET utf8 +BEGIN +/** + * Gets user name from it's id. + * + * @param vSelf The user id + * @return The user name + */ + DECLARE vName VARCHAR(30); + + SELECT `name` INTO vName + FROM user + WHERE id = vId; + + RETURN vSelf; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_hasRole.sql b/db/changes/10221-accountModule/00-user_hasRole.sql new file mode 100644 index 000000000..d42c81deb --- /dev/null +++ b/db/changes/10221-accountModule/00-user_hasRole.sql @@ -0,0 +1,25 @@ +DROP FUNCTION IF EXISTS account.user_hasRole; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_hasRole`(vUserName VARCHAR(255), vRoleName VARCHAR(255)) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * Checks if user has/inherits a role. + * + * @param vUserName The user name + * @param vRoleName Role to check + * @return %TRUE if it has role, %FALSE otherwise + */ + DECLARE vHasRole BOOL DEFAULT FALSE; + + SELECT COUNT(*) > 0 INTO vHasRole + FROM user u + JOIN roleRole rr ON rr.role = u.role + JOIN role r ON r.id = rr.inheritsFrom + WHERE u.`name` = vUserName + AND r.`name` = vRoleName COLLATE 'utf8_unicode_ci'; + + RETURN vHasRole; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_hasRoleId.sql b/db/changes/10221-accountModule/00-user_hasRoleId.sql new file mode 100644 index 000000000..b2f523e8c --- /dev/null +++ b/db/changes/10221-accountModule/00-user_hasRoleId.sql @@ -0,0 +1,25 @@ +DROP FUNCTION IF EXISTS account.user_hasRoleId; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_hasRoleId`(vUser VARCHAR(255), vRoleId INT) RETURNS tinyint(1) + DETERMINISTIC +BEGIN +/** + * Checks if user has/inherits a role. + * + * @param vUserName The user name + * @param vRoleId Role id to check + * @return %TRUE if it has role, %FALSE otherwise + */ + DECLARE vHasRole BOOL DEFAULT FALSE; + + SELECT COUNT(*) > 0 INTO vHasRole + FROM user u + JOIN roleRole rr ON rr.role = u.role + JOIN role r ON r.id = rr.inheritsFrom + WHERE u.`name` = vUser + AND r.id = vRoleId; + + RETURN vHasRole; +END$$ +DELIMITER ; diff --git a/db/changes/10221-accountModule/00-user_setPassword.sql b/db/changes/10221-accountModule/00-user_setPassword.sql new file mode 100644 index 000000000..3dcbb1653 --- /dev/null +++ b/db/changes/10221-accountModule/00-user_setPassword.sql @@ -0,0 +1,21 @@ +DROP PROCEDURE IF EXISTS account.user_setPassword; + +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE account.user_setPassword(vSelf INT, vPassword VARCHAR(255)) +BEGIN +/** + * Change the password of the passed as a parameter. Only administrators should + * have execute privileges on the procedure since it does not request the user's + * current password. + * + * @param vSelf The user id + * @param vPassword New password + */ + CALL user_checkPassword(vPassword); + + UPDATE user SET + `password` = MD5(vPassword), + `recoverPass` = FALSE + WHERE id = vSelf; +END$$ +DELIMITER ; diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql index 9c05923cf..374c1ed50 100644 --- a/db/dump/dumpedFixtures.sql +++ b/db/dump/dumpedFixtures.sql @@ -84,6 +84,36 @@ LOCK TABLES `roleRole` WRITE; INSERT INTO `roleRole` VALUES (0,0),(0,1),(0,2),(0,3),(0,5),(0,6),(0,9),(0,11),(0,13),(0,15),(0,16),(0,17),(0,18),(0,19),(0,20),(0,21),(0,22),(0,30),(0,31),(0,32),(0,33),(0,34),(0,35),(0,36),(0,37),(0,38),(0,39),(0,40),(0,41),(0,42),(0,43),(0,44),(0,45),(0,47),(0,48),(0,49),(0,50),(0,51),(0,52),(0,53),(0,54),(0,55),(0,56),(0,57),(0,58),(0,59),(0,60),(0,61),(0,62),(0,64),(0,65),(0,66),(0,67),(0,69),(0,70),(1,1),(1,2),(1,3),(1,6),(1,11),(1,70),(2,2),(2,6),(2,11),(3,3),(3,6),(3,11),(5,1),(5,2),(5,3),(5,5),(5,6),(5,11),(5,13),(5,18),(5,21),(5,33),(5,53),(5,70),(6,6),(9,0),(9,1),(9,2),(9,3),(9,5),(9,6),(9,9),(9,11),(9,13),(9,15),(9,16),(9,17),(9,18),(9,19),(9,20),(9,21),(9,22),(9,30),(9,31),(9,32),(9,33),(9,34),(9,35),(9,36),(9,37),(9,38),(9,39),(9,40),(9,41),(9,42),(9,43),(9,44),(9,45),(9,47),(9,48),(9,49),(9,50),(9,51),(9,52),(9,53),(9,54),(9,55),(9,56),(9,57),(9,58),(9,59),(9,60),(9,61),(9,62),(9,64),(9,65),(9,66),(9,67),(9,69),(9,70),(11,6),(11,11),(13,1),(13,2),(13,3),(13,6),(13,11),(13,13),(13,70),(15,1),(15,2),(15,3),(15,6),(15,11),(15,13),(15,15),(15,35),(15,56),(15,57),(15,70),(16,1),(16,2),(16,3),(16,6),(16,11),(16,13),(16,15),(16,16),(16,35),(16,56),(16,57),(16,70),(17,1),(17,2),(17,3),(17,5),(17,6),(17,11),(17,13),(17,15),(17,16),(17,17),(17,18),(17,19),(17,20),(17,21),(17,33),(17,35),(17,36),(17,37),(17,39),(17,44),(17,47),(17,49),(17,50),(17,53),(17,56),(17,57),(17,58),(17,59),(17,64),(17,65),(17,70),(18,1),(18,2),(18,3),(18,6),(18,11),(18,18),(18,70),(19,1),(19,2),(19,3),(19,6),(19,11),(19,13),(19,18),(19,19),(19,21),(19,53),(19,70),(20,1),(20,2),(20,3),(20,6),(20,11),(20,13),(20,15),(20,16),(20,18),(20,19),(20,20),(20,21),(20,35),(20,36),(20,44),(20,47),(20,49),(20,50),(20,53),(20,56),(20,57),(20,58),(20,59),(20,65),(20,70),(21,1),(21,2),(21,3),(21,6),(21,11),(21,13),(21,18),(21,21),(21,53),(21,70),(22,1),(22,2),(22,3),(22,6),(22,11),(22,13),(22,18),(22,21),(22,22),(22,53),(22,70),(30,1),(30,2),(30,3),(30,5),(30,6),(30,11),(30,13),(30,15),(30,16),(30,18),(30,19),(30,20),(30,21),(30,22),(30,30),(30,33),(30,35),(30,36),(30,44),(30,47),(30,49),(30,50),(30,53),(30,56),(30,57),(30,58),(30,59),(30,64),(30,65),(30,70),(31,1),(31,2),(31,3),(31,6),(31,11),(31,31),(31,70),(32,1),(32,2),(32,3),(32,6),(32,11),(32,32),(32,70),(33,33),(34,1),(34,2),(34,3),(34,6),(34,11),(34,13),(34,33),(34,34),(34,70),(35,1),(35,2),(35,3),(35,6),(35,11),(35,35),(35,70),(36,1),(36,2),(36,3),(36,6),(36,11),(36,36),(36,44),(36,47),(36,70),(37,1),(37,2),(37,3),(37,6),(37,11),(37,37),(37,70),(38,1),(38,2),(38,3),(38,6),(38,11),(38,37),(38,38),(38,64),(38,70),(39,1),(39,2),(39,3),(39,5),(39,6),(39,11),(39,13),(39,18),(39,21),(39,33),(39,39),(39,53),(39,56),(39,57),(39,70),(40,1),(40,2),(40,3),(40,6),(40,11),(40,36),(40,40),(40,44),(40,47),(40,49),(40,58),(40,70),(41,1),(41,2),(41,3),(41,6),(41,11),(41,13),(41,35),(41,36),(41,40),(41,41),(41,44),(41,47),(41,49),(41,58),(41,70),(42,1),(42,2),(42,3),(42,6),(42,11),(42,35),(42,36),(42,42),(42,44),(42,47),(42,49),(42,58),(42,70),(43,1),(43,2),(43,3),(43,6),(43,11),(43,13),(43,35),(43,36),(43,42),(43,43),(43,44),(43,47),(43,49),(43,58),(43,70),(44,1),(44,2),(44,3),(44,6),(44,11),(44,44),(44,70),(45,1),(45,2),(45,3),(45,6),(45,11),(45,13),(45,44),(45,45),(45,70),(47,1),(47,2),(47,3),(47,6),(47,11),(47,47),(47,70),(48,1),(48,2),(48,3),(48,6),(48,11),(48,13),(48,47),(48,48),(48,70),(49,1),(49,2),(49,3),(49,6),(49,11),(49,36),(49,44),(49,47),(49,49),(49,58),(49,70),(50,1),(50,2),(50,3),(50,6),(50,11),(50,13),(50,18),(50,21),(50,35),(50,36),(50,44),(50,47),(50,49),(50,50),(50,53),(50,56),(50,57),(50,58),(50,59),(50,70),(51,1),(51,2),(51,3),(51,6),(51,11),(51,51),(51,70),(52,1),(52,2),(52,3),(52,6),(52,11),(52,13),(52,18),(52,19),(52,21),(52,35),(52,51),(52,52),(52,53),(52,70),(53,1),(53,2),(53,3),(53,6),(53,11),(53,53),(53,70),(54,1),(54,2),(54,3),(54,6),(54,11),(54,54),(54,70),(55,1),(55,2),(55,3),(55,6),(55,11),(55,13),(55,54),(55,55),(55,70),(56,1),(56,2),(56,3),(56,6),(56,11),(56,56),(56,70),(57,1),(57,2),(57,3),(57,6),(57,11),(57,13),(57,56),(57,57),(57,70),(58,1),(58,2),(58,3),(58,6),(58,11),(58,58),(58,70),(59,1),(59,2),(59,3),(59,6),(59,11),(59,13),(59,36),(59,44),(59,47),(59,49),(59,58),(59,59),(59,70),(60,1),(60,2),(60,3),(60,5),(60,6),(60,11),(60,13),(60,18),(60,21),(60,33),(60,35),(60,36),(60,37),(60,44),(60,47),(60,49),(60,50),(60,53),(60,56),(60,57),(60,58),(60,59),(60,60),(60,70),(61,1),(61,2),(61,3),(61,6),(61,11),(61,13),(61,36),(61,44),(61,47),(61,61),(61,70),(62,62),(64,64),(65,1),(65,2),(65,3),(65,6),(65,11),(65,13),(65,18),(65,19),(65,21),(65,35),(65,36),(65,44),(65,47),(65,49),(65,50),(65,53),(65,56),(65,57),(65,58),(65,59),(65,65),(65,70),(66,0),(66,1),(66,2),(66,3),(66,5),(66,6),(66,9),(66,11),(66,13),(66,15),(66,16),(66,17),(66,18),(66,19),(66,20),(66,21),(66,22),(66,30),(66,31),(66,32),(66,33),(66,34),(66,35),(66,36),(66,37),(66,38),(66,39),(66,40),(66,41),(66,42),(66,43),(66,44),(66,45),(66,47),(66,48),(66,49),(66,50),(66,51),(66,52),(66,53),(66,54),(66,55),(66,56),(66,57),(66,58),(66,59),(66,60),(66,61),(66,62),(66,64),(66,65),(66,66),(66,67),(66,69),(66,70),(67,1),(67,2),(67,3),(67,5),(67,6),(67,11),(67,13),(67,18),(67,21),(67,33),(67,37),(67,53),(67,67),(67,70),(69,1),(69,2),(69,3),(69,6),(69,11),(69,35),(69,47),(69,69),(69,70),(70,6),(70,11),(70,70); /*!40000 ALTER TABLE `roleRole` ENABLE KEYS */; UNLOCK TABLES; + +-- +-- Dumping data for table `userPassword` +-- + +LOCK TABLES `userPassword` WRITE; +/*!40000 ALTER TABLE `userPassword` DISABLE KEYS */; +INSERT INTO `userPassword` VALUES (1,7,1,0,1,1); +/*!40000 ALTER TABLE `userPassword` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `accountConfig` +-- + +LOCK TABLES `accountConfig` WRITE; +/*!40000 ALTER TABLE `accountConfig` DISABLE KEYS */; +INSERT INTO `accountConfig` VALUES (1,'/mnt/storage/homes','/bin/bash',10000,5,60,5,30); +/*!40000 ALTER TABLE `accountConfig` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `mailConfig` +-- + +LOCK TABLES `mailConfig` WRITE; +/*!40000 ALTER TABLE `mailConfig` DISABLE KEYS */; +INSERT INTO `mailConfig` VALUES (1,'verdnatura.es'); +/*!40000 ALTER TABLE `mailConfig` ENABLE KEYS */; +UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 61587b99d..9074f3763 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -12,10 +12,6 @@ INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`) VALUES ('1', '6'); -INSERT INTO `account`.`mailConfig` (`id`, `domain`) - VALUES - ('1', 'verdnatura.es'); - INSERT INTO `vn`.`bionicConfig` (`generalInflationCoeficient`, `minimumDensityVolumetricWeight`, `verdnaturaVolumeBox`, `itemCarryBox`) VALUES (1.30, 167.00, 138000, 71); @@ -33,12 +29,16 @@ INSERT INTO `vn`.`packagingConfig`(`upperGap`) ('10'); UPDATE `account`.`role` SET id = 100 WHERE id = 0; +CALL `account`.`role_sync`; INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`) SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en' FROM `account`.`role` WHERE id <> 20 ORDER BY id; +INSERT INTO `account`.`account`(`id`) + SELECT id FROM `account`.`user`; + INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `userFk`, `bossFk`) SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9 FROM `vn`.`user`; @@ -52,7 +52,7 @@ DELETE FROM `vn`.`worker` WHERE firstName ='customer'; INSERT INTO `hedera`.`tpvConfig`(`id`, `currency`, `terminal`, `transactionType`, `maxAmount`, `employeeFk`, `testUrl`) VALUES (1, 978, 1, 0, 2000, 9, 0); - + INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,`email`,`lang`) VALUES (101, 'BruceWayne', 'Bruce Wayne', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es'), @@ -68,6 +68,24 @@ INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`, (111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'), (112, 'Trash', 'Trash', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'); +INSERT INTO `account`.`mailAlias`(`id`, `alias`, `description`, `isPublic`) + VALUES + (1, 'general', 'General mailing list', FALSE), + (2, 'it' , 'IT department' , TRUE), + (3, 'sales' , 'Sales department' , TRUE); + +INSERT INTO `account`.`mailAliasAccount`(`mailAlias`, `account`) + VALUES + (1, 1), + (1, 18), + (3, 18), + (1, 9), + (2, 9); + +INSERT INTO `account`.`mailForward`(`account`, `forwardTo`) + VALUES + (1, 'employee@domain.local'); + INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`) VALUES (106, 'LGN', 'David Charles', 'Haller', 106, 19, 432978106), @@ -88,6 +106,15 @@ INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, (19,'Francia', 1, 'FR', 1, 27), (30,'Canarias', 1, 'IC', 1, 24); +INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`) + VALUES + ('ca', 'Català' , 'Catalan' , TRUE), + ('en', 'English' , 'English' , TRUE), + ('es', 'Español' , 'Spanish' , TRUE), + ('fr', 'Français' , 'French' , TRUE), + ('mn', 'Португалий', 'Mongolian' , TRUE), + ('pt', 'Português' , 'Portuguese', TRUE); + INSERT INTO `vn`.`warehouseAlias`(`id`, `name`) VALUES (1, 'Main Warehouse'), diff --git a/db/export-data.sh b/db/export-data.sh index 9f3997cf7..0aa9fb319 100755 --- a/db/export-data.sh +++ b/db/export-data.sh @@ -22,6 +22,9 @@ TABLES=( role roleInherit roleRole + userPassword + accountConfig + mailConfig ) dump_tables ${TABLES[@]} diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index e1189ca50..91961c5e3 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -276,7 +276,7 @@ export default { relevancy: 'vn-item-basic-data vn-input-number[ng-model="$ctrl.item.relevancy"]', origin: 'vn-autocomplete[ng-model="$ctrl.item.originFk"]', compression: 'vn-item-basic-data vn-input-number[ng-model="$ctrl.item.compression"]', - isFragile: 'vn-check[label="isFragile"]', + isFragile: 'vn-check[ng-model="$ctrl.item.isFragile"]', longName: 'vn-textfield[ng-model="$ctrl.item.longName"]', isActiveCheckbox: 'vn-check[label="Active"]', priceInKgCheckbox: 'vn-check[label="Price in kg"]', diff --git a/e2e/paths/02-client/14_balance.spec.js b/e2e/paths/02-client/14_balance.spec.js index 6c16d455d..8fb2eb40c 100644 --- a/e2e/paths/02-client/14_balance.spec.js +++ b/e2e/paths/02-client/14_balance.spec.js @@ -45,6 +45,7 @@ describe('Client balance path', () => { }); it('should create a new payment that clears the debt', async() => { + await page.closePopup(); await page.waitToClick(selectors.clientBalance.newPaymentButton); await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Pay on receipt'); await page.waitToClick(selectors.clientBalance.saveButton); diff --git a/e2e/paths/04-item/02_basic_data.spec.js b/e2e/paths/04-item/02_basic_data.spec.js index 836efaa2e..f04031480 100644 --- a/e2e/paths/04-item/02_basic_data.spec.js +++ b/e2e/paths/04-item/02_basic_data.spec.js @@ -113,7 +113,7 @@ describe('Item Edit basic data path', () => { const result = await page .checkboxState(selectors.itemBasicData.isFragile); - expect(result).toBe('unchecked'); + expect(result).toBe('checked'); }); it('should confirm isActive checkbox is unchecked', async() => { diff --git a/front/core/components/crud-model/crud-model.js b/front/core/components/crud-model/crud-model.js index 9cfa3b410..4994e1547 100644 --- a/front/core/components/crud-model/crud-model.js +++ b/front/core/components/crud-model/crud-model.js @@ -134,7 +134,7 @@ export default class CrudModel extends ModelProxy { */ save() { if (!this.isChanged) - return null; + return this.$q.resolve(); let deletes = []; let updates = []; diff --git a/front/core/components/data-viewer/index.html b/front/core/components/data-viewer/index.html index 8c843d869..3488b7b0b 100644 --- a/front/core/components/data-viewer/index.html +++ b/front/core/components/data-viewer/index.html @@ -1,6 +1,7 @@
diff --git a/front/core/components/list/style.scss b/front/core/components/list/style.scss index 223748f4a..adbc496c7 100644 --- a/front/core/components/list/style.scss +++ b/front/core/components/list/style.scss @@ -32,7 +32,6 @@ vn-list, vn-item, .vn-item { - @extend %clickable; display: flex; align-items: center; color: inherit; @@ -85,4 +84,6 @@ vn-item, } } - +a.vn-item { + @extend %clickable; +} diff --git a/front/core/components/menu/menu.js b/front/core/components/menu/menu.js index 3eb169926..d0c649004 100755 --- a/front/core/components/menu/menu.js +++ b/front/core/components/menu/menu.js @@ -1,5 +1,6 @@ import ngModule from '../../module'; import Popover from '../popover'; +import './style.scss'; export default class Menu extends Popover { show(parent) { diff --git a/front/core/components/menu/style.scss b/front/core/components/menu/style.scss new file mode 100644 index 000000000..92f437243 --- /dev/null +++ b/front/core/components/menu/style.scss @@ -0,0 +1,7 @@ +@import "./effects"; + +.vn-menu { + vn-item, .vn-item { + @extend %clickable; + } +} diff --git a/front/core/components/model-proxy/model-proxy.js b/front/core/components/model-proxy/model-proxy.js index 592b25710..26c28c803 100644 --- a/front/core/components/model-proxy/model-proxy.js +++ b/front/core/components/model-proxy/model-proxy.js @@ -107,15 +107,16 @@ export default class ModelProxy extends DataModel { * Removes a row from the model and emits the 'rowRemove' event. * * @param {Number} index The row index + * @return {Promise} The save request promise */ remove(index) { - let [item] = this.data.splice(index, 1); + let [row] = this.data.splice(index, 1); - let proxiedIndex = this.proxiedData.indexOf(item); + let proxiedIndex = this.proxiedData.indexOf(row); this.proxiedData.splice(proxiedIndex, 1); - if (!item.$isNew) - this.removed.push(item); + if (!row.$isNew) + this.removed.push(row); this.isChanged = true; if (!this.data.length) @@ -125,7 +126,19 @@ export default class ModelProxy extends DataModel { this.emit('dataUpdate'); if (this.autoSave) - this.save(); + return this.save(); + else + return this.$q.resolve(); + } + + /** + * Removes a row from the model and emits the 'rowRemove' event. + * + * @param {Object} row The row object + * @return {Promise} The save request promise + */ + removeRow(row) { + return this.remove(this.data.indexOf(row)); } /** diff --git a/front/core/components/searchbar/locale/en.yml b/front/core/components/searchbar/locale/en.yml index 52ab6a184..e5b562c24 100644 --- a/front/core/components/searchbar/locale/en.yml +++ b/front/core/components/searchbar/locale/en.yml @@ -1 +1 @@ -Search by: Search by {{module | translate}} \ No newline at end of file +Search for: Search {{module}} \ No newline at end of file diff --git a/front/core/components/searchbar/locale/es.yml b/front/core/components/searchbar/locale/es.yml index 730564a7b..53a5bb289 100644 --- a/front/core/components/searchbar/locale/es.yml +++ b/front/core/components/searchbar/locale/es.yml @@ -1 +1 @@ -Search by: Buscar por {{module | translate}} \ No newline at end of file +Search for: Buscar {{module}} \ No newline at end of file diff --git a/front/core/components/searchbar/searchbar.js b/front/core/components/searchbar/searchbar.js index 0b7bd8cd2..8adc40b67 100644 --- a/front/core/components/searchbar/searchbar.js +++ b/front/core/components/searchbar/searchbar.js @@ -16,6 +16,7 @@ import './style.scss'; * @property {Function} onSearch Function to call when search is submited * @property {CrudModel} model The model used for searching * @property {Function} exprBuilder If defined, is used to build each non-null param expresion + * @property {String} baseState The base state for searchs */ export default class Searchbar extends Component { constructor($element, $) { @@ -23,6 +24,8 @@ export default class Searchbar extends Component { this.searchState = '.'; this.placeholder = 'Search'; this.autoState = true; + this.separateIndex = true; + this.entityState = 'card.summary'; this.deregisterCallback = this.$transitions.onSuccess( {}, transition => this.onStateChange(transition)); @@ -33,11 +36,13 @@ export default class Searchbar extends Component { if (!this.baseState) { let stateParts = this.$state.current.name.split('.'); this.baseState = stateParts[0]; - } + this.searchState = `${this.baseState}.index`; + } else + this.searchState = this.baseState; - this.searchState = `${this.baseState}.index`; - this.placeholder = this.$t('Search by', { - module: this.baseState + let description = this.$state.get(this.baseState).description; + this.placeholder = this.$t('Search for', { + module: this.$t(description).toLowerCase() }); } @@ -222,7 +227,7 @@ export default class Searchbar extends Component { subState += '.index'; break; default: - subState = 'card.summary'; + subState = this.entityState; } if (this.stateParams) @@ -292,8 +297,10 @@ ngModule.vnComponent('vnSearchbar', { panel: '@', info: '@?', onSearch: '&?', - baseState: '@?', autoState: ' this.callback(transition)); - this.updateOriginalData(); + this.snapshot(); } $onInit() { - if (this.get && this.url) - this.fetchData(); - else if (this.get && !this.url) - throw new Error('URL parameter ommitted'); - } + let fetch = !this.insertMode + && this.get + && this.url + && this.idValue; - $onChanges() { - if (this.data) - this.updateOriginalData(); + if (fetch) + this.fetch(); + else { + this.isNew = !!this.insertMode; + this.snapshot(); + } } $onDestroy() { this.deregisterCallback(); } + /** + * @type {Booelan} The computed instance HTTP path + */ + get instanceUrl() { + return `${this.url}/${this.idValue}`; + } + + /** + * @type {Object} The instance data + */ + get data() { + return this._data; + } + + set data(value) { + this._data = value; + this.isNew = !!this.insertMode; + this.snapshot(); + } + + /** + * @type {Booelan} Whether it's popullated with data + */ + get hasData() { + return !!this.data; + } + + set hasData(value) { + if (value) + this.fill(); + else + this.delete(); + } + + /** + * @type {Booelan} Whether instance data have been modified + */ get dirty() { return this.form && this.form.$dirty || this.dataChanged(); } - dataChanged() { - let data = this.copyInNewObject(this.data); - return !isEqual(data, this.orgData); + fetch() { + let filter = mergeFilters({ + fields: this.fields, + where: this.where, + include: this.include + }, this.filter); + + let params = filter ? {filter} : null; + + return this.$http.get(this.instanceUrl, params) + .then(json => { + this.overwrite(json.data); + this.isNew = false; + this.snapshot(); + }) + .catch(err => { + if (!(err.name == 'HttpError' && err.status == 404)) + throw err; + + if (this.autoFill) { + this.insert(); + this.snapshot(); + } + }); } - fetchData() { - let id = this.data[this.idField]; - return this.$http.get(`${this.url}/${id}`).then( - json => { - this.data = copyObject(json.data); - this.updateOriginalData(); - } - ); + insert(data) { + this.assign({[this.idField]: this.idValue}, data); + this.isNew = true; + this.deleted = null; + } + + delete() { + if (!this.hasData) return; + this.deleted = this.makeSnapshot(); + this.clear(); + this.isNew = false; + } + + recover() { + if (!this.deleted) return; + this.restoreSnapshot(this.deleted); + } + + fill() { + if (this.hasData) + return; + + if (this.deleted) + this.recover(); + else if (this.original && this.original.data) + this.reset(); + else + this.insert(); + } + + reset() { + this.restoreSnapshot(this.original); + this.setPristine(); + } + + snapshot() { + const snapshot = this.makeSnapshot(); + + if (snapshot.data) { + const idValue = snapshot.data[this.idField]; + if (idValue) this.idValue = idValue; + } + + this.original = snapshot; + this.orgData = snapshot.data; + this.deleted = null; + this.setPristine(); + } + + makeSnapshot() { + return { + data: this.copyData(), + isNew: this.isNew, + ref: this.data + }; + } + + restoreSnapshot(snapshot) { + if (!snapshot) return; + this._data = snapshot.ref; + this.overwrite(snapshot.data); + this.isNew = snapshot.isNew; + this.deleted = null; + } + + writeData(res) { + if (this.hasData) + this.assign(res.data); + this.isNew = false; + this.snapshot(); + return res; + } + + clear() { + this._data = null; + } + + overwrite(data) { + if (data) { + if (!this.data) this._data = {}; + overwrite(this.data, data); + } else + this._data = null; + } + + assign(...args) { + this._data = Object.assign(this.data || {}, ...args); + } + + copyData() { + return copyObject(this.data); + } + + refresh() { + return this.fetch(); + } + + dataChanged() { + return !isEqual(this.orgData, this.copyData()); } /** @@ -100,9 +250,10 @@ export default class Watcher extends Component { */ submit() { try { - if (this.requestMethod() !== 'post') + if (this.isNew) + this.isInvalid(); + else this.check(); - else this.isInvalid(); } catch (err) { return this.$q.reject(err); } @@ -122,65 +273,42 @@ export default class Watcher extends Component { if (this.form) this.form.$setSubmitted(); - const isPost = (this.requestMethod() === 'post'); - if (!this.dataChanged() && !isPost) { - this.updateOriginalData(); + if (!this.dataChanged() && !this.isNew) { + this.snapshot(); return this.$q.resolve(); } - let changedData = isPost + let changedData = this.isNew ? this.data : getModifiedData(this.data, this.orgData); - let id = this.idField ? this.orgData[this.idField] : null; - // If watcher is associated to mgCrud if (this.save && this.save.accept) { - if (id) - changedData[this.idField] = id; - this.save.model = changedData; - return this.$q((resolve, reject) => { - this.save.accept().then( - json => this.writeData({data: json}, resolve), - reject - ); - }); + return this.save.accept() + .then(json => this.writeData({data: json})); } // When mgCrud is not used - if (id) { - return this.$q((resolve, reject) => { - this.$http.patch(`${this.url}/${id}`, changedData).then( - json => this.writeData(json, resolve), - reject - ); - }); - } + let req; - return this.$q((resolve, reject) => { - this.$http.post(this.url, changedData).then( - json => this.writeData(json, resolve), - reject - ); - }); - } - /** - * return the request method. - */ + if (this.deleted) + req = this.$http.delete(this.instanceUrl); + else if (this.isNew) + req = this.$http.post(this.url, changedData); + else + req = this.$http.patch(this.instanceUrl, changedData); - requestMethod() { - return this.$attrs.save && this.$attrs.save.toLowerCase(); + return req.then(res => this.writeData(res)); } /** * Checks if data is ready to send. */ check() { - if (this.form && this.form.$invalid) - throw new UserError('Some fields are invalid'); + this.isInvalid(); if (!this.dirty) throw new UserError('No changes to save'); } @@ -220,62 +348,82 @@ export default class Watcher extends Component { onConfirmResponse(response) { if (response === 'accept') { - if (this.data) - Object.assign(this.data, this.orgData); + this.reset(); this.$state.go(this.state); } else this.state = null; } - writeData(json, resolve) { - Object.assign(this.data, json.data); - this.updateOriginalData(); - resolve(json); - } - - updateOriginalData() { - this.orgData = this.copyInNewObject(this.data); - this.setPristine(); - } - + /** + * @deprecated Use reset() + */ loadOriginalData() { - const orgData = JSON.parse(JSON.stringify(this.orgData)); - this.data = Object.assign(this.data, orgData); - this.setPristine(); + this.reset(); } - copyInNewObject(data) { - let newCopy = {}; - if (data && typeof data === 'object') { - Object.keys(data).forEach( - key => { - let value = data[key]; - if (value instanceof Date) - newCopy[key] = new Date(value.getTime()); - else if (!isFullEmpty(value)) { - if (typeof value === 'object') - newCopy[key] = this.copyInNewObject(value); - else - newCopy[key] = value; - } - } - ); - } - - return newCopy; + /** + * @deprecated Use snapshot() + */ + updateOriginalData() { + this.snapshot(); } } -Watcher.$inject = ['$element', '$scope', '$state', '$stateParams', '$transitions', '$http', 'vnApp', '$translate', '$attrs', '$q']; +Watcher.$inject = ['$element', '$scope']; + +function copyObject(data) { + let newCopy; + + if (data && typeof data === 'object') { + newCopy = {}; + Object.keys(data).forEach( + key => { + let value = data[key]; + if (value instanceof Date) + newCopy[key] = new Date(value.getTime()); + else if (!isFullEmpty(value)) { + if (typeof value === 'object') + newCopy[key] = copyObject(value); + else + newCopy[key] = value; + } + } + ); + } else + newCopy = data; + + return newCopy; +} + +function clearObject(obj) { + if (!obj) return; + for (let key in obj) { + if (obj.hasOwnProperty(key)) + delete obj[key]; + } +} + +function overwrite(obj, data) { + if (!obj) return; + clearObject(obj); + Object.assign(obj, data); +} ngModule.vnComponent('vnWatcher', { template: require('./watcher.html'), bindings: { url: '@?', idField: '@?', - data: '<', + idValue: ' { let $scope; @@ -9,10 +8,16 @@ describe('Component vnWatcher', () => { let controller; let $attrs; let $q; + let data; beforeEach(ngModule('vnCore')); beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$state_, _$q_) => { + data = { + id: 1, + foo: 'bar' + }; + $scope = $rootScope.$new(); $element = angular.element('
'); $state = _$state_; @@ -25,38 +30,49 @@ describe('Component vnWatcher', () => { })); describe('$onInit()', () => { - it('should call fetchData() if controllers get and url properties are defined', () => { - controller.get = () => {}; - controller.url = 'test.com'; - jest.spyOn(controller, 'fetchData').mockReturnThis(); + it('should set data empty by default', () => { controller.$onInit(); - expect(controller.fetchData).toHaveBeenCalledWith(); + expect(controller.data).toBeUndefined(); }); - it(`should throw an error if $onInit is called without url defined`, () => { - controller.get = () => {}; + it('should set new data when insert mode is enabled', () => { + controller.insertMode = true; + controller.data = data; - expect(function() { - controller.$onInit(); - }).toThrowError(/parameter/); + controller.$onInit(); + + expect(controller.orgData).toEqual(data); + expect(controller.orgData).toEqual(data); + expect(controller.isNew).toBeTruthy(); + }); + + it('should call backend and fetch data if url and idValue properties are defined', () => { + controller.url = 'Foos'; + controller.idValue = 1; + + $httpBackend.expectGET('Foos/1').respond(data); + controller.$onInit(); + $httpBackend.flush(); + + expect(controller.orgData).toEqual(data); + expect(controller.orgData).toEqual(data); + expect(controller.orgData).not.toBe(controller.data); }); }); - describe('fetchData()', () => { - it(`should perform a query then store the received data into controller.data and call updateOriginalData()`, () => { - jest.spyOn(controller, 'updateOriginalData'); - let json = {data: 'some data'}; - controller.data = [1]; - controller.idField = 0; - controller.url = 'test.com'; - $httpBackend.whenGET('test.com/1').respond(json); - $httpBackend.expectGET('test.com/1'); - controller.fetchData(); + describe('fetch()', () => { + it(`should perform a query then store the received data into data property and make an snapshot into orgData`, () => { + controller.url = 'Bars'; + controller.idValue = 1; + + $httpBackend.expectGET('Bars/1').respond(data); + controller.$onInit(); $httpBackend.flush(); - expect(controller.data).toEqual({data: 'some data'}); - expect(controller.updateOriginalData).toHaveBeenCalledWith(); + expect(controller.orgData).toEqual(data); + expect(controller.orgData).toEqual(data); + expect(controller.orgData).not.toBe(controller.data); }); }); @@ -95,14 +111,6 @@ describe('Component vnWatcher', () => { controller.check(); }).toThrowError(); }); - - it(`should throw error if controller.dirty is true`, () => { - controller.form = {$invalid: true}; - - expect(function() { - controller.check(); - }).toThrowError(); - }); }); describe('realSubmit()', () => { @@ -130,59 +138,60 @@ describe('Component vnWatcher', () => { }); }); - describe('when id is defined', () => { - it(`should perform a query then call controller.writeData()`, done => { - controller.dataChanged = () => { - return true; - }; - controller.data = {id: 2}; - controller.orgData = {id: 1}; - let changedData = getModifiedData(controller.data, controller.orgData); - controller.idField = 'id'; - controller.url = 'test.com'; - let json = {data: 'some data'}; - jest.spyOn(controller, 'writeData'); - $httpBackend.whenPATCH(`${controller.url}/1`, changedData).respond(json); - $httpBackend.expectPATCH(`${controller.url}/1`); - controller.realSubmit() - .then(() => { - expect(controller.writeData).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function)); - done(); - }).catch(done.fail); + describe('should perform a PATCH query and save the data', () => { + it(`should perform a query then call controller.writeData()`, () => { + controller.url = 'Foos'; + controller.data = data; + + const changedData = {baz: 'value'}; + Object.assign(controller.data, changedData); + + $httpBackend.expectPATCH('Foos/1', changedData).respond({newProp: 'some'}); + controller.realSubmit(); $httpBackend.flush(); + + expect(controller.data).toEqual(Object.assign({}, data, changedData)); }); }); - it(`should perform a POST query then call controller.writeData()`, done => { - controller.dataChanged = () => { - return true; - }; - controller.data = {id: 2}; - controller.orgData = {id: 1}; - controller.url = 'test.com'; - let json = {data: 'some data'}; - jest.spyOn(controller, 'writeData'); - $httpBackend.whenPOST(`${controller.url}`, controller.data).respond(json); - $httpBackend.expectPOST(`${controller.url}`, controller.data); - controller.realSubmit() - .then(() => { - expect(controller.writeData).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function)); - done(); - }).catch(done.fail); + it(`should perform a POST query and save the data`, () => { + controller.insertMode = true; + controller.url = 'Foos'; + controller.data = data; + + const changedData = {baz: 'value'}; + Object.assign(controller.data, changedData); + + $httpBackend.expectPOST('Foos', controller.data).respond({newProp: 'some'}); + controller.realSubmit(); $httpBackend.flush(); + + expect(controller.data).toEqual(Object.assign({}, data, changedData)); + }); + + describe('should perform a DELETE query and save empty data', () => { + it(`should perform a query then call controller.writeData()`, () => { + controller.url = 'Foos'; + controller.data = data; + controller.delete(); + + $httpBackend.expectDELETE('Foos/1').respond(); + controller.realSubmit(); + $httpBackend.flush(); + + expect(controller.data).toBeNull(); + }); }); }); describe('writeData()', () => { - it(`should call Object.asssign() function over controllers.data with json.data, then call updateOriginalData function and finally call resolve() function`, () => { - jest.spyOn(controller, 'updateOriginalData'); - controller.data = {}; - let json = {data: 'some data'}; - let resolve = jasmine.createSpy('resolve'); - controller.writeData(json, resolve); + it(`should save data into orgData`, () => { + controller.data = data; + Object.assign(controller.data, {baz: 'value'}); - expect(controller.updateOriginalData).toHaveBeenCalledWith(); - expect(resolve).toHaveBeenCalledWith(json); + controller.writeData({}); + + expect(controller.data).toEqual(controller.orgData); }); }); @@ -224,37 +233,38 @@ describe('Component vnWatcher', () => { describe(`onConfirmResponse()`, () => { describe(`when response is accept`, () => { - it(`should call Object.assing on controlle.data with controller.orgData then call go() on state`, () => { - let response = 'accept'; - controller.data = {}; - controller.orgData = {name: 'Batman'}; + it(`should reset data them go to state`, () => { + let data = {key: 'value'}; + controller.data = data; + data.foo = 'bar'; controller.$state = {go: jasmine.createSpy('go')}; - controller.state = 'Batman'; - controller.onConfirmResponse(response); + controller.state = 'foo.bar'; + controller.onConfirmResponse('accept'); - expect(controller.data).toEqual(controller.orgData); + expect(controller.data).toEqual({key: 'value'}); expect(controller.$state.go).toHaveBeenCalledWith(controller.state); }); }); describe(`when response is not accept`, () => { it(`should set controller.state to null`, () => { - let response = 'anything but accept'; controller.state = 'Batman'; - controller.onConfirmResponse(response); + controller.onConfirmResponse('cancel'); expect(controller.state).toBeFalsy(); }); }); }); - describe(`loadOriginalData()`, () => { - it(`should iterate over the current data object, delete all properties then assign the ones from original data`, () => { - controller.data = {name: 'Bruce'}; - controller.orgData = {name: 'Batman'}; - controller.loadOriginalData(); + describe(`reset()`, () => { + it(`should reset data as it was before changing it`, () => { + let data = {key: 'value'}; - expect(controller.data).toEqual(controller.orgData); + controller.data = data; + data.foo = 'bar'; + controller.reset(); + + expect(data).toEqual({key: 'value'}); }); }); }); diff --git a/front/core/directives/rule.js b/front/core/directives/rule.js index 85b6041f8..f65efe176 100644 --- a/front/core/directives/rule.js +++ b/front/core/directives/rule.js @@ -65,7 +65,7 @@ export function directive($translate, $window) { }; if (form) - $scope.$watch(form.$submitted, refreshError); + $scope.$watch(() => form.$submitted, refreshError); } } ngModule.directive('rule', directive); diff --git a/front/core/styles/effects.scss b/front/core/styles/effects.scss index 205a23bd2..38499f52f 100644 --- a/front/core/styles/effects.scss +++ b/front/core/styles/effects.scss @@ -25,4 +25,10 @@ %active { background-color: $color-active; color: $color-active-font; + + &:hover, + &:focus { + background-color: $color-active; + color: $color-active-font; + } } diff --git a/front/core/styles/icons/MaterialIcons-Regular.woff2 b/front/core/styles/icons/MaterialIcons-Regular.woff2 index 9fa211252..2b86ebfe6 100644 Binary files a/front/core/styles/icons/MaterialIcons-Regular.woff2 and b/front/core/styles/icons/MaterialIcons-Regular.woff2 differ diff --git a/front/module-import.js b/front/module-import.js index dd1692c18..baebe30eb 100755 --- a/front/module-import.js +++ b/front/module-import.js @@ -18,5 +18,6 @@ export default function moduleImport(moduleName) { case 'invoiceOut' : return import('invoiceOut/front'); case 'route' : return import('route/front'); case 'entry' : return import('entry/front'); + case 'account' : return import('account/front'); } } diff --git a/front/salix/components/descriptor/index.html b/front/salix/components/descriptor/index.html index 366bfab5d..ed2305f13 100644 --- a/front/salix/components/descriptor/index.html +++ b/front/salix/components/descriptor/index.html @@ -8,13 +8,13 @@
+ ui-sref="{{::$ctrl.summaryState}}({id: $ctrl.descriptor.id})"> { this.$.$root.user = json.data; - window.localStorage.currentUserWorkerId = json.data.workerId; + window.localStorage.currentUserWorkerId = json.data.id; }); } } diff --git a/front/salix/components/left-menu/left-menu.html b/front/salix/components/left-menu/left-menu.html index ff32eea70..90689189d 100644 --- a/front/salix/components/left-menu/left-menu.html +++ b/front/salix/components/left-menu/left-menu.html @@ -1,6 +1,6 @@
My account diff --git a/front/salix/locale/es.yml b/front/salix/locale/es.yml index 287f840a5..75c65ef64 100644 --- a/front/salix/locale/es.yml +++ b/front/salix/locale/es.yml @@ -44,6 +44,7 @@ Routes: Rutas Locator: Localizador Invoices out: Facturas emitidas Entries: Entradas +Users: Usuarios # Common diff --git a/loopback/locale/es.json b/loopback/locale/es.json index d28cceb41..ea5a09952 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -134,5 +134,14 @@ "This ticket is deleted": "Este ticket está eliminado", "A travel with this data already exists": "Ya existe un travel con estos datos", "This thermograph id already exists": "La id del termógrafo ya existe", - "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante" + "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante", + "ORDER_ALREADY_CONFIRMED": "ORDER_ALREADY_CONFIRMED", + "Invalid password": "Invalid password", + "Password does not meet requirements": "Password does not meet requirements", + "Role already assigned": "Role already assigned", + "Invalid role name": "Invalid role name", + "Role name must be written in camelCase": "Role name must be written in camelCase", + "can't be set": "can't be set", + "Email already exists": "Email already exists", + "User already exists": "User already exists" } \ No newline at end of file diff --git a/modules/account/back/methods/role-inherit/sync.js b/modules/account/back/methods/role-inherit/sync.js new file mode 100644 index 000000000..1b6a3cd83 --- /dev/null +++ b/modules/account/back/methods/role-inherit/sync.js @@ -0,0 +1,114 @@ +const ldap = require('../../util/ldapjs-extra'); + +module.exports = Self => { + Self.remoteMethod('sync', { + description: 'Synchronizes the user with the other user databases', + http: { + path: `/sync`, + verb: 'PATCH' + } + }); + + Self.sync = async function() { + let $ = Self.app.models; + + let ldapConfig = await $.LdapConfig.findOne({ + fields: ['host', 'rdn', 'password', 'groupDn'] + }); + let accountConfig = await $.AccountConfig.findOne({ + fields: ['idBase'] + }); + + if (!ldapConfig) return; + + // Connect + + let client = ldap.createClient({ + url: `ldap://${ldapConfig.host}:389` + }); + + let ldapPassword = Buffer + .from(ldapConfig.password, 'base64') + .toString('ascii'); + await client.bind(ldapConfig.rdn, ldapPassword); + + let err; + try { + // Delete roles + + let opts = { + scope: 'sub', + attributes: ['dn'], + filter: 'objectClass=posixGroup' + }; + res = await client.search(ldapConfig.groupDn, opts); + + let reqs = []; + await new Promise((resolve, reject) => { + res.on('error', err => { + if (err.name === 'NoSuchObjectError') + err = new Error(`Object '${ldapConfig.groupDn}' does not exist`); + reject(err); + }); + res.on('searchEntry', e => { + reqs.push(client.del(e.object.dn)); + }); + res.on('end', resolve); + }); + await Promise.all(reqs); + + // Recreate roles + + let roles = await $.Role.find({ + fields: ['id', 'name'] + }); + let accounts = await $.UserAccount.find({ + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['name'], + include: { + relation: 'roles', + scope: { + fields: ['inheritsFrom'] + } + } + } + } + }); + + let map = new Map(); + for (let account of accounts) { + let user = account.user(); + for (let inherit of user.roles()) { + let roleId = inherit.inheritsFrom; + if (!map.has(roleId)) map.set(roleId, []); + map.get(roleId).push(user.name); + } + } + + reqs = []; + for (let role of roles) { + let newEntry = { + objectClass: ['top', 'posixGroup'], + cn: role.name, + gidNumber: accountConfig.idBase + role.id + }; + + let memberUid = map.get(role.id); + if (memberUid) newEntry.memberUid = memberUid; + + let dn = `cn=${role.name},${ldapConfig.groupDn}`; + reqs.push(client.add(dn, newEntry)); + } + await Promise.all(reqs); + } catch (e) { + err = e; + } + + // FIXME: Cannot disconnect, hangs on undind() call + // await client.unbind(); + if (err) throw err; + }; +}; diff --git a/modules/account/back/methods/user-account/sync-by-id.js b/modules/account/back/methods/user-account/sync-by-id.js new file mode 100644 index 000000000..3c292dd4c --- /dev/null +++ b/modules/account/back/methods/user-account/sync-by-id.js @@ -0,0 +1,27 @@ + +module.exports = Self => { + Self.remoteMethod('syncById', { + description: 'Synchronizes the user with the other user databases', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'The user id', + required: true + }, { + arg: 'password', + type: 'string', + description: 'The password' + } + ], + http: { + path: `/:id/syncById`, + verb: 'PATCH' + } + }); + + Self.syncById = async function(id, password) { + let user = await Self.app.models.Account.findById(id, {fields: ['name']}); + await Self.sync(user.name, password); + }; +}; diff --git a/modules/account/back/methods/user-account/sync.js b/modules/account/back/methods/user-account/sync.js new file mode 100644 index 000000000..202411e35 --- /dev/null +++ b/modules/account/back/methods/user-account/sync.js @@ -0,0 +1,267 @@ +const ldap = require('../../util/ldapjs-extra'); +const nthash = require('smbhash').nthash; +const ssh = require('node-ssh'); +const crypto = require('crypto'); + +module.exports = Self => { + Self.remoteMethod('sync', { + description: 'Synchronizes the user with the other user databases', + accepts: [ + { + arg: 'userName', + type: 'string', + description: 'The user name', + required: true + }, { + arg: 'password', + type: 'string', + description: 'The password' + } + ], + http: { + path: `/sync`, + verb: 'PATCH' + } + }); + + Self.sync = async function(userName, password) { + let $ = Self.app.models; + + let user = await $.Account.findOne({ + fields: ['id'], + where: {name: userName} + }); + let isSync = !await $.UserSync.exists(userName); + + if (user && isSync) return; + + let accountConfig; + let mailConfig; + let extraParams; + let hasAccount = false; + + if (user) { + accountConfig = await $.AccountConfig.findOne({ + fields: ['homedir', 'shell', 'idBase'] + }); + mailConfig = await $.MailConfig.findOne({ + fields: ['domain'] + }); + + user = await $.Account.findById(user.id, { + fields: [ + 'id', + 'nickname', + 'email', + 'lang', + 'roleFk', + 'sync', + 'active', + 'created', + 'updated' + ], + where: {name: userName}, + include: { + relation: 'roles', + scope: { + include: { + relation: 'inherits', + scope: { + fields: ['name'] + } + } + } + } + }); + + extraParams = { + corporateMail: `${userName}@${mailConfig.domain}`, + uidNumber: accountConfig.idBase + user.id + }; + + hasAccount = user.active + && await $.UserAccount.exists(user.id); + } + + if (user) { + let bcryptPassword = $.User.hashPassword(password); + + await $.Account.upsertWithWhere({id: user.id}, + {bcryptPassword} + ); + await $.user.upsert({ + id: user.id, + username: userName, + password: bcryptPassword, + email: user.email, + created: user.created, + updated: user.updated + }); + } + + // SIP + + if (hasAccount) { + await Self.rawSql('CALL pbx.sip_setPassword(?, ?)', + [user.id, password] + ); + } + + // LDAP + + let ldapConfig = await $.LdapConfig.findOne({ + fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn'] + }); + + if (ldapConfig) { + let ldapClient = ldap.createClient({ + url: `ldap://${ldapConfig.host}:389` + }); + + let ldapPassword = Buffer + .from(ldapConfig.password, 'base64') + .toString('ascii'); + await ldapClient.bind(ldapConfig.rdn, ldapPassword); + + let err; + try { + // Deletes user + + try { + let dn = `uid=${userName},${ldapConfig.baseDn}`; + await ldapClient.del(dn); + } catch (e) { + if (e.name !== 'NoSuchObjectError') throw e; + } + + // Removes user from groups + + let opts = { + scope: 'sub', + attributes: ['dn'], + filter: `&(memberUid=${userName})(objectClass=posixGroup)` + }; + res = await ldapClient.search(ldapConfig.groupDn, opts); + + let oldGroups = []; + await new Promise((resolve, reject) => { + res.on('error', reject); + res.on('searchEntry', e => oldGroups.push(e.object)); + res.on('end', resolve); + }); + + let reqs = []; + for (oldGroup of oldGroups) { + let change = new ldap.Change({ + operation: 'delete', + modification: {memberUid: userName} + }); + reqs.push(ldapClient.modify(oldGroup.dn, change)); + } + await Promise.all(reqs); + + if (hasAccount) { + // Recreates user + + let nameArgs = user.nickname.split(' '); + let sshaPassword = crypto + .createHash('sha1') + .update(password) + .digest('base64'); + + let dn = `uid=${userName},${ldapConfig.baseDn}`; + let newEntry = { + uid: userName, + objectClass: [ + 'inetOrgPerson', + 'posixAccount', + 'sambaSamAccount' + ], + cn: user.nickname || userName, + displayName: user.nickname, + givenName: nameArgs[0], + sn: nameArgs[1] || 'Empty', + mail: extraParams.corporateMail, + userPassword: `{SSHA}${sshaPassword}`, + preferredLanguage: user.lang, + homeDirectory: `${accountConfig.homedir}/${userName}`, + loginShell: accountConfig.shell, + uidNumber: extraParams.uidNumber, + gidNumber: accountConfig.idBase + user.roleFk, + sambaSID: '-', + sambaNTPassword: nthash(password) + }; + await ldapClient.add(dn, newEntry); + + // Adds user to groups + + let reqs = []; + for (let role of user.roles()) { + let change = new ldap.Change({ + operation: 'add', + modification: {memberUid: userName} + }); + let roleName = role.inherits().name; + let dn = `cn=${roleName},${ldapConfig.groupDn}`; + reqs.push(ldapClient.modify(dn, change)); + } + await Promise.all(reqs); + } + } catch (e) { + err = e; + } + + // FIXME: Cannot disconnect, hangs on undind() call + // await ldapClient.unbind(); + if (err) throw err; + } + + // Samba + + let sambaConfig = await $.SambaConfig.findOne({ + fields: ['host', 'sshUser', 'sshPass'] + }); + + if (sambaConfig) { + let sshPassword = Buffer + .from(sambaConfig.sshPass, 'base64') + .toString('ascii'); + + let sshClient = new ssh.NodeSSH(); + await sshClient.connect({ + host: sambaConfig.host, + username: sambaConfig.sshUser, + password: sshPassword + }); + + let commands; + + if (hasAccount) { + commands = [ + `samba-tool user create "${userName}" ` + + `--uid-number=${extraParams.uidNumber} ` + + `--mail-address="${extraParams.corporateMail}" ` + + `--random-password`, + `samba-tool user setexpiry "${userName}" ` + + `--noexpiry`, + `samba-tool user setpassword "${userName}" ` + + `--newpassword="${password}"`, + `mkhomedir_helper "${userName}" 0027` + ]; + } else { + commands = [ + `samba-tool user delete "${userName}"` + ]; + } + + for (let command of commands) + await sshClient.execCommand(command); + + await sshClient.dispose(); + } + + // Mark as synchronized + + await $.UserSync.destroyById(userName); + }; +}; diff --git a/modules/account/back/model-config.json b/modules/account/back/model-config.json new file mode 100644 index 000000000..d243a2cca --- /dev/null +++ b/modules/account/back/model-config.json @@ -0,0 +1,38 @@ +{ + "AccountConfig": { + "dataSource": "vn" + }, + "LdapConfig": { + "dataSource": "vn" + }, + "MailAlias": { + "dataSource": "vn" + }, + "MailAliasAccount": { + "dataSource": "vn" + }, + "MailConfig": { + "dataSource": "vn" + }, + "MailForward": { + "dataSource": "vn" + }, + "RoleInherit": { + "dataSource": "vn" + }, + "RoleRole": { + "dataSource": "vn" + }, + "SambaConfig": { + "dataSource": "vn" + }, + "UserAccount": { + "dataSource": "vn" + }, + "UserPassword": { + "dataSource": "vn" + }, + "UserSync": { + "dataSource": "vn" + } +} \ No newline at end of file diff --git a/modules/account/back/models/account-config.json b/modules/account/back/models/account-config.json new file mode 100644 index 000000000..a2a405610 --- /dev/null +++ b/modules/account/back/models/account-config.json @@ -0,0 +1,43 @@ +{ + "name": "AccountConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.accountConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "homedir": { + "type": "string", + "required": true + }, + "shell": { + "type": "string", + "required": true + }, + "idBase": { + "type": "number", + "required": true + }, + "min": { + "type": "number", + "required": true + }, + "max": { + "type": "number", + "required": true + }, + "warn": { + "type": "number", + "required": true + }, + "inact": { + "type": "number", + "required": true + } + } +} diff --git a/modules/account/back/models/ldap-config.json b/modules/account/back/models/ldap-config.json new file mode 100644 index 000000000..e3061d651 --- /dev/null +++ b/modules/account/back/models/ldap-config.json @@ -0,0 +1,36 @@ +{ + "name": "LdapConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.ldapConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "host": { + "type": "string", + "required": true + }, + "rdn": { + "type": "string", + "required": true + }, + "password": { + "type": "string", + "required": true + }, + "baseDn": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "groupDn": { + "type": "string" + } + } +} diff --git a/modules/account/back/models/mail-alias-account.json b/modules/account/back/models/mail-alias-account.json new file mode 100644 index 000000000..114d401e0 --- /dev/null +++ b/modules/account/back/models/mail-alias-account.json @@ -0,0 +1,27 @@ +{ + "name": "MailAliasAccount", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.mailAliasAccount" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + } + }, + "relations": { + "alias": { + "type": "belongsTo", + "model": "MailAlias", + "foreignKey": "mailAlias" + }, + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "account" + } + } +} diff --git a/modules/account/back/models/mail-alias.json b/modules/account/back/models/mail-alias.json new file mode 100644 index 000000000..a5970bd3f --- /dev/null +++ b/modules/account/back/models/mail-alias.json @@ -0,0 +1,33 @@ +{ + "name": "MailAlias", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.mailAlias" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "alias": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + }, + "isPublic": { + "type": "boolean" + } + }, + "relations": { + "accounts": { + "type": "hasMany", + "model": "MailAliasAccount", + "foreignKey": "mailAlias", + "property": "id" + } + } +} diff --git a/modules/account/back/models/mail-config.json b/modules/account/back/models/mail-config.json new file mode 100644 index 000000000..1b3d31fd8 --- /dev/null +++ b/modules/account/back/models/mail-config.json @@ -0,0 +1,19 @@ +{ + "name": "MailConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.mailConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "domain": { + "type": "string", + "required": true + } + } +} diff --git a/modules/account/back/models/mail-forward.json b/modules/account/back/models/mail-forward.json new file mode 100644 index 000000000..a3e0eafd9 --- /dev/null +++ b/modules/account/back/models/mail-forward.json @@ -0,0 +1,25 @@ +{ + "name": "MailForward", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.mailForward" + } + }, + "properties": { + "account": { + "id": true + }, + "forwardTo": { + "type": "string", + "required": true + } + }, + "relations": { + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "account" + } + } +} diff --git a/modules/account/back/models/role-inherit.js b/modules/account/back/models/role-inherit.js new file mode 100644 index 000000000..7d31e62b1 --- /dev/null +++ b/modules/account/back/models/role-inherit.js @@ -0,0 +1,22 @@ +const app = require('vn-loopback/server/server'); + +module.exports = Self => { + require('../methods/role-inherit/sync')(Self); + + app.on('started', function() { + let hooks = ['after save', 'after delete']; + for (let hook of hooks) { + Self.observe(hook, async() => { + try { + await Self.rawSql(` + CREATE EVENT account.role_sync + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 5 SECOND + DO CALL role_sync; + `); + } catch (err) { + if (err.code != 'ER_EVENT_ALREADY_EXISTS') throw err; + } + }); + } + }); +}; diff --git a/modules/account/back/models/role-inherit.json b/modules/account/back/models/role-inherit.json new file mode 100644 index 000000000..4b69ffdc2 --- /dev/null +++ b/modules/account/back/models/role-inherit.json @@ -0,0 +1,27 @@ +{ + "name": "RoleInherit", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.roleInherit" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + } + }, + "relations": { + "owner": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "role" + }, + "inherits": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "inheritsFrom" + } + } +} diff --git a/modules/account/back/models/role-role.json b/modules/account/back/models/role-role.json new file mode 100644 index 000000000..f8f16e9e7 --- /dev/null +++ b/modules/account/back/models/role-role.json @@ -0,0 +1,26 @@ +{ + "name": "RoleRole", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.roleRole" + } + }, + "properties": { + "role": { + "id": true + } + }, + "relations": { + "owner": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "role" + }, + "inherits": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "inheritsFrom" + } + } +} diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json new file mode 100644 index 000000000..ffbcce4eb --- /dev/null +++ b/modules/account/back/models/samba-config.json @@ -0,0 +1,25 @@ +{ + "name": "SambaConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.sambaConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true + }, + "host": { + "type": "string", + "required": true + }, + "sshUser": { + "type": "string" + }, + "sshPass": { + "type": "string" + } + } +} diff --git a/modules/account/back/models/user-account.js b/modules/account/back/models/user-account.js new file mode 100644 index 000000000..6fb8fd103 --- /dev/null +++ b/modules/account/back/models/user-account.js @@ -0,0 +1,5 @@ + +module.exports = Self => { + require('../methods/user-account/sync')(Self); + require('../methods/user-account/sync-by-id')(Self); +}; diff --git a/modules/account/back/models/user-account.json b/modules/account/back/models/user-account.json new file mode 100644 index 000000000..fc0526388 --- /dev/null +++ b/modules/account/back/models/user-account.json @@ -0,0 +1,26 @@ +{ + "name": "UserAccount", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.account" + } + }, + "properties": { + "id": { + "id": true + } + }, + "relations": { + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "id" + }, + "aliases": { + "type": "hasMany", + "model": "MailAliasAccount", + "foreignKey": "account" + } + } +} diff --git a/modules/account/back/models/user-password.json b/modules/account/back/models/user-password.json new file mode 100644 index 000000000..1b7e49edd --- /dev/null +++ b/modules/account/back/models/user-password.json @@ -0,0 +1,34 @@ +{ + "name": "UserPassword", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.userPassword" + } + }, + "properties": { + "id": { + "id": true + }, + "length": { + "type": "number", + "required": true + }, + "nAlpha": { + "type": "number", + "required": true + }, + "nUpper": { + "type": "number", + "required": true + }, + "nDigits": { + "type": "number", + "required": true + }, + "nPunct": { + "type": "number", + "required": true + } + } +} diff --git a/modules/account/back/models/user-sync.json b/modules/account/back/models/user-sync.json new file mode 100644 index 000000000..60fd3fb3d --- /dev/null +++ b/modules/account/back/models/user-sync.json @@ -0,0 +1,15 @@ +{ + "name": "UserSync", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.userSync" + } + }, + "properties": { + "name": { + "type": "string", + "id": true + } + } +} diff --git a/modules/account/back/util/ldapjs-extra.js b/modules/account/back/util/ldapjs-extra.js new file mode 100644 index 000000000..381eebb6f --- /dev/null +++ b/modules/account/back/util/ldapjs-extra.js @@ -0,0 +1,30 @@ +const ldap = require('ldapjs'); +const promisifyObject = require('./promisify').promisifyObject; + +module.exports = { + createClient, + Change: ldap.Change +}; + +/** + * Creates a promisified version of LDAP client. + * + * @param {Object} opts Client options + * @return {Client} The promisified LDAP client + */ +function createClient(opts) { + let client = ldap.createClient(opts); + promisifyObject(client, [ + 'bind', + 'add', + 'compare', + 'del', + 'exop', + 'modify', + 'modifyDN', + 'search', + 'starttls', + 'unbind' + ]); + return client; +} diff --git a/modules/account/back/util/promisify.js b/modules/account/back/util/promisify.js new file mode 100644 index 000000000..c23cbb36d --- /dev/null +++ b/modules/account/back/util/promisify.js @@ -0,0 +1,47 @@ + +module.exports = { + promisify, + promisifyObject +}; + +/** + * Promisifies a function wich follows the (err, res) => {} pattern as last + * function argument and returns the promisified version. + * + * @param {Function} fn Function to promisify + * @return {Function} The promisified function + */ +function promisify(fn) { + return function(...args) { + let thisArg = this; + let orgCb = args[args.length - 1]; + if (typeof orgCb !== 'function') orgCb = null; + + return new Promise(function(resolve, reject) { + function cb(err, res) { + if (orgCb) orgCb(err, res); + err ? reject(err) : resolve(res); + } + + if (orgCb) + args[args.length - 1] = cb; + else + args.push(cb); + + fn.apply(thisArg, args); + }); + }; +} + +/** + * Promisifies object methods. + * + * @param {Object} obj Object to promisify + * @param {Array} methods Array of method names to promisify + */ +function promisifyObject(obj, methods) { + for (let method of methods) { + let orgMethod = obj[method]; + obj[method] = promisify(orgMethod); + } +} diff --git a/modules/account/front/acl/create/index.html b/modules/account/front/acl/create/index.html new file mode 100644 index 000000000..96fe5abad --- /dev/null +++ b/modules/account/front/acl/create/index.html @@ -0,0 +1,65 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/account/front/acl/create/index.js b/modules/account/front/acl/create/index.js new file mode 100644 index 000000000..fea71991f --- /dev/null +++ b/modules/account/front/acl/create/index.js @@ -0,0 +1,33 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor(...args) { + super(...args); + this.accessTypes = [ + {name: '*'}, + {name: 'READ'}, + {name: 'WRITE'} + ]; + this.permissions = [ + {name: 'ALLOW'}, + {name: 'DENY'} + ]; + + this.models = []; + for (let model in window.validations) + this.models.push({name: model}); + + this.acl = { + property: '*', + principalType: 'ROLE', + accessType: 'READ', + permission: 'ALLOW' + }; + } +} + +ngModule.component('vnAclCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/acl/index.js b/modules/account/front/acl/index.js new file mode 100644 index 000000000..8393859a5 --- /dev/null +++ b/modules/account/front/acl/index.js @@ -0,0 +1,4 @@ +import './main'; +import './index/'; +import './create'; +import './search-panel'; diff --git a/modules/account/front/acl/index/index.html b/modules/account/front/acl/index/index.html new file mode 100644 index 000000000..af06ec481 --- /dev/null +++ b/modules/account/front/acl/index/index.html @@ -0,0 +1,51 @@ + + + + + +
+ +
{{::row.model}}.{{::row.property}}
+ + + + + + +
+ + + + +
+ + + + + + + + \ No newline at end of file diff --git a/modules/account/front/acl/index/index.js b/modules/account/front/acl/index/index.js new file mode 100644 index 000000000..a2aec534a --- /dev/null +++ b/modules/account/front/acl/index/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onDelete(row) { + return this.$http.delete(`ACLs/${row.id}`) + .then(() => this.$.model.refresh()) + .then(() => this.vnApp.showSuccess(this.$t('ACL removed'))); + } +} + +ngModule.component('vnAclIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/acl/index/locale/es.yml b/modules/account/front/acl/index/locale/es.yml new file mode 100644 index 000000000..8024f804c --- /dev/null +++ b/modules/account/front/acl/index/locale/es.yml @@ -0,0 +1,4 @@ +New ACL: Nuevo ACL +Edit ACL: Editar ACL +ACL will be removed: El ACL será eliminado +ACL removed: ACL eliminado diff --git a/modules/account/front/acl/locale/es.yml b/modules/account/front/acl/locale/es.yml new file mode 100644 index 000000000..ff6a1b41c --- /dev/null +++ b/modules/account/front/acl/locale/es.yml @@ -0,0 +1,4 @@ +Model: Modelo +Property: Propiedad +Access type: Tipo de acceso +Permission: Permiso \ No newline at end of file diff --git a/modules/account/front/acl/main/index.html b/modules/account/front/acl/main/index.html new file mode 100644 index 000000000..7767768d9 --- /dev/null +++ b/modules/account/front/acl/main/index.html @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/acl/main/index.js b/modules/account/front/acl/main/index.js new file mode 100644 index 000000000..a91a71cb7 --- /dev/null +++ b/modules/account/front/acl/main/index.js @@ -0,0 +1,18 @@ +import ngModule from '../../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class ACL extends ModuleMain { + exprBuilder(param, value) { + switch (param) { + case 'search': + return {model: {like: `%${value}%`}}; + default: + return {[param]: value}; + } + } +} + +ngModule.vnComponent('vnAclComponent', { + controller: ACL, + template: require('./index.html') +}); diff --git a/modules/account/front/acl/search-panel/index.html b/modules/account/front/acl/search-panel/index.html new file mode 100644 index 000000000..b83b9c255 --- /dev/null +++ b/modules/account/front/acl/search-panel/index.html @@ -0,0 +1,39 @@ +
+
+ + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/modules/account/front/acl/search-panel/index.js b/modules/account/front/acl/search-panel/index.js new file mode 100644 index 000000000..4f571059e --- /dev/null +++ b/modules/account/front/acl/search-panel/index.js @@ -0,0 +1,26 @@ +import ngModule from '../../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +export default class Controller extends SearchPanel { + constructor(...args) { + super(...args); + this.accessTypes = [ + {name: '*'}, + {name: 'READ'}, + {name: 'WRITE'} + ]; + this.permissions = [ + {name: 'ALLOW'}, + {name: 'DENY'} + ]; + + this.models = []; + for (let model in window.validations) + this.models.push({name: model}); + } +} + +ngModule.component('vnAclSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/alias/basic-data/index.html b/modules/account/front/alias/basic-data/index.html new file mode 100644 index 000000000..ede77f929 --- /dev/null +++ b/modules/account/front/alias/basic-data/index.html @@ -0,0 +1,44 @@ + + +
+ + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/modules/account/front/alias/basic-data/index.js b/modules/account/front/alias/basic-data/index.js new file mode 100644 index 000000000..b7c2db089 --- /dev/null +++ b/modules/account/front/alias/basic-data/index.js @@ -0,0 +1,12 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnAliasBasicData', { + template: require('./index.html'), + controller: Controller, + bindings: { + alias: '<' + } +}); diff --git a/modules/account/front/alias/card/index.html b/modules/account/front/alias/card/index.html new file mode 100644 index 000000000..712147a24 --- /dev/null +++ b/modules/account/front/alias/card/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/account/front/alias/card/index.js b/modules/account/front/alias/card/index.js new file mode 100644 index 000000000..fd1a18f6a --- /dev/null +++ b/modules/account/front/alias/card/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import ModuleCard from 'salix/components/module-card'; + +class Controller extends ModuleCard { + reload() { + this.$http.get(`MailAliases/${this.$params.id}`) + .then(res => this.alias = res.data); + } +} + +ngModule.vnComponent('vnAliasCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/alias/create/index.html b/modules/account/front/alias/create/index.html new file mode 100644 index 000000000..dee59d26e --- /dev/null +++ b/modules/account/front/alias/create/index.html @@ -0,0 +1,33 @@ + + +
+ + + + + + + + + + +
diff --git a/modules/account/front/alias/create/index.js b/modules/account/front/alias/create/index.js new file mode 100644 index 000000000..c058c3adf --- /dev/null +++ b/modules/account/front/alias/create/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + return this.$.watcher.submit().then(res => + this.$state.go('account.alias.card.basicData', {id: res.data.id}) + ); + } +} + +ngModule.component('vnAliasCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/alias/descriptor/index.html b/modules/account/front/alias/descriptor/index.html new file mode 100644 index 000000000..71b98c6a3 --- /dev/null +++ b/modules/account/front/alias/descriptor/index.html @@ -0,0 +1,27 @@ + + + + Delete + + + +
+ + +
+
+
+ + \ No newline at end of file diff --git a/modules/account/front/alias/descriptor/index.js b/modules/account/front/alias/descriptor/index.js new file mode 100644 index 000000000..a21baae5a --- /dev/null +++ b/modules/account/front/alias/descriptor/index.js @@ -0,0 +1,26 @@ +import ngModule from '../../module'; +import Descriptor from 'salix/components/descriptor'; + +class Controller extends Descriptor { + get alias() { + return this.entity; + } + + set alias(value) { + this.entity = value; + } + + onDelete() { + return this.$http.delete(`MailAliases/${this.id}`) + .then(() => this.$state.go('account.alias')) + .then(() => this.vnApp.showSuccess(this.$t('Alias removed'))); + } +} + +ngModule.component('vnAliasDescriptor', { + template: require('./index.html'), + controller: Controller, + bindings: { + alias: '<' + } +}); diff --git a/modules/account/front/alias/descriptor/locale/es.yml b/modules/account/front/alias/descriptor/locale/es.yml new file mode 100644 index 000000000..9c6fa0e73 --- /dev/null +++ b/modules/account/front/alias/descriptor/locale/es.yml @@ -0,0 +1,2 @@ +Alias will be removed: El alias será eliminado +Alias removed: Alias eliminado \ No newline at end of file diff --git a/modules/account/front/alias/index.js b/modules/account/front/alias/index.js new file mode 100644 index 000000000..8eed3a3d3 --- /dev/null +++ b/modules/account/front/alias/index.js @@ -0,0 +1,9 @@ +import './main'; +import './index/'; +import './create'; +import './summary'; +import './card'; +import './descriptor'; +import './create'; +import './basic-data'; +import './users'; diff --git a/modules/account/front/alias/index/index.html b/modules/account/front/alias/index/index.html new file mode 100644 index 000000000..d140973ba --- /dev/null +++ b/modules/account/front/alias/index/index.html @@ -0,0 +1,39 @@ + + + + + + + +
{{::alias.alias}}
+
{{::alias.description}}
+
+ + + + +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/modules/account/front/alias/index/index.js b/modules/account/front/alias/index/index.js new file mode 100644 index 000000000..44e146fb4 --- /dev/null +++ b/modules/account/front/alias/index/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + preview(alias) { + this.selectedAlias = alias; + this.$.summary.show(); + } +} + +ngModule.component('vnAliasIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/alias/index/locale/es.yml b/modules/account/front/alias/index/locale/es.yml new file mode 100644 index 000000000..4df41c0be --- /dev/null +++ b/modules/account/front/alias/index/locale/es.yml @@ -0,0 +1,2 @@ +New alias: Nuevo alias +View alias: Ver alias \ No newline at end of file diff --git a/modules/account/front/alias/locale/es.yml b/modules/account/front/alias/locale/es.yml new file mode 100644 index 000000000..ecc856fcf --- /dev/null +++ b/modules/account/front/alias/locale/es.yml @@ -0,0 +1 @@ +Public: Público \ No newline at end of file diff --git a/modules/account/front/alias/main/index.html b/modules/account/front/alias/main/index.html new file mode 100644 index 000000000..43f6e2f51 --- /dev/null +++ b/modules/account/front/alias/main/index.html @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/alias/main/index.js b/modules/account/front/alias/main/index.js new file mode 100644 index 000000000..21eed3d85 --- /dev/null +++ b/modules/account/front/alias/main/index.js @@ -0,0 +1,18 @@ +import ngModule from '../../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class Alias extends ModuleMain { + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {alias: {like: `%${value}%`}}; + } + } +} + +ngModule.vnComponent('vnAlias', { + controller: Alias, + template: require('./index.html') +}); diff --git a/modules/account/front/alias/summary/index.html b/modules/account/front/alias/summary/index.html new file mode 100644 index 000000000..52ee2813d --- /dev/null +++ b/modules/account/front/alias/summary/index.html @@ -0,0 +1,16 @@ + +
{{summary.alias}}
+ + +

Basic data

+ + + + +
+
+
\ No newline at end of file diff --git a/modules/account/front/alias/summary/index.js b/modules/account/front/alias/summary/index.js new file mode 100644 index 000000000..21bc8d9ba --- /dev/null +++ b/modules/account/front/alias/summary/index.js @@ -0,0 +1,25 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + set alias(value) { + this._alias = value; + this.$.summary = null; + if (!value) return; + + this.$http.get(`MailAliases/${value.id}`) + .then(res => this.$.summary = res.data); + } + + get alias() { + return this._alias; + } +} + +ngModule.component('vnAliasSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + alias: '<' + } +}); diff --git a/modules/account/front/alias/users/index.html b/modules/account/front/alias/users/index.html new file mode 100644 index 000000000..048a702ea --- /dev/null +++ b/modules/account/front/alias/users/index.html @@ -0,0 +1,26 @@ + + + + + + {{::row.user.name}} + + + + + + + + + + + diff --git a/modules/account/front/alias/users/index.js b/modules/account/front/alias/users/index.js new file mode 100644 index 000000000..b2446d71b --- /dev/null +++ b/modules/account/front/alias/users/index.js @@ -0,0 +1,31 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + let filter = { + include: { + relation: 'user', + scope: { + fields: ['id', 'name'] + } + } + }; + this.$http.get(`MailAliases/${this.$params.id}/accounts`, {filter}) + .then(res => this.$.data = res.data); + } + + onRemove(row) { + return this.$http.delete(`MailAliases/${this.$params.id}/accounts/${row.id}`) + .then(() => { + let index = this.$.data.indexOf(row); + if (index !== -1) this.$.data.splice(index, 1); + this.vnApp.showSuccess(this.$t('User removed')); + }); + } +} + +ngModule.component('vnAliasUsers', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/alias/users/index.spec.js b/modules/account/front/alias/users/index.spec.js new file mode 100644 index 000000000..d618f1de1 --- /dev/null +++ b/modules/account/front/alias/users/index.spec.js @@ -0,0 +1,42 @@ +import './index'; + +describe('component vnAliasUsers', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnAliasUsers', {$element: null}); + controller.$params.id = 1; + })); + + describe('$onInit()', () => { + it('should delete entity and go to index', () => { + $httpBackend.expectGET('MailAliases/1/accounts').respond('foo'); + controller.$onInit(); + $httpBackend.flush(); + + expect(controller.$.data).toBe('foo'); + }); + }); + + describe('onRemove()', () => { + it('should call backend method to change role', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.$.data = [ + {id: 1, alias: 'foo'}, + {id: 2, alias: 'bar'} + ]; + + $httpBackend.expectDELETE('MailAliases/1/accounts/1').respond(); + controller.onRemove(controller.$.data[0]); + $httpBackend.flush(); + + expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/account/front/alias/users/locale/es.yml b/modules/account/front/alias/users/locale/es.yml new file mode 100644 index 000000000..dc24eb318 --- /dev/null +++ b/modules/account/front/alias/users/locale/es.yml @@ -0,0 +1,2 @@ +User will be removed from alias: El usuario será borrado del alias +User removed: Usuario borrado \ No newline at end of file diff --git a/modules/account/front/aliases/index.html b/modules/account/front/aliases/index.html new file mode 100644 index 000000000..9f4ba857f --- /dev/null +++ b/modules/account/front/aliases/index.html @@ -0,0 +1,64 @@ +
+ + + + + +
+ {{::row.alias.alias}} +
+
+ {{::row.alias.description}} +
+
+ + + + +
+
+ +
+
+ + + + + + + + + + + + + + +
+
+ Account not enabled +
diff --git a/modules/account/front/aliases/index.js b/modules/account/front/aliases/index.js new file mode 100644 index 000000000..0fc806a71 --- /dev/null +++ b/modules/account/front/aliases/index.js @@ -0,0 +1,51 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + this.refresh(); + } + + refresh() { + let filter = { + where: {account: this.$params.id}, + include: { + relation: 'alias', + scope: { + fields: ['id', 'alias', 'description'] + } + } + }; + return this.$http.get(`MailAliasAccounts`, {filter}) + .then(res => this.$.data = res.data); + } + + onAddClick() { + this.addData = {account: this.$params.id}; + this.$.dialog.show(); + } + + onAddSave() { + return this.$http.post(`MailAliasAccounts`, this.addData) + .then(() => this.refresh()) + .then(() => this.vnApp.showSuccess( + this.$t('Subscribed to alias!')) + ); + } + + onRemove(row) { + return this.$http.delete(`MailAliasAccounts/${row.id}`) + .then(() => { + this.$.data.splice(this.$.data.indexOf(row), 1); + this.vnApp.showSuccess(this.$t('Unsubscribed from alias!')); + }); + } +} + +ngModule.component('vnUserAliases', { + template: require('./index.html'), + controller: Controller, + require: { + card: '^vnUserCard' + } +}); diff --git a/modules/account/front/aliases/index.spec.js b/modules/account/front/aliases/index.spec.js new file mode 100644 index 000000000..466f1e1e9 --- /dev/null +++ b/modules/account/front/aliases/index.spec.js @@ -0,0 +1,53 @@ +import './index'; + +describe('component vnUserAliases', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnUserAliases', {$element: null}); + jest.spyOn(controller.vnApp, 'showSuccess'); + })); + + describe('refresh()', () => { + it('should refresh the controller data', () => { + $httpBackend.expectGET('MailAliasAccounts').respond('foo'); + controller.refresh(); + $httpBackend.flush(); + + expect(controller.$.data).toBe('foo'); + }); + }); + + describe('onAddSave()', () => { + it('should add the new row', () => { + controller.addData = {account: 1}; + + $httpBackend.expectPOST('MailAliasAccounts').respond(); + $httpBackend.expectGET('MailAliasAccounts').respond('foo'); + controller.onAddSave(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('onRemove()', () => { + it('shoud remove the passed row remote and locally', () => { + controller.$.data = [ + {id: 1, alias: 'foo'}, + {id: 2, alias: 'bar'} + ]; + + $httpBackend.expectDELETE('MailAliasAccounts/1').respond(); + controller.onRemove(controller.$.data[0]); + $httpBackend.flush(); + + expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/account/front/aliases/locale/es.yml b/modules/account/front/aliases/locale/es.yml new file mode 100644 index 000000000..4d1ad76a7 --- /dev/null +++ b/modules/account/front/aliases/locale/es.yml @@ -0,0 +1,3 @@ +Unsubscribe: Desuscribir +Subscribed to alias!: ¡Suscrito al alias! +Unsubscribed from alias!: ¡Desuscrito del alias! \ No newline at end of file diff --git a/modules/account/front/basic-data/index.html b/modules/account/front/basic-data/index.html new file mode 100644 index 000000000..d21dda45a --- /dev/null +++ b/modules/account/front/basic-data/index.html @@ -0,0 +1,52 @@ + + +
+ + + + + + + + + + + + + + + + + + +
diff --git a/modules/account/front/basic-data/index.js b/modules/account/front/basic-data/index.js new file mode 100644 index 000000000..243a46068 --- /dev/null +++ b/modules/account/front/basic-data/index.js @@ -0,0 +1,20 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + this.$.watcher.submit() + .then(() => this.card.reload()); + } +} + +ngModule.component('vnUserBasicData', { + template: require('./index.html'), + controller: Controller, + require: { + card: '^vnUserCard' + }, + bindings: { + user: '<' + } +}); diff --git a/modules/account/front/card/index.html b/modules/account/front/card/index.html new file mode 100644 index 000000000..cba6b93c6 --- /dev/null +++ b/modules/account/front/card/index.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/modules/account/front/card/index.js b/modules/account/front/card/index.js new file mode 100644 index 000000000..5266592f3 --- /dev/null +++ b/modules/account/front/card/index.js @@ -0,0 +1,28 @@ +import ngModule from '../module'; +import ModuleCard from 'salix/components/module-card'; +import './style.scss'; + +class Controller extends ModuleCard { + reload() { + const filter = { + include: { + relation: 'role', + scope: { + fields: ['id', 'name'] + } + } + }; + + return Promise.all([ + this.$http.get(`Accounts/${this.$params.id}`, {filter}) + .then(res => this.user = res.data), + this.$http.get(`UserAccounts/${this.$params.id}/exists`) + .then(res => this.hasAccount = res.data.exists) + ]); + } +} + +ngModule.vnComponent('vnUserCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/card/index.spec.js b/modules/account/front/card/index.spec.js new file mode 100644 index 000000000..cd28c458a --- /dev/null +++ b/modules/account/front/card/index.spec.js @@ -0,0 +1,27 @@ +import './index'; + +describe('component vnUserCard', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnUserCard', {$element: null}); + })); + + describe('reload()', () => { + it('should reload the controller data', () => { + controller.$params.id = 1; + + $httpBackend.expectGET('Accounts/1').respond('foo'); + $httpBackend.expectGET('UserAccounts/1/exists').respond({exists: true}); + controller.reload(); + $httpBackend.flush(); + + expect(controller.user).toBe('foo'); + expect(controller.hasAccount).toBeTruthy(); + }); + }); +}); diff --git a/modules/account/front/card/style.scss b/modules/account/front/card/style.scss new file mode 100644 index 000000000..4d9d108a0 --- /dev/null +++ b/modules/account/front/card/style.scss @@ -0,0 +1,10 @@ +@import "variables"; + +.bg-title { + display: block; + text-align: center; + padding: 24px; + box-sizing: border-box; + color: $color-font-secondary; + font-size: 1.375rem; +} diff --git a/modules/account/front/connections/index.html b/modules/account/front/connections/index.html new file mode 100644 index 000000000..d634b7a9f --- /dev/null +++ b/modules/account/front/connections/index.html @@ -0,0 +1,45 @@ + + + + + + + +
{{::row.user.username}}
+
{{::row.created | date:'dd/MM HH:mm'}}
+
+ + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/modules/account/front/connections/index.js b/modules/account/front/connections/index.js new file mode 100644 index 000000000..c4ddd5615 --- /dev/null +++ b/modules/account/front/connections/index.js @@ -0,0 +1,29 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor(...args) { + super(...args); + this.filter = { + fields: ['id', 'created', 'userId'], + include: { + relation: 'user', + scope: { + fields: ['username'] + } + }, + order: 'created DESC' + }; + } + + onDisconnect(row) { + return this.$http.delete(`AccessTokens/${row.id}`) + .then(() => this.$.model.refresh()) + .then(() => this.vnApp.showSuccess(this.$t('Session killed'))); + } +} + +ngModule.component('vnConnections', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/connections/locale/es.yml b/modules/account/front/connections/locale/es.yml new file mode 100644 index 000000000..41ef18b45 --- /dev/null +++ b/modules/account/front/connections/locale/es.yml @@ -0,0 +1,5 @@ +Go to user: Ir al usuario +Refresh: Actualizar +Session will be killed: Se va a matar la sesión +Kill session: Matar sesión +Session killed: Sesión matada \ No newline at end of file diff --git a/modules/account/front/create/index.html b/modules/account/front/create/index.html new file mode 100644 index 000000000..407ac0e3c --- /dev/null +++ b/modules/account/front/create/index.html @@ -0,0 +1,53 @@ + + +
+ + + + + + + + + + + + + + + + + + +
diff --git a/modules/account/front/create/index.js b/modules/account/front/create/index.js new file mode 100644 index 000000000..41fd718f6 --- /dev/null +++ b/modules/account/front/create/index.js @@ -0,0 +1,15 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + return this.$.watcher.submit().then(res => { + this.$state.go('account.card.basicData', {id: res.data.id}); + }); + } +} + +ngModule.component('vnUserCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/descriptor/__snapshots__/index.spec.js.snap b/modules/account/front/descriptor/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..de5f8e8c2 --- /dev/null +++ b/modules/account/front/descriptor/__snapshots__/index.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component vnUserDescriptor onPassChange() should throw an error when password is empty 1`] = `"You must enter a new password"`; + +exports[`component vnUserDescriptor onPassChange() should throw an error when repeat password not matches new password 1`] = `"Passwords don't match"`; diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html new file mode 100644 index 000000000..88b1a9c6d --- /dev/null +++ b/modules/account/front/descriptor/index.html @@ -0,0 +1,164 @@ + + + + Delete + + + Change role + + + Change password + + + Set password + + + Enable account + + + Disable account + + + Activate user + + + Deactivate user + + + +
+ + + + +
+
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js new file mode 100644 index 000000000..3f27b1f76 --- /dev/null +++ b/modules/account/front/descriptor/index.js @@ -0,0 +1,123 @@ +import ngModule from '../module'; +import Descriptor from 'salix/components/descriptor'; +import UserError from 'core/lib/user-error'; + +class Controller extends Descriptor { + get user() { + return this.entity; + } + + set user(value) { + this.entity = value; + } + + get entity() { + return super.entity; + } + + set entity(value) { + super.entity = value; + this.hasAccount = null; + if (!value) return; + + this.$http.get(`UserAccounts/${value.id}/exists`) + .then(res => this.hasAccount = res.data.exists); + } + + onDelete() { + return this.$http.delete(`Accounts/${this.id}`) + .then(() => this.$state.go('account.index')) + .then(() => this.vnApp.showSuccess(this.$t('User removed'))); + } + + onChangeRole() { + this.newRole = this.user.role.id; + this.$.changeRole.show(); + } + + onChangeRoleAccept() { + const params = {roleFk: this.newRole}; + return this.$http.patch(`Accounts/${this.id}`, params) + .then(() => { + this.emit('change'); + this.vnApp.showSuccess(this.$t('Role changed succesfully!')); + }); + } + + onChangePassClick(askOldPass) { + this.$http.get('UserPasswords/findOne') + .then(res => { + this.passRequirements = res.data; + this.askOldPass = askOldPass; + this.$.changePass.show(); + }); + } + + onPassChange() { + if (!this.newPassword) + throw new UserError(`You must enter a new password`); + if (this.newPassword != this.repeatPassword) + throw new UserError(`Passwords don't match`); + + let method; + const params = {newPassword: this.newPassword}; + + if (this.askOldPass) { + method = 'changePassword'; + params.oldPassword = this.oldPassword; + } else + method = 'setPassword'; + + return this.$http.patch(`Accounts/${this.id}/${method}`, params) + .then(() => { + this.emit('change'); + this.vnApp.showSuccess(this.$t('Password changed succesfully!')); + }); + } + + onPassClose() { + this.oldPassword = ''; + this.newPassword = ''; + this.repeatPassword = ''; + this.$.$apply(); + } + + onEnableAccount() { + return this.$http.post(`UserAccounts`, {id: this.id}) + .then(() => this.onSwitchAccount(true)); + } + + onDisableAccount() { + return this.$http.delete(`UserAccounts/${this.id}`) + .then(() => this.onSwitchAccount(false)); + } + + onSwitchAccount(enable) { + this.hasAccount = enable; + const message = enable + ? 'Account enabled!' + : 'Account disabled!'; + this.emit('change'); + this.vnApp.showSuccess(this.$t(message)); + } + + onSetActive(active) { + return this.$http.patch(`Accounts/${this.id}`, {active}) + .then(() => { + this.user.active = active; + const message = active + ? 'User activated!' + : 'User deactivated!'; + this.emit('change'); + this.vnApp.showSuccess(this.$t(message)); + }); + } +} + +ngModule.component('vnUserDescriptor', { + template: require('./index.html'), + controller: Controller, + bindings: { + user: '<' + } +}); diff --git a/modules/account/front/descriptor/index.spec.js b/modules/account/front/descriptor/index.spec.js new file mode 100644 index 000000000..8ee67a304 --- /dev/null +++ b/modules/account/front/descriptor/index.spec.js @@ -0,0 +1,108 @@ +import './index'; + +describe('component vnUserDescriptor', () => { + let controller; + let $httpBackend; + + let user = {id: 1, name: 'foo'}; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + $httpBackend.whenGET('UserAccounts/1/exists').respond({exists: true}); + + controller = $componentController('vnUserDescriptor', {$element: null}, {user}); + jest.spyOn(controller, 'emit'); + jest.spyOn(controller.vnApp, 'showSuccess'); + })); + + describe('onDelete()', () => { + it('should delete entity and go to index', () => { + controller.$state.go = jest.fn(); + + $httpBackend.expectDELETE('Accounts/1').respond(); + controller.onDelete(); + $httpBackend.flush(); + + expect(controller.$state.go).toHaveBeenCalledWith('account.index'); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('onChangeRoleAccept()', () => { + it('should call backend method to change role', () => { + $httpBackend.expectPATCH('Accounts/1').respond(); + controller.onChangeRoleAccept(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.emit).toHaveBeenCalledWith('change'); + }); + }); + + describe('onPassChange()', () => { + it('should throw an error when password is empty', () => { + expect(() => { + controller.onPassChange(); + }).toThrowErrorMatchingSnapshot(); + }); + + it('should throw an error when repeat password not matches new password', () => { + controller.newPassword = 'foo'; + controller.repeatPassword = 'bar'; + + expect(() => { + controller.onPassChange(); + }).toThrowErrorMatchingSnapshot(); + }); + + it('should make a request when password checks passes', () => { + controller.newPassword = 'foo'; + controller.repeatPassword = 'foo'; + + $httpBackend.expectPATCH('Accounts/1/setPassword').respond(); + controller.onPassChange(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.emit).toHaveBeenCalledWith('change'); + }); + }); + + describe('onEnableAccount()', () => { + it('should make request to enable account', () => { + $httpBackend.expectPOST('UserAccounts', {id: 1}).respond(); + controller.onEnableAccount(); + $httpBackend.flush(); + + expect(controller.hasAccount).toBeTruthy(); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.emit).toHaveBeenCalledWith('change'); + }); + }); + + describe('onDisableAccount()', () => { + it('should make request to disable account', () => { + $httpBackend.expectDELETE('UserAccounts/1').respond(); + controller.onDisableAccount(); + $httpBackend.flush(); + + expect(controller.hasAccount).toBeFalsy(); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.emit).toHaveBeenCalledWith('change'); + }); + }); + + describe('onSetActive()', () => { + it('should make request to activate/deactivate the user', () => { + $httpBackend.expectPATCH('Accounts/1', {active: true}).respond(); + controller.onSetActive(true); + $httpBackend.flush(); + + expect(controller.user.active).toBeTruthy(); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.emit).toHaveBeenCalledWith('change'); + }); + }); +}); diff --git a/modules/account/front/descriptor/locale/es.yml b/modules/account/front/descriptor/locale/es.yml new file mode 100644 index 000000000..5e8242819 --- /dev/null +++ b/modules/account/front/descriptor/locale/es.yml @@ -0,0 +1,31 @@ +User will be removed: El usuario será eliminado +User removed: Usuario eliminado +Are you sure you want to continue?: ¿Seguro que quieres continuar? +Account will be enabled: La cuenta será habilitada +Account will be disabled: La cuenta será deshabilitada +Account enabled!: ¡Cuenta habilitada! +Account disabled!: ¡Cuenta deshabilitada! +User will activated: El usuario será activado +User will be deactivated: El usuario será desactivado +User activated!: ¡Usuario activado! +User deactivated!: ¡Usuario desactivado! +Account enabled: Cuenta habilitada +User deactivated: Usuario desactivado +Change role: Modificar rol +Change password: Cambiar contraseña +Set password: Establecer contraseña +Enable account: Habilitar cuenta +Disable account: Deshabilitar cuenta +Activate user: Activar usuario +Deactivate user: Desactivar usuario +Old password: Contraseña antigua +New password: Nueva contraseña +Repeat password: Repetir contraseña +Password changed succesfully!: ¡Contraseña modificada correctamente! +Role changed succesfully!: ¡Rol modificado correctamente! +Password requirements: > + La contraseña debe tener al menos {{ length }} caracteres de longitud, + {{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}} + dígitos y {{nPunct}} símbolos (Ej: $%&.) +You must enter a new password: Debes introducir la nueva contraseña +Passwords don't match: Las contraseñas no coinciden diff --git a/modules/account/front/index.js b/modules/account/front/index.js new file mode 100644 index 000000000..359fdff72 --- /dev/null +++ b/modules/account/front/index.js @@ -0,0 +1,17 @@ +export * from './module'; + +import './main'; +import './index/'; +import './role'; +import './alias'; +import './connections'; +import './acl'; +import './summary'; +import './card'; +import './descriptor'; +import './search-panel'; +import './create'; +import './basic-data'; +import './mail-forwarding'; +import './aliases'; +import './roles'; diff --git a/modules/account/front/index/index.html b/modules/account/front/index/index.html new file mode 100644 index 000000000..3c967250d --- /dev/null +++ b/modules/account/front/index/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/index/index.js b/modules/account/front/index/index.js new file mode 100644 index 000000000..9324ca740 --- /dev/null +++ b/modules/account/front/index/index.js @@ -0,0 +1,14 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + preview(user) { + this.selectedUser = user; + this.$.summary.show(); + } +} + +ngModule.component('vnUserIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/index/locale/es.yml b/modules/account/front/index/locale/es.yml new file mode 100644 index 000000000..074fb054e --- /dev/null +++ b/modules/account/front/index/locale/es.yml @@ -0,0 +1,2 @@ +New user: Nuevo usuario +View user: Ver usuario \ No newline at end of file diff --git a/modules/account/front/locale/es.yml b/modules/account/front/locale/es.yml new file mode 100644 index 000000000..02c58a8b0 --- /dev/null +++ b/modules/account/front/locale/es.yml @@ -0,0 +1,10 @@ +Active: Activo +Connections: Conexiones +Description: Descripción +Name: Nombre +Nickname: Nombre mostrado +Personal email: Correo personal +Role: Rol +Mail aliases: Alias de correo +Account not enabled: Cuenta no habilitada +Inherited roles: Roles heredados diff --git a/modules/account/front/mail-forwarding/index.html b/modules/account/front/mail-forwarding/index.html new file mode 100644 index 000000000..a6be2782a --- /dev/null +++ b/modules/account/front/mail-forwarding/index.html @@ -0,0 +1,43 @@ +
+ + +
+ + + + + + + + + + +
+
+
+ Account not enabled +
diff --git a/modules/account/front/mail-forwarding/index.js b/modules/account/front/mail-forwarding/index.js new file mode 100644 index 000000000..5118e8eab --- /dev/null +++ b/modules/account/front/mail-forwarding/index.js @@ -0,0 +1,12 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnUserMailForwarding', { + template: require('./index.html'), + controller: Controller, + require: { + card: '^vnUserCard' + }, +}); diff --git a/modules/account/front/mail-forwarding/locale/es.yml b/modules/account/front/mail-forwarding/locale/es.yml new file mode 100644 index 000000000..0322e3e42 --- /dev/null +++ b/modules/account/front/mail-forwarding/locale/es.yml @@ -0,0 +1,6 @@ +Mail forwarding: Reenvío de correo +Forward email: Dirección de reenvío +Enable mail forwarding: Habilitar redirección de correo +All emails will be forwarded to the specified address.: > + Todos los correos serán reenviados a la dirección especificada, no se + mantendrá copia de los mismos en el buzón del usuario. diff --git a/modules/account/front/main/index.html b/modules/account/front/main/index.html new file mode 100644 index 000000000..5736b3a3b --- /dev/null +++ b/modules/account/front/main/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/main/index.js b/modules/account/front/main/index.js new file mode 100644 index 000000000..a43ffb76b --- /dev/null +++ b/modules/account/front/main/index.js @@ -0,0 +1,39 @@ +import ngModule from '../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class User extends ModuleMain { + constructor($element, $) { + super($element, $); + this.filter = { + fields: ['id', 'nickname', 'name', 'role'], + include: { + relation: 'role', + scope: { + fields: ['id', 'name'] + } + } + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {or: [ + {name: {like: `%${value}%`}}, + {nickname: {like: `%${value}%`}} + ]}; + case 'name': + case 'nickname': + return {[param]: {like: `%${value}%`}}; + case 'roleFk': + return {[param]: value}; + } + } +} + +ngModule.vnComponent('vnUser', { + controller: User, + template: require('./index.html') +}); diff --git a/modules/account/front/main/index.spec.js b/modules/account/front/main/index.spec.js new file mode 100644 index 000000000..c232aa849 --- /dev/null +++ b/modules/account/front/main/index.spec.js @@ -0,0 +1,28 @@ +import './index'; + +describe('component vnUser', () => { + let controller; + + beforeEach(ngModule('account')); + + beforeEach(inject($componentController => { + controller = $componentController('vnUser', {$element: null}); + })); + + describe('exprBuilder()', () => { + it('should search by id when only digits string is passed', () => { + let expr = controller.exprBuilder('search', '1'); + + expect(expr).toEqual({id: '1'}); + }); + + it('should search by name when non-only digits string is passed', () => { + let expr = controller.exprBuilder('search', '1foo'); + + expect(expr).toEqual({or: [ + {name: {like: '%1foo%'}}, + {nickname: {like: '%1foo%'}} + ]}); + }); + }); +}); diff --git a/modules/account/front/module.js b/modules/account/front/module.js new file mode 100644 index 000000000..0002f0b7a --- /dev/null +++ b/modules/account/front/module.js @@ -0,0 +1,3 @@ +import {ng} from 'core/vendor'; + +export default ng.module('account', ['vnCore']); diff --git a/modules/account/front/role/basic-data/index.html b/modules/account/front/role/basic-data/index.html new file mode 100644 index 000000000..e79601dcc --- /dev/null +++ b/modules/account/front/role/basic-data/index.html @@ -0,0 +1,39 @@ + + +
+ + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/modules/account/front/role/basic-data/index.js b/modules/account/front/role/basic-data/index.js new file mode 100644 index 000000000..4e26906ee --- /dev/null +++ b/modules/account/front/role/basic-data/index.js @@ -0,0 +1,12 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnRoleBasicData', { + template: require('./index.html'), + controller: Controller, + bindings: { + role: '<' + } +}); diff --git a/modules/account/front/role/card/index.html b/modules/account/front/role/card/index.html new file mode 100644 index 000000000..2f51f88b5 --- /dev/null +++ b/modules/account/front/role/card/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/account/front/role/card/index.js b/modules/account/front/role/card/index.js new file mode 100644 index 000000000..6f888211d --- /dev/null +++ b/modules/account/front/role/card/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import ModuleCard from 'salix/components/module-card'; + +class Controller extends ModuleCard { + reload() { + this.$http.get(`Roles/${this.$params.id}`) + .then(res => this.role = res.data); + } +} + +ngModule.vnComponent('vnRoleCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/role/card/index.spec.js b/modules/account/front/role/card/index.spec.js new file mode 100644 index 000000000..f39840e5f --- /dev/null +++ b/modules/account/front/role/card/index.spec.js @@ -0,0 +1,25 @@ +import './index'; + +describe('component vnRoleCard', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnRoleCard', {$element: null}); + })); + + describe('reload()', () => { + it('should reload the controller data', () => { + controller.$params.id = 1; + + $httpBackend.expectGET('Roles/1').respond('foo'); + controller.reload(); + $httpBackend.flush(); + + expect(controller.role).toBe('foo'); + }); + }); +}); diff --git a/modules/account/front/role/create/index.html b/modules/account/front/role/create/index.html new file mode 100644 index 000000000..f610f6d23 --- /dev/null +++ b/modules/account/front/role/create/index.html @@ -0,0 +1,33 @@ + + +
+ + + + + + + + + + +
diff --git a/modules/account/front/role/create/index.js b/modules/account/front/role/create/index.js new file mode 100644 index 000000000..3f7fcc9cf --- /dev/null +++ b/modules/account/front/role/create/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + return this.$.watcher.submit().then(res => + this.$state.go('account.role.card.basicData', {id: res.data.id}) + ); + } +} + +ngModule.component('vnRoleCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/role/descriptor/index.html b/modules/account/front/role/descriptor/index.html new file mode 100644 index 000000000..4cd4ac822 --- /dev/null +++ b/modules/account/front/role/descriptor/index.html @@ -0,0 +1,27 @@ + + + + Delete + + + +
+ + +
+
+
+ + \ No newline at end of file diff --git a/modules/account/front/role/descriptor/index.js b/modules/account/front/role/descriptor/index.js new file mode 100644 index 000000000..a1b578133 --- /dev/null +++ b/modules/account/front/role/descriptor/index.js @@ -0,0 +1,26 @@ +import ngModule from '../../module'; +import Descriptor from 'salix/components/descriptor'; + +class Controller extends Descriptor { + get role() { + return this.entity; + } + + set role(value) { + this.entity = value; + } + + onDelete() { + return this.$http.delete(`Roles/${this.id}`) + .then(() => this.$state.go('account.role')) + .then(() => this.vnApp.showSuccess(this.$t('Role removed'))); + } +} + +ngModule.component('vnRoleDescriptor', { + template: require('./index.html'), + controller: Controller, + bindings: { + role: '<' + } +}); diff --git a/modules/account/front/role/descriptor/index.spec.js b/modules/account/front/role/descriptor/index.spec.js new file mode 100644 index 000000000..e2761c639 --- /dev/null +++ b/modules/account/front/role/descriptor/index.spec.js @@ -0,0 +1,29 @@ +import './index'; + +describe('component vnRoleDescriptor', () => { + let controller; + let $httpBackend; + + let role = {id: 1, name: 'foo'}; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnRoleDescriptor', {$element: null}, {role}); + })); + + describe('onDelete()', () => { + it('should delete entity and go to index', () => { + controller.$state.go = jest.fn(); + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expectDELETE('Roles/1').respond(); + controller.onDelete(); + $httpBackend.flush(); + + expect(controller.$state.go).toHaveBeenCalledWith('account.role'); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/account/front/role/descriptor/locale/es.yml b/modules/account/front/role/descriptor/locale/es.yml new file mode 100644 index 000000000..1ca512e4f --- /dev/null +++ b/modules/account/front/role/descriptor/locale/es.yml @@ -0,0 +1,2 @@ +Role will be removed: El rol va a ser eliminado +Role removed: Rol eliminado \ No newline at end of file diff --git a/modules/account/front/role/index.js b/modules/account/front/role/index.js new file mode 100644 index 000000000..97a20d3bc --- /dev/null +++ b/modules/account/front/role/index.js @@ -0,0 +1,10 @@ +import './main'; +import './index/'; +import './summary'; +import './card'; +import './descriptor'; +import './search-panel'; +import './create'; +import './basic-data'; +import './subroles'; +import './inherited'; diff --git a/modules/account/front/role/index/index.html b/modules/account/front/role/index/index.html new file mode 100644 index 000000000..92cca58d9 --- /dev/null +++ b/modules/account/front/role/index/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/role/index/index.js b/modules/account/front/role/index/index.js new file mode 100644 index 000000000..40773b23b --- /dev/null +++ b/modules/account/front/role/index/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + preview(role) { + this.selectedRole = role; + this.$.summary.show(); + } +} + +ngModule.component('vnRoleIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/role/index/locale/es.yml b/modules/account/front/role/index/locale/es.yml new file mode 100644 index 000000000..70932e983 --- /dev/null +++ b/modules/account/front/role/index/locale/es.yml @@ -0,0 +1,2 @@ +New role: Nuevo rol +View role: Ver rol \ No newline at end of file diff --git a/modules/account/front/role/inherited/index.html b/modules/account/front/role/inherited/index.html new file mode 100644 index 000000000..83ecbbff4 --- /dev/null +++ b/modules/account/front/role/inherited/index.html @@ -0,0 +1,21 @@ + + + + + +
+ {{::row.inherits.name}} +
+
+ {{::row.inherits.description}} +
+
+
+
+
+
diff --git a/modules/account/front/role/inherited/index.js b/modules/account/front/role/inherited/index.js new file mode 100644 index 000000000..5927493ee --- /dev/null +++ b/modules/account/front/role/inherited/index.js @@ -0,0 +1,23 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + let filter = { + where: {role: this.$params.id}, + include: { + relation: 'inherits', + scope: { + fields: ['id', 'name', 'description'] + } + } + }; + this.$http.get('RoleRoles', {filter}) + .then(res => this.$.data = res.data); + } +} + +ngModule.component('vnRoleInherited', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/role/inherited/index.spec.js b/modules/account/front/role/inherited/index.spec.js new file mode 100644 index 000000000..16b0c53b2 --- /dev/null +++ b/modules/account/front/role/inherited/index.spec.js @@ -0,0 +1,23 @@ +import './index'; + +describe('component vnRoleInherited', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnRoleInherited', {$element: null}); + })); + + describe('$onInit()', () => { + it('should delete entity and go to index', () => { + $httpBackend.expectGET('RoleRoles').respond('foo'); + controller.$onInit(); + $httpBackend.flush(); + + expect(controller.$.data).toBe('foo'); + }); + }); +}); diff --git a/modules/account/front/role/locale/es.yml b/modules/account/front/role/locale/es.yml new file mode 100644 index 000000000..159fc7f16 --- /dev/null +++ b/modules/account/front/role/locale/es.yml @@ -0,0 +1 @@ +Subroles: Subroles diff --git a/modules/account/front/role/main/index.html b/modules/account/front/role/main/index.html new file mode 100644 index 000000000..9d7e6e053 --- /dev/null +++ b/modules/account/front/role/main/index.html @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/account/front/role/main/index.js b/modules/account/front/role/main/index.js new file mode 100644 index 000000000..77d15cf17 --- /dev/null +++ b/modules/account/front/role/main/index.js @@ -0,0 +1,24 @@ +import ngModule from '../../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class Role extends ModuleMain { + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {or: [ + {name: {like: `%${value}%`}}, + {nickname: {like: `%${value}%`}} + ]}; + case 'name': + case 'description': + return {[param]: {like: `%${value}%`}}; + } + } +} + +ngModule.vnComponent('vnRole', { + controller: Role, + template: require('./index.html') +}); diff --git a/modules/account/front/role/search-panel/index.html b/modules/account/front/role/search-panel/index.html new file mode 100644 index 000000000..dfea9f01c --- /dev/null +++ b/modules/account/front/role/search-panel/index.html @@ -0,0 +1,21 @@ +
+
+ + + + + + + + + + + +
+
\ No newline at end of file diff --git a/modules/account/front/role/search-panel/index.js b/modules/account/front/role/search-panel/index.js new file mode 100644 index 000000000..35da591ad --- /dev/null +++ b/modules/account/front/role/search-panel/index.js @@ -0,0 +1,7 @@ +import ngModule from '../../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +ngModule.component('vnRoleSearchPanel', { + template: require('./index.html'), + controller: SearchPanel +}); diff --git a/modules/account/front/role/subroles/index.html b/modules/account/front/role/subroles/index.html new file mode 100644 index 000000000..bc554b9f9 --- /dev/null +++ b/modules/account/front/role/subroles/index.html @@ -0,0 +1,57 @@ + + + + + +
+ {{::row.inherits.name}} +
+
+ {{::row.inherits.description}} +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + diff --git a/modules/account/front/role/subroles/index.js b/modules/account/front/role/subroles/index.js new file mode 100644 index 000000000..b7e1caaa4 --- /dev/null +++ b/modules/account/front/role/subroles/index.js @@ -0,0 +1,51 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + this.refresh(); + } + + get path() { + return `RoleInherits`; + } + + refresh() { + let filter = { + where: {role: this.$params.id}, + include: { + relation: 'inherits', + scope: { + fields: ['id', 'name', 'description'] + } + } + }; + this.$http.get(this.path, {filter}) + .then(res => this.$.data = res.data); + } + + onAddClick() { + this.addData = {role: this.$params.id}; + this.$.dialog.show(); + } + + onAddSave() { + return this.$http.post(this.path, this.addData) + .then(() => this.refresh()) + .then(() => this.vnApp.showSuccess(this.$t('Role added! Changes will take a while to fully propagate.'))); + } + + onRemove(row) { + return this.$http.delete(`${this.path}/${row.id}`) + .then(() => { + let index = this.$.data.indexOf(row); + if (index !== -1) this.$.data.splice(index, 1); + this.vnApp.showSuccess(this.$t('Role removed. Changes will take a while to fully propagate.')); + }); + } +} + +ngModule.component('vnRoleSubroles', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/role/subroles/index.spec.js b/modules/account/front/role/subroles/index.spec.js new file mode 100644 index 000000000..e7d9a4d0e --- /dev/null +++ b/modules/account/front/role/subroles/index.spec.js @@ -0,0 +1,53 @@ +import './index'; + +describe('component vnRoleSubroles', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('account')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnRoleSubroles', {$element: null}); + jest.spyOn(controller.vnApp, 'showSuccess'); + })); + + describe('refresh()', () => { + it('should delete entity and go to index', () => { + $httpBackend.expectGET('RoleInherits').respond('foo'); + controller.refresh(); + $httpBackend.flush(); + + expect(controller.$.data).toBe('foo'); + }); + }); + + describe('onAddSave()', () => { + it('should add a subrole', () => { + controller.addData = {role: 'foo'}; + + $httpBackend.expectPOST('RoleInherits', {role: 'foo'}).respond(); + $httpBackend.expectGET('RoleInherits').respond(); + controller.onAddSave(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('onRemove()', () => { + it('should remove a subrole', () => { + controller.$.data = [ + {id: 1, name: 'foo'}, + {id: 2, name: 'bar'} + ]; + + $httpBackend.expectDELETE('RoleInherits/1').respond(); + controller.onRemove(controller.$.data[0]); + $httpBackend.flush(); + + expect(controller.$.data).toEqual([{id: 2, name: 'bar'}]); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/account/front/role/subroles/locale/es.yml b/modules/account/front/role/subroles/locale/es.yml new file mode 100644 index 000000000..170882405 --- /dev/null +++ b/modules/account/front/role/subroles/locale/es.yml @@ -0,0 +1,4 @@ +Role added! Changes will take a while to fully propagate.: > + ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente. +Role removed. Changes will take a while to fully propagate.: > + Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente. diff --git a/modules/account/front/role/summary/index.html b/modules/account/front/role/summary/index.html new file mode 100644 index 000000000..f7971190c --- /dev/null +++ b/modules/account/front/role/summary/index.html @@ -0,0 +1,20 @@ + +
{{summary.name}}
+ + +

Basic data

+ + + + + + +
+
+
\ No newline at end of file diff --git a/modules/account/front/role/summary/index.js b/modules/account/front/role/summary/index.js new file mode 100644 index 000000000..0a08fe8b2 --- /dev/null +++ b/modules/account/front/role/summary/index.js @@ -0,0 +1,25 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + set role(value) { + this._role = value; + this.$.summary = null; + if (!value) return; + + this.$http.get(`Roles/${value.id}`) + .then(res => this.$.summary = res.data); + } + + get role() { + return this._role; + } +} + +ngModule.component('vnRoleSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + role: '<' + } +}); diff --git a/modules/account/front/roles/index.html b/modules/account/front/roles/index.html new file mode 100644 index 000000000..8c8583929 --- /dev/null +++ b/modules/account/front/roles/index.html @@ -0,0 +1,21 @@ + + + + + +
+ {{::row.role.name}} +
+
+ {{::row.role.description}} +
+
+
+
+
+
diff --git a/modules/account/front/roles/index.js b/modules/account/front/roles/index.js new file mode 100644 index 000000000..0982dcf10 --- /dev/null +++ b/modules/account/front/roles/index.js @@ -0,0 +1,26 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + let filter = { + where: { + prindicpalType: 'USER', + principalId: this.$params.id + }, + include: { + relation: 'role', + scope: { + fields: ['id', 'name', 'description'] + } + } + }; + this.$http.get('RoleMappings', {filter}) + .then(res => this.$.data = res.data); + } +} + +ngModule.component('vnUserRoles', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json new file mode 100644 index 000000000..7316fff94 --- /dev/null +++ b/modules/account/front/routes.json @@ -0,0 +1,199 @@ +{ + "module": "account", + "name": "Users", + "icon" : "face", + "validations" : true, + "dependencies": [], + "menus": { + "main": [ + {"state": "account.index", "icon": "face"}, + {"state": "account.role", "icon": "group"}, + {"state": "account.alias", "icon": "email"}, + {"state": "account.acl", "icon": "check"}, + {"state": "account.connections", "icon": "share"} + ], + "card": [ + {"state": "account.card.basicData", "icon": "settings"}, + {"state": "account.card.roles", "icon": "group"}, + {"state": "account.card.mailForwarding", "icon": "forward"}, + {"state": "account.card.aliases", "icon": "email"} + ], + "role": [ + {"state": "account.role.card.basicData", "icon": "settings"}, + {"state": "account.role.card.subroles", "icon": "groups"}, + {"state": "account.role.card.inherited", "icon": "account_tree"} + ], + "alias": [ + {"state": "account.alias.card.basicData", "icon": "settings"}, + {"state": "account.alias.card.users", "icon": "groups"} + ] + }, + "routes": [ + { + "url": "/account", + "state": "account", + "component": "vn-user", + "description": "Users", + "abstract": true + }, { + "url": "/index?q", + "state": "account.index", + "component": "vn-user-index", + "description": "Users" + }, { + "url": "/create", + "state": "account.create", + "component": "vn-user-create", + "description": "New user" + }, { + "url": "/:id", + "state": "account.card", + "component": "vn-user-card", + "abstract": true, + "description": "Detail" + }, { + "url": "/summary", + "state": "account.card.summary", + "component": "vn-user-summary", + "description": "Summary", + "params": { + "user": "$ctrl.user" + } + }, { + "url": "/basic-data", + "state": "account.card.basicData", + "component": "vn-user-basic-data", + "description": "Basic data", + "params": { + "user": "$ctrl.user" + } + }, { + "url": "/roles", + "state": "account.card.roles", + "component": "vn-user-roles", + "description": "Inherited roles", + "params": { + "user": "$ctrl.user" + } + }, { + "url": "/mail-forwarding", + "state": "account.card.mailForwarding", + "component": "vn-user-mail-forwarding", + "description": "Mail forwarding", + "params": { + "user": "$ctrl.user" + } + }, { + "url": "/aliases", + "state": "account.card.aliases", + "component": "vn-user-aliases", + "description": "Mail aliases", + "params": { + "user": "$ctrl.user" + } + }, { + "url": "/role?q", + "state": "account.role", + "component": "vn-role", + "description": "Roles" + }, { + "url": "/create", + "state": "account.role.create", + "component": "vn-role-create", + "description": "New role" + }, { + "url": "/:id", + "state": "account.role.card", + "component": "vn-role-card", + "abstract": true, + "description": "Detail" + }, { + "url": "/summary", + "state": "account.role.card.summary", + "component": "vn-role-summary", + "description": "Summary", + "params": { + "role": "$ctrl.role" + } + }, { + "url": "/basic-data", + "state": "account.role.card.basicData", + "component": "vn-role-basic-data", + "description": "Basic data", + "acl": ["developer"], + "params": { + "role": "$ctrl.role" + } + }, { + "url": "/subroles", + "state": "account.role.card.subroles", + "component": "vn-role-subroles", + "acl": ["developer"], + "description": "Subroles" + }, { + "url": "/inherited", + "state": "account.role.card.inherited", + "component": "vn-role-inherited", + "description": "Inherited roles" + }, { + "url": "/alias?q", + "state": "account.alias", + "component": "vn-alias", + "description": "Mail aliases", + "acl": ["developer"] + }, { + "url": "/create", + "state": "account.alias.create", + "component": "vn-alias-create", + "description": "New alias" + }, { + "url": "/:id", + "state": "account.alias.card", + "component": "vn-alias-card", + "abstract": true, + "description": "Detail" + }, { + "url": "/summary", + "state": "account.alias.card.summary", + "component": "vn-alias-summary", + "description": "Summary", + "params": { + "alias": "$ctrl.alias" + } + }, { + "url": "/basic-data", + "state": "account.alias.card.basicData", + "component": "vn-alias-basic-data", + "description": "Basic data", + "params": { + "alias": "$ctrl.alias" + } + }, { + "url": "/users", + "state": "account.alias.card.users", + "component": "vn-alias-users", + "description": "Users" + }, { + "url": "/acl?q", + "state": "account.acl", + "component": "vn-acl-component", + "description": "ACLs", + "acl": ["developer"] + }, { + "url": "/create", + "state": "account.acl.create", + "component": "vn-acl-create", + "description": "New ACL" + }, { + "url": "/:id/edit", + "state": "account.acl.edit", + "component": "vn-acl-create", + "description": "Edit ACL" + }, { + "url": "/connections", + "state": "account.connections", + "component": "vn-connections", + "description": "Connections" + } + ] +} \ No newline at end of file diff --git a/modules/account/front/search-panel/index.html b/modules/account/front/search-panel/index.html new file mode 100644 index 000000000..f80b537aa --- /dev/null +++ b/modules/account/front/search-panel/index.html @@ -0,0 +1,31 @@ +
+
+ + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/modules/account/front/search-panel/index.js b/modules/account/front/search-panel/index.js new file mode 100644 index 000000000..fff3bf7b9 --- /dev/null +++ b/modules/account/front/search-panel/index.js @@ -0,0 +1,7 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +ngModule.component('vnUserSearchPanel', { + template: require('./index.html'), + controller: SearchPanel +}); diff --git a/modules/account/front/summary/index.html b/modules/account/front/summary/index.html new file mode 100644 index 000000000..7f390f17c --- /dev/null +++ b/modules/account/front/summary/index.html @@ -0,0 +1,20 @@ + +
{{summary.nickname}}
+ + +

Basic data

+ + + + + + +
+
+
\ No newline at end of file diff --git a/modules/account/front/summary/index.js b/modules/account/front/summary/index.js new file mode 100644 index 000000000..31c2d7d69 --- /dev/null +++ b/modules/account/front/summary/index.js @@ -0,0 +1,33 @@ +import ngModule from '../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + set user(value) { + this._user = value; + this.$.summary = null; + if (!value) return; + + const filter = { + include: { + relation: 'role', + scope: { + fields: ['id', 'name'] + } + } + }; + this.$http.get(`Accounts/${value.id}`, {filter}) + .then(res => this.$.summary = res.data); + } + + get user() { + return this._user; + } +} + +ngModule.component('vnUserSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + user: '<' + } +}); diff --git a/modules/claim/front/action/index.html b/modules/claim/front/action/index.html index f7a43bd2e..25343b310 100644 --- a/modules/claim/front/action/index.html +++ b/modules/claim/front/action/index.html @@ -7,7 +7,8 @@ auto-save="true" on-save="$ctrl.onSave()"> - diff --git a/modules/claim/front/basic-data/index.html b/modules/claim/front/basic-data/index.html index 710068196..c73e04b24 100644 --- a/modules/claim/front/basic-data/index.html +++ b/modules/claim/front/basic-data/index.html @@ -1,42 +1,42 @@ + data="$ctrl.claim" + insert-mode="true" + form="form"> + +
- - diff --git a/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js b/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js index 7eb8e7ed9..5dbf6cb48 100644 --- a/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js +++ b/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js @@ -7,7 +7,7 @@ describe('Client activeWorkersWithRole', () => { let isSalesPerson = await app.models.Account.hasRole(result[0].id, 'salesPerson'); - expect(result.length).toEqual(15); + expect(result.length).toEqual(16); expect(isSalesPerson).toBeTruthy(); }); @@ -17,7 +17,7 @@ describe('Client activeWorkersWithRole', () => { let isBuyer = await app.models.Account.hasRole(result[0].id, 'buyer'); - expect(result.length).toEqual(15); + expect(result.length).toEqual(16); expect(isBuyer).toBeTruthy(); }); }); diff --git a/modules/client/back/methods/client/specs/listWorkers.spec.js b/modules/client/back/methods/client/specs/listWorkers.spec.js index 97f4b591d..329a27aa5 100644 --- a/modules/client/back/methods/client/specs/listWorkers.spec.js +++ b/modules/client/back/methods/client/specs/listWorkers.spec.js @@ -6,7 +6,7 @@ describe('Client listWorkers', () => { .then(result => { let amountOfEmployees = Object.keys(result).length; - expect(amountOfEmployees).toEqual(50); + expect(amountOfEmployees).toEqual(51); done(); }) .catch(done.fail); diff --git a/modules/client/back/methods/client/specs/sendSms.spec.js b/modules/client/back/methods/client/specs/sendSms.spec.js index b299ac3c1..673a95cae 100644 --- a/modules/client/back/methods/client/specs/sendSms.spec.js +++ b/modules/client/back/methods/client/specs/sendSms.spec.js @@ -1,6 +1,7 @@ const app = require('vn-loopback/server/server'); -describe('client sendSms()', () => { +// Issue #2471 +xdescribe('client sendSms()', () => { let createdLog; afterAll(async done => { diff --git a/modules/client/back/methods/sms/send.spec.js b/modules/client/back/methods/sms/send.spec.js index 612a16cf1..06288ffb5 100644 --- a/modules/client/back/methods/sms/send.spec.js +++ b/modules/client/back/methods/sms/send.spec.js @@ -1,7 +1,8 @@ const app = require('vn-loopback/server/server'); const soap = require('soap'); -describe('sms send()', () => { +// Issue #2471 +xdescribe('sms send()', () => { it('should return the expected message and status code', async() => { const code = 200; const smsConfig = await app.models.SmsConfig.findOne(); diff --git a/modules/client/front/address/create/index.html b/modules/client/front/address/create/index.html index 89c674a41..d6b5bcf8e 100644 --- a/modules/client/front/address/create/index.html +++ b/modules/client/front/address/create/index.html @@ -4,16 +4,26 @@ id-field="id" data="$ctrl.address" params="$ctrl.address" - save="post" + insert-mode="true" form="form"> + auto-load="true" + url="Provinces/location" + data="provincesLocation" + order="id"> + + + + @@ -79,7 +89,7 @@ diff --git a/modules/client/front/balance/create/index.html b/modules/client/front/balance/create/index.html index 6cfdc0666..a2775164d 100644 --- a/modules/client/front/balance/create/index.html +++ b/modules/client/front/balance/create/index.html @@ -2,13 +2,19 @@ New payment + + + selection="$ctrl.bankSelection" + order="id"> {{id}}: {{bank}} + +
diff --git a/modules/client/front/basic-data/index.html b/modules/client/front/basic-data/index.html index 8c00f7a18..07b24db89 100644 --- a/modules/client/front/basic-data/index.html +++ b/modules/client/front/basic-data/index.html @@ -5,6 +5,11 @@ form="form" save="patch"> + + @@ -60,7 +65,7 @@ diff --git a/modules/client/front/billing-data/index.html b/modules/client/front/billing-data/index.html index ca188955c..0e61f4d16 100644 --- a/modules/client/front/billing-data/index.html +++ b/modules/client/front/billing-data/index.html @@ -5,6 +5,11 @@ form="form" save="patch"> + + @@ -13,7 +18,7 @@ label="Billing data" vn-acl="salesAssistant" ng-model="$ctrl.client.payMethodFk" - url="PayMethods" + data="paymethods" fields="['ibanRequired']" initial-data="$ctrl.client.payMethod"> diff --git a/modules/client/front/create/index.html b/modules/client/front/create/index.html index b2ef42beb..420b9685c 100644 --- a/modules/client/front/create/index.html +++ b/modules/client/front/create/index.html @@ -1,10 +1,16 @@ - + insert-mode="true" + form="form"> + + @@ -15,14 +21,13 @@ rule vn-focus> - + where="{role: 'employee'}"> {{firstName}} {{lastName}} @@ -34,7 +39,6 @@ rule> @@ -49,7 +53,7 @@ - - {{name}}, {{province.name}} @@ -88,34 +92,31 @@ - {{name}} ({{country.country}}) - + show-field="country"> diff --git a/modules/client/front/credit-insurance/insurance/create/index.html b/modules/client/front/credit-insurance/insurance/create/index.html index 2331cf40b..f3de7fbe7 100644 --- a/modules/client/front/credit-insurance/insurance/create/index.html +++ b/modules/client/front/credit-insurance/insurance/create/index.html @@ -1,15 +1,14 @@ - + insert-mode="true" + form="form">
+ +
diff --git a/modules/client/front/dms/create/index.html b/modules/client/front/dms/create/index.html index 4ed221ae8..e585dc398 100644 --- a/modules/client/front/dms/create/index.html +++ b/modules/client/front/dms/create/index.html @@ -3,6 +3,24 @@ vn-id="watcher" data="$ctrl.dms"> + + + + + + @@ -30,14 +48,14 @@ diff --git a/modules/client/front/fiscal-data/index.html b/modules/client/front/fiscal-data/index.html index 3fb2563f9..b3789b34a 100644 --- a/modules/client/front/fiscal-data/index.html +++ b/modules/client/front/fiscal-data/index.html @@ -6,6 +6,18 @@ form="form" save="patch"> + + + + @@ -76,7 +88,7 @@ label="Province" ng-model="$ctrl.client.provinceFk" selection="$ctrl.province" - url="Provinces/location" + data="provincesLocation" fields="['id', 'name', 'countryFk']" show-field="name" value-field="id" @@ -85,7 +97,7 @@ + insert-mode="true"> + + @@ -40,6 +41,6 @@ - + \ No newline at end of file diff --git a/modules/client/front/greuge/create/index.js b/modules/client/front/greuge/create/index.js index baf9f6a49..dcc0fa522 100644 --- a/modules/client/front/greuge/create/index.js +++ b/modules/client/front/greuge/create/index.js @@ -2,31 +2,23 @@ import ngModule from '../../module'; import Section from 'salix/components/section'; class Controller extends Section { - constructor($element, $) { - super($element, $); + constructor(...args) { + super(...args); this.greuge = { shipped: new Date(), clientFk: this.$params.id }; } - cancel() { - this.goToIndex(); - } - goToIndex() { - this.$state.go('client.card.greuge.index'); + return this.$state.go('client.card.greuge.index'); } onSubmit() { - this.$.watcher.submit().then( - () => { - this.goToIndex(); - } - ); + this.$.watcher.submit() + .then(() => this.goToIndex()); } } -Controller.$inject = ['$element', '$scope']; ngModule.vnComponent('vnClientGreugeCreate', { template: require('./index.html'), diff --git a/modules/client/front/note/create/index.html b/modules/client/front/note/create/index.html index c9c351199..aba03aafb 100644 --- a/modules/client/front/note/create/index.html +++ b/modules/client/front/note/create/index.html @@ -3,7 +3,7 @@ url="ClientObservations" id-field="id" data="$ctrl.note" - save="post" + insert-mode="true" form="form">
diff --git a/modules/client/front/recovery/create/index.html b/modules/client/front/recovery/create/index.html index 56eb7eeb7..7747bb4d7 100644 --- a/modules/client/front/recovery/create/index.html +++ b/modules/client/front/recovery/create/index.html @@ -1,34 +1,30 @@ - + insert-mode="true" + form="form"> + rule + vn-focus> diff --git a/modules/client/front/sample/create/index.html b/modules/client/front/sample/create/index.html index 22f95eb20..d8bd4712a 100644 --- a/modules/client/front/sample/create/index.html +++ b/modules/client/front/sample/create/index.html @@ -1,40 +1,52 @@ - - + + + insert-mode="true" + form="form"> + + - - - @@ -49,7 +61,6 @@ - diff --git a/modules/entry/front/create/index.html b/modules/entry/front/create/index.html index 7b5dfc928..0662615ae 100644 --- a/modules/entry/front/create/index.html +++ b/modules/entry/front/create/index.html @@ -1,9 +1,9 @@ - + insert-mode="true" + form="form">
@@ -12,15 +12,13 @@ vn-tooltip="Required fields (*)"> - {{::id}} - {{::nickname}} @@ -29,13 +27,11 @@ {{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) → @@ -45,11 +41,10 @@ diff --git a/modules/item/front/basic-data/index.html b/modules/item/front/basic-data/index.html index 71fd3848d..0a40328b1 100644 --- a/modules/item/front/basic-data/index.html +++ b/modules/item/front/basic-data/index.html @@ -9,6 +9,13 @@ form="form" save="patch"> + + @@ -64,7 +71,13 @@ + + - + id-value="$ctrl.$params.id" + include="[{relation: 'genus'}, {relation: 'specie'}]" + auto-fill="true" + form="form"> @@ -21,19 +19,18 @@ + value-field="genus_id"> - - + + \ No newline at end of file diff --git a/modules/item/front/botanical/index.js b/modules/item/front/botanical/index.js index 61eed3ce8..3786cdb45 100644 --- a/modules/item/front/botanical/index.js +++ b/modules/item/front/botanical/index.js @@ -1,30 +1,7 @@ import ngModule from '../module'; import Section from 'salix/components/section'; -class Controller extends Section { - constructor($element, $) { - super($element, $); - this.botanical = { - itemFk: this.$params.id - }; - } - - _getBotanical() { - let filter = { - where: {itemFk: this.$params.id}, - include: [{relation: 'genus'}, {relation: 'specie'}] - }; - this.$http.get(`ItemBotanicals?filter=${JSON.stringify(filter)}`) - .then(res => { - if (res.data.length) - this.botanical = res.data[0]; - }); - } - - $onInit() { - this._getBotanical(); - } -} +class Controller extends Section {} ngModule.vnComponent('vnItemBotanical', { template: require('./index.html'), diff --git a/modules/item/front/botanical/index.spec.js b/modules/item/front/botanical/index.spec.js deleted file mode 100644 index 24e48e4ae..000000000 --- a/modules/item/front/botanical/index.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import './index.js'; - -describe('ItemBotanical', () => { - describe('Component vnItemBotanical', () => { - let $httpBackend; - let $scope; - let controller; - - beforeEach(ngModule('item')); - - beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => { - $httpBackend = _$httpBackend_; - $scope = $rootScope.$new(); - const $element = angular.element(''); - controller = $componentController('vnItemBotanical', {$element, $scope}); - controller.$params = {id: 123}; - })); - - describe('_getBotanical()', () => { - it('should request to patch the propagation of botanical status', () => { - $httpBackend.whenGET('ItemBotanicals?filter={"where":{"itemFk":123},"include":[{"relation":"genus"},{"relation":"specie"}]}').respond({data: 'item'}); - $httpBackend.expectGET('ItemBotanicals?filter={"where":{"itemFk":123},"include":[{"relation":"genus"},{"relation":"specie"}]}'); - controller.$onInit(); - $httpBackend.flush(); - }); - }); - }); -}); diff --git a/modules/item/front/create/index.html b/modules/item/front/create/index.html index e57da198c..1756852c9 100644 --- a/modules/item/front/create/index.html +++ b/modules/item/front/create/index.html @@ -1,27 +1,32 @@ - + insert-mode="true" + form="form"> + +
- - @@ -34,12 +39,11 @@ -
{{::id}}
@@ -49,10 +53,8 @@
diff --git a/modules/item/front/diary/index.html b/modules/item/front/diary/index.html index 272c58a7d..080cc0f36 100644 --- a/modules/item/front/diary/index.html +++ b/modules/item/front/diary/index.html @@ -5,13 +5,20 @@ data="sales" auto-load="false"> + + + + - + vn-acl="buyer,replenisher"> { it('should perform a post to update taxes', () => { jest.spyOn(controller.$.watcher, 'notifySaved'); jest.spyOn(controller.$.watcher, 'updateOriginalData'); - controller.taxes = [ - {id: 37, countryFk: 1, taxClassFk: 1, country: {id: 1, country: 'España'}} - ]; - controller.$.watcher.data = [ - {id: 37, countryFk: 1, taxClassFk: 2, country: {id: 1, country: 'España'}} - ]; - $httpBackend.whenPOST(`Items/updateTaxes`).respond('oki doki'); + controller.$onInit(); + $httpBackend.flush(); + + controller.taxes.push({id: 3, description: 'General VAT', code: 'G'}); + + $httpBackend.whenPOST(`Items/updateTaxes`).respond(true); controller.submit(); $httpBackend.flush(); diff --git a/modules/order/front/basic-data/index.html b/modules/order/front/basic-data/index.html index eaa73a25c..8945de2b7 100644 --- a/modules/order/front/basic-data/index.html +++ b/modules/order/front/basic-data/index.html @@ -1,10 +1,10 @@ + vn-id="address-model"> + insert-mode="true" + form="form"> - + where="{role: 'employee'}"> - + show-field="numberPlate"> + url="AgencyModes"> diff --git a/modules/ticket/back/methods/ticket-tracking/changeState.js b/modules/ticket/back/methods/ticket-tracking/changeState.js index e28413b24..f7baeecfd 100644 --- a/modules/ticket/back/methods/ticket-tracking/changeState.js +++ b/modules/ticket/back/methods/ticket-tracking/changeState.js @@ -40,15 +40,17 @@ module.exports = Self => { params.workerFk = worker.id; } - let ticket = await models.TicketState.findById( + let ticketState = await models.TicketState.findById( params.ticketFk, {fields: ['stateFk']} ); - let oldStateAllowed = await models.State.isEditable(ctx, ticket.stateFk); + let oldStateAllowed; + if (ticketState) + oldStateAllowed = await models.State.isEditable(ctx, ticketState.stateFk); let newStateAllowed = await models.State.isEditable(ctx, params.stateFk); - let isAllowed = oldStateAllowed && newStateAllowed; + let isAllowed = (!ticketState || oldStateAllowed == true) && newStateAllowed == true; if (!isAllowed) throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED'); diff --git a/modules/ticket/back/methods/ticket/specs/sendSms.spec.js b/modules/ticket/back/methods/ticket/specs/sendSms.spec.js index 20066a5ba..a08e7555a 100644 --- a/modules/ticket/back/methods/ticket/specs/sendSms.spec.js +++ b/modules/ticket/back/methods/ticket/specs/sendSms.spec.js @@ -1,6 +1,7 @@ const app = require('vn-loopback/server/server'); -describe('ticket sendSms()', () => { +// Issue #2471 +xdescribe('ticket sendSms()', () => { let logId; afterAll(async done => { diff --git a/modules/ticket/front/create/card.html b/modules/ticket/front/create/card.html index 6e6b5de62..36e62d8e6 100644 --- a/modules/ticket/front/create/card.html +++ b/modules/ticket/front/create/card.html @@ -1,3 +1,9 @@ + + diff --git a/modules/ticket/front/dms/create/index.html b/modules/ticket/front/dms/create/index.html index 8e6af2b87..54758aa83 100644 --- a/modules/ticket/front/dms/create/index.html +++ b/modules/ticket/front/dms/create/index.html @@ -2,6 +2,24 @@ vn-id="watcher" data="$ctrl.dms"> + + + + + + @@ -29,14 +47,14 @@ diff --git a/modules/ticket/front/request/create/index.html b/modules/ticket/front/request/create/index.html index 46d96ca44..7dafbd801 100644 --- a/modules/ticket/front/request/create/index.html +++ b/modules/ticket/front/request/create/index.html @@ -1,46 +1,41 @@ - + insert-mode="true" + form="form">
- + search-function="{firstName: $search}"> diff --git a/modules/ticket/front/services/index.html b/modules/ticket/front/services/index.html index 261b50d6f..f6da210dc 100644 --- a/modules/ticket/front/services/index.html +++ b/modules/ticket/front/services/index.html @@ -5,6 +5,18 @@ data="$ctrl.services" auto-load="true"> + + + + @@ -13,7 +25,7 @@ + + diff --git a/modules/travel/front/basic-data/index.html b/modules/travel/front/basic-data/index.html index 85a5fad2b..7e12949fd 100644 --- a/modules/travel/front/basic-data/index.html +++ b/modules/travel/front/basic-data/index.html @@ -5,6 +5,12 @@ form="form" save="patch"> + + @@ -38,7 +44,7 @@ @@ -46,7 +52,7 @@ diff --git a/modules/travel/front/create/index.html b/modules/travel/front/create/index.html index 586689ff7..652d2fbb9 100644 --- a/modules/travel/front/create/index.html +++ b/modules/travel/front/create/index.html @@ -1,55 +1,43 @@ - + insert-mode="true" + form="form"> + url="AgencyModes"> + url="Warehouses"> + url="Warehouses"> diff --git a/modules/travel/front/thermograph/create/index.html b/modules/travel/front/thermograph/create/index.html index 0232c1b12..c965c9867 100644 --- a/modules/travel/front/thermograph/create/index.html +++ b/modules/travel/front/thermograph/create/index.html @@ -2,6 +2,24 @@ vn-id="watcher" data="$ctrl.dms"> + + + + + + @@ -54,14 +72,14 @@ diff --git a/modules/worker/front/descriptor/index.html b/modules/worker/front/descriptor/index.html index e75d14322..fb2264494 100644 --- a/modules/worker/front/descriptor/index.html +++ b/modules/worker/front/descriptor/index.html @@ -32,7 +32,13 @@ icon="person">
-
+
+ + +
diff --git a/modules/zone/front/create/index.html b/modules/zone/front/create/index.html index 07ac38477..cda13362a 100644 --- a/modules/zone/front/create/index.html +++ b/modules/zone/front/create/index.html @@ -1,10 +1,22 @@ - + insert-mode="true" + form="form"> + + + + + rule + vn-focus> @@ -57,7 +66,6 @@ + + diff --git a/package-lock.json b/package-lock.json index 37af78e3a..b5a27cb21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6778,6 +6778,14 @@ "now-and-later": "^2.0.0" } }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -6879,6 +6887,11 @@ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, + "bigdecimal": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/bigdecimal/-/bigdecimal-0.6.1.tgz", + "integrity": "sha1-GFiNS08ia3cxDtBFdIWMA2pUSFs=" + }, "bignumber.js": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", @@ -7245,6 +7258,17 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bunyan": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", + "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -8637,6 +8661,15 @@ "is-obj": "^1.0.0" } }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplex": { "version": "1.0.0", "resolved": "http://registry.npmjs.org/duplex/-/duplex-1.0.0.tgz", @@ -18447,6 +18480,45 @@ "invert-kv": "^2.0.0" } }, + "ldap-filter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz", + "integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=", + "requires": { + "assert-plus": "0.1.5" + }, + "dependencies": { + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" + } + } + }, + "ldapjs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz", + "integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "bunyan": "^1.8.3", + "dashdash": "^1.14.0", + "dtrace-provider": "~0.8", + "ldap-filter": "0.2.2", + "once": "^1.4.0", + "vasync": "^1.6.4", + "verror": "^1.8.1" + }, + "dependencies": { + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + } + } + }, "lead": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", @@ -19769,6 +19841,12 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "optional": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -19907,6 +19985,41 @@ "xtend": "~1.0.3" } }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -19996,8 +20109,7 @@ "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanoid": { "version": "2.0.3", @@ -20034,6 +20146,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -20346,6 +20464,33 @@ } } }, + "node-ssh": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-11.0.0.tgz", + "integrity": "sha512-zC8TuZX82/x/ZsH4GzE5jmUGQAS2ajcDOoD352x6W56A7+4ChuGdGYi+t9NnLKEtRYvbvNH4HO+LvlQVENmxog==", + "requires": { + "make-dir": "^3.1.0", + "sb-promise-queue": "^2.1.0", + "sb-scandir": "^3.1.0", + "shell-escape": "^0.2.0", + "ssh2": "^0.8.9" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "node.extend": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", @@ -21381,6 +21526,11 @@ } } }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -22340,6 +22490,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -22520,6 +22676,19 @@ "xmlchars": "^2.2.0" } }, + "sb-promise-queue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.0.tgz", + "integrity": "sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg==" + }, + "sb-scandir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.0.tgz", + "integrity": "sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg==", + "requires": { + "sb-promise-queue": "^2.1.0" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -22840,6 +23009,11 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "shell-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", + "integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=" + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -22938,6 +23112,14 @@ "is-fullwidth-code-point": "^2.0.0" } }, + "smbhash": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/smbhash/-/smbhash-0.0.1.tgz", + "integrity": "sha1-Pgtzz8bALUwMGamT6E5S4R/3oJk=", + "requires": { + "bigdecimal": ">= 0.6.0" + } + }, "smtp-connection": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz", @@ -23350,6 +23532,24 @@ "options": "0.0.6" } }, + "ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "requires": { + "ssh2-streams": "~0.4.10" + } + }, + "ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -23504,6 +23704,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-length": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", @@ -25201,6 +25406,29 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "vasync": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz", + "integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=", + "requires": { + "verror": "1.6.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz", + "integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk=" + }, + "verror": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", + "integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=", + "requires": { + "extsprintf": "1.2.0" + } + } + } + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index f9d291467..586b77108 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "fs-extra": "^5.0.0", "helmet": "^3.21.2", "i18n": "^0.8.4", + "ldapjs": "^1.0.2", "loopback": "^3.26.0", "loopback-boot": "^2.27.1", "loopback-component-explorer": "^6.5.0", @@ -24,12 +25,14 @@ "loopback-connector-remote": "^3.4.1", "loopback-context": "^3.4.0", "md5": "^2.2.1", + "node-ssh": "^11.0.0", "object-diff": "0.0.4", "object.pick": "^1.3.0", "request": "^2.88.0", "request-promise-native": "^1.0.8", "require-yaml": "0.0.1", "sharp": "^0.25.4", + "smbhash": "0.0.1", "soap": "^0.26.0", "strong-error-handler": "^2.3.2", "uuid": "^3.3.3", diff --git a/print/core/config.js b/print/core/config.js index c7307bafc..fbe867440 100644 --- a/print/core/config.js +++ b/print/core/config.js @@ -5,8 +5,8 @@ let env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; let configPath = `/etc/salix`; let config = require('../config/print.json'); let configFiles = [ - `../config/print.local.json`, - `../config/print.${env}.json`, + `${__dirname}/../config/print.local.json`, + `${__dirname}/../config/print.${env}.json`, `${configPath}/print.json`, `${configPath}/print.local.json`, `${configPath}/print.${env}.json` diff --git a/print/core/filters/specs/number.spec.js b/print/core/filters/specs/number.spec.js index 752200694..72d5de0c5 100644 --- a/print/core/filters/specs/number.spec.js +++ b/print/core/filters/specs/number.spec.js @@ -1,6 +1,7 @@ import number from '../number.js'; -describe('number filter', () => { +// Issue #2437 +xdescribe('number filter', () => { const superDuperNumber = 18021984; it('should filter the number with commas by default', () => { diff --git a/print/core/filters/specs/percentage.spec.js b/print/core/filters/specs/percentage.spec.js index 0a9111cbc..af037abb6 100644 --- a/print/core/filters/specs/percentage.spec.js +++ b/print/core/filters/specs/percentage.spec.js @@ -1,6 +1,7 @@ import percentage from '../percentage.js'; -describe('percentage filter', () => { +// Issue #2437 +xdescribe('percentage filter', () => { it('should filter the percentage also round it correctly', () => { expect(percentage(99.9999999999999999 / 100)).toEqual('100.00%'); });