diff --git a/back/methods/account/change-password.js b/back/methods/account/change-password.js new file mode 100644 index 000000000..b2afd5402 --- /dev/null +++ b/back/methods/account/change-password.js @@ -0,0 +1,64 @@ + +module.exports = Self => { + Self.remoteMethod('changePassword', { + description: 'Changes the user password', + accepts: [ + { + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, { + 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 + } + ], + returns: { + type: 'Boolean', + root: true + }, + http: { + path: `/:id/changePassword`, + verb: 'PATCH' + } + }); + + Self.changePassword = async function(ctx, id, oldPassword, newPassword) { + let params = [id, oldPassword, newPassword]; + await Self.rawSql(`CALL account.user_changePassword(?, ?, ?)`, params); + + /* + const ldap = require('ldapjs'); + + let ldapConf = { + url: 'ldap://domain.local:389', + dn: 'cn=admin,dc=domain,dc=local', + password: '123456' + }; + + await new Promise((reject, resolve) => { + let client = ldap.createClient({url: ldapConf.url}); + + client.bind(ldapConf.dn, ldapConf.password, err => { + if (err) + reject(err); + else + resolve(); + }); + }); + */ + + return true; + }; +}; diff --git a/back/methods/account/set-password.js b/back/methods/account/set-password.js new file mode 100644 index 000000000..027649548 --- /dev/null +++ b/back/methods/account/set-password.js @@ -0,0 +1,33 @@ + +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 + } + ], + returns: { + type: 'Boolean', + root: true + }, + http: { + path: `/:id/setPassword`, + verb: 'PATCH' + } + }); + + Self.setPassword = async function(id, newPassword) { + let params = [id, newPassword]; + await Self.rawSql(`CALL account.user_setPassword(?, ?)`, params); + return true; + }; +}; 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..b74a14997 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 diff --git a/back/models/account.json b/back/models/account.json index 7186621b4..fbf736f03 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -24,6 +24,9 @@ "nickname": { "type": "string" }, + "lang": { + "type": "string" + }, "password": { "type": "string", "required": true 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/10211-accountModule/00-account.sql b/db/changes/10211-accountModule/00-account.sql new file mode 100644 index 000000000..987256793 --- /dev/null +++ b/db/changes/10211-accountModule/00-account.sql @@ -0,0 +1,47 @@ + +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`); + + +USE account; + +DELIMITER $$ + +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/10211-accountModule/00-myUserChangePassword.sql b/db/changes/10211-accountModule/00-myUserChangePassword.sql new file mode 100644 index 000000000..94fc02087 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUserCheckLogin.sql b/db/changes/10211-accountModule/00-myUserCheckLogin.sql new file mode 100644 index 000000000..eaa962b63 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUserGetId.sql b/db/changes/10211-accountModule/00-myUserGetId.sql new file mode 100644 index 000000000..f0bb972aa --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUserGetName.sql b/db/changes/10211-accountModule/00-myUserGetName.sql new file mode 100644 index 000000000..2f758d0c6 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUserHasRole.sql b/db/changes/10211-accountModule/00-myUserHasRole.sql new file mode 100644 index 000000000..6d2301328 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUserHasRoleId.sql b/db/changes/10211-accountModule/00-myUserHasRoleId.sql new file mode 100644 index 000000000..380bd0641 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_changePassword.sql b/db/changes/10211-accountModule/00-myUser_changePassword.sql new file mode 100644 index 000000000..3dd86a881 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_checkLogin.sql b/db/changes/10211-accountModule/00-myUser_checkLogin.sql new file mode 100644 index 000000000..843f57fff --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_getId.sql b/db/changes/10211-accountModule/00-myUser_getId.sql new file mode 100644 index 000000000..b3d3f1b28 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_getName.sql b/db/changes/10211-accountModule/00-myUser_getName.sql new file mode 100644 index 000000000..b055227d3 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_hasRole.sql b/db/changes/10211-accountModule/00-myUser_hasRole.sql new file mode 100644 index 000000000..538b58f08 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_hasRoleId.sql b/db/changes/10211-accountModule/00-myUser_hasRoleId.sql new file mode 100644 index 000000000..2931443e1 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_login.sql b/db/changes/10211-accountModule/00-myUser_login.sql new file mode 100644 index 000000000..9d92828b0 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_loginWithKey.sql b/db/changes/10211-accountModule/00-myUser_loginWithKey.sql new file mode 100644 index 000000000..fc12a79d9 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_loginWithName.sql b/db/changes/10211-accountModule/00-myUser_loginWithName.sql new file mode 100644 index 000000000..6b86a37f3 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-myUser_logout.sql b/db/changes/10211-accountModule/00-myUser_logout.sql new file mode 100644 index 000000000..ffa2c969e --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-passwordGenerate.sql b/db/changes/10211-accountModule/00-passwordGenerate.sql new file mode 100644 index 000000000..46048e24d --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-role_checkName.sql b/db/changes/10211-accountModule/00-role_checkName.sql new file mode 100644 index 000000000..1e4f31767 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-role_getDescendents.sql b/db/changes/10211-accountModule/00-role_getDescendents.sql new file mode 100644 index 000000000..9b224f6eb --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-role_sync.sql b/db/changes/10211-accountModule/00-role_sync.sql new file mode 100644 index 000000000..8e16ef567 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-role_syncPrivileges.sql b/db/changes/10211-accountModule/00-role_syncPrivileges.sql new file mode 100644 index 000000000..0d6d8975b --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userGetId.sql b/db/changes/10211-accountModule/00-userGetId.sql new file mode 100644 index 000000000..219ea680c --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userGetMysqlRole.sql b/db/changes/10211-accountModule/00-userGetMysqlRole.sql new file mode 100644 index 000000000..673f1aac9 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userGetName.sql b/db/changes/10211-accountModule/00-userGetName.sql new file mode 100644 index 000000000..f49f2dbef --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userGetNameFromId.sql b/db/changes/10211-accountModule/00-userGetNameFromId.sql new file mode 100644 index 000000000..f8e9333cb --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userHasRole.sql b/db/changes/10211-accountModule/00-userHasRole.sql new file mode 100644 index 000000000..3e09d27bf --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userHasRoleId.sql b/db/changes/10211-accountModule/00-userHasRoleId.sql new file mode 100644 index 000000000..9fcd9f073 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userLogin.sql b/db/changes/10211-accountModule/00-userLogin.sql new file mode 100644 index 000000000..63a332254 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userLoginWithKey.sql b/db/changes/10211-accountModule/00-userLoginWithKey.sql new file mode 100644 index 000000000..45a490c71 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userLoginWithName.sql b/db/changes/10211-accountModule/00-userLoginWithName.sql new file mode 100644 index 000000000..4053970e4 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userLogout.sql b/db/changes/10211-accountModule/00-userLogout.sql new file mode 100644 index 000000000..7d0d68324 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-userSetPassword.sql b/db/changes/10211-accountModule/00-userSetPassword.sql new file mode 100644 index 000000000..fd3daec53 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_changePassword.sql b/db/changes/10211-accountModule/00-user_changePassword.sql new file mode 100644 index 000000000..c137213e0 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_checkName.sql b/db/changes/10211-accountModule/00-user_checkName.sql new file mode 100644 index 000000000..9b54d6175 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_getMysqlRole.sql b/db/changes/10211-accountModule/00-user_getMysqlRole.sql new file mode 100644 index 000000000..4088ea8a4 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_getNameFromId.sql b/db/changes/10211-accountModule/00-user_getNameFromId.sql new file mode 100644 index 000000000..ae9ae5941 --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_hasRole.sql b/db/changes/10211-accountModule/00-user_hasRole.sql new file mode 100644 index 000000000..d42c81deb --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_hasRoleId.sql b/db/changes/10211-accountModule/00-user_hasRoleId.sql new file mode 100644 index 000000000..b2f523e8c --- /dev/null +++ b/db/changes/10211-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/10211-accountModule/00-user_setPassword.sql b/db/changes/10211-accountModule/00-user_setPassword.sql new file mode 100644 index 000000000..430c60eab --- /dev/null +++ b/db/changes/10211-accountModule/00-user_setPassword.sql @@ -0,0 +1,23 @@ +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; + + CALL user_syncPassword(vSelf, vPassword); +END$$ +DELIMITER ; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 56f779e55..d339506db 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,6 +29,7 @@ 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' @@ -52,7 +49,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 +65,36 @@ 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`.`userPassword` (`id`, `length`, `nAlpha`, `nUpper`, `nDigits`, `nPunct`) + VALUES + (1, 8, 1, 1, 1, 1); + +INSERT INTO `account`.`account`(`id`) + VALUES + (101), + (102); + +INSERT INTO `account`.`mailConfig` (`id`, `domain`) + VALUES + (1, 'verdnatura.es'); + +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, 101), + (1, 102), + (2, 101); + +INSERT INTO `account`.`mailForward`(`account`, `forwardTo`) + VALUES + (101, '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 +115,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/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..58fdadb65 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() }); } @@ -205,7 +210,9 @@ export default class Searchbar extends Component { && source != 'state' && !angular.equals(filter, {}) && data - && data.length == 1; + && data.length == 1 + && filter.search + && Object.keys(filter).length == 1; if (isOneResult) { let baseDepth = this.baseState.split('.').length; @@ -222,7 +229,7 @@ export default class Searchbar extends Component { subState += '.index'; break; default: - subState = 'card.summary'; + subState = this.entityState; } if (this.stateParams) @@ -292,8 +299,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})">
  • - - diff --git a/front/salix/components/left-menu/left-menu.js b/front/salix/components/left-menu/left-menu.js index 5f047060a..da545b291 100644 --- a/front/salix/components/left-menu/left-menu.js +++ b/front/salix/components/left-menu/left-menu.js @@ -32,73 +32,86 @@ export default class LeftMenu { let moduleIndex = this.$state.current.data.moduleIndex; let moduleFile = window.routes[moduleIndex] || []; let menu = moduleFile.menus && moduleFile.menus[this.source] || []; - let items = []; - let addItem = (items, item) => { - let state = states[item.state]; - if (state) { - state = state.self; - let acl = state.data.acl; + let cloneItems = (items, parent) => { + let myItems = []; - if (acl && !this.aclService.hasAny(acl)) - return; - } else if (!item.external) { - console.warn('wrong left-menu definition'); - return; + for (let item of items) { + let state = states[item.state]; + if (state) { + state = state.self; + let acl = state.data.acl; + + if (acl && !this.aclService.hasAny(acl)) + continue; + } + + let myItem = { + icon: item.icon, + description: item.description || state.description, + state: item.state, + external: item.external, + url: item.url, + parent + }; + + if (item.childs) { + let myChilds = cloneItems(item.childs, myItem); + + if (myChilds.length > 0) { + myItem.childs = myChilds; + myItems.push(myItem); + } + } else + myItems.push(myItem); } - items.push({ - icon: item.icon, - description: item.description || state.description, - state: item.state, - external: item.external, - url: item.url - }); + return myItems; }; - for (let item of menu) { - if (item.state || item.external) - addItem(items, item); - else { - let childs = []; - - for (let child of item.childs) - addItem(childs, child); - - if (childs.length > 0) { - items.push({ - icon: item.icon, - description: item.description, - childs: childs - }); - } - } - } - - return items; + return cloneItems(menu); } activateItem() { - let myState = this.$state.current.name - .split('.') - .slice(0, this._depth) - .join('.'); - let re = new RegExp(`^${myState}(\\..*)?$`); + if (!this.items) return; + let currentState = this.$state.current.name; + let maxSpecificity = 0; + let selectedItem; - if (this.items) { - // Check items matching current path - for (let item of this.items) { - item.active = re.test(item.state); + function isParentState(state, currentState) { + if (!state) return 0; + let match = state.match(/^(.*)\.index$/); + if (match) state = match[1]; - if (item.childs) { - for (let child of item.childs) { - child.active = re.test(child.state); - if (child.active) - item.active = child.active; - } + let isParent = + currentState.startsWith(`${state}.`) || + currentState === state; + + return isParent + ? (state.match(/\./g) || []).length + 1 + : 0; + } + + function selectItem(items) { + if (!items) return; + for (let item of items) { + item.active = false; + let specificity = isParentState(item.state, currentState); + if (specificity > maxSpecificity) { + selectedItem = item; + maxSpecificity = specificity; } + selectItem(item.childs); } } + + // Check items matching current path + selectItem(this.items); + + while (selectedItem) { + selectedItem.active = true; + selectedItem = selectedItem.parent; + } } setActive(item) { 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..285617a77 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -134,5 +134,12 @@ "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" } \ No newline at end of file diff --git a/modules/account/back/model-config.json b/modules/account/back/model-config.json new file mode 100644 index 000000000..48d7c427d --- /dev/null +++ b/modules/account/back/model-config.json @@ -0,0 +1,24 @@ +{ + "MailAlias": { + "dataSource": "vn" + }, + "MailAliasAccount": { + "dataSource": "vn" + }, + "MailForward": { + "dataSource": "vn" + }, + "RoleInherit": { + "dataSource": "vn" + }, + "RoleRole": { + "dataSource": "vn" + }, + "UserAccount": { + "dataSource": "vn" + }, + "UserPassword": { + "dataSource": "vn" + } + +} \ No newline at end of file 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-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.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.js b/modules/account/back/models/role-role.js new file mode 100644 index 000000000..e2e860187 --- /dev/null +++ b/modules/account/back/models/role-role.js @@ -0,0 +1,20 @@ +const app = require('vn-loopback/server/server'); + +module.exports = Self => { + app.on('started', function() { + let hooks = ['after save', 'after delete']; + for (let hook of hooks) { + app.models.RoleInherit.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-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/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/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..ca87d14b4 --- /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..419a9744a --- /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..56c6770c3 --- /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/basic-data/index.html b/modules/claim/front/basic-data/index.html index 710068196..c9183a229 100644 --- a/modules/claim/front/basic-data/index.html +++ b/modules/claim/front/basic-data/index.html @@ -1,40 +1,35 @@ + 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/front/address/create/index.html b/modules/client/front/address/create/index.html index 89c674a41..1cbe3fb00 100644 --- a/modules/client/front/address/create/index.html +++ b/modules/client/front/address/create/index.html @@ -4,7 +4,7 @@ id-field="id" data="$ctrl.address" params="$ctrl.address" - save="post" + insert-mode="true" form="form"> + insert-mode="true" + form="form"> @@ -15,14 +15,13 @@ rule vn-focus> - + where="{role: 'employee'}"> {{firstName}} {{lastName}} @@ -34,7 +33,6 @@ rule> @@ -49,7 +47,7 @@ - - {{name}}, {{province.name}} @@ -88,34 +86,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/greuge/create/index.html b/modules/client/front/greuge/create/index.html index f2eeacd88..7dd6ab1a8 100644 --- a/modules/client/front/greuge/create/index.html +++ b/modules/client/front/greuge/create/index.html @@ -1,15 +1,14 @@ - + insert-mode="true"> - + \ 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..7df7a571e 100644 --- a/modules/client/front/sample/create/index.html +++ b/modules/client/front/sample/create/index.html @@ -1,40 +1,40 @@ - - + + - - - - - 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/botanical/index.html b/modules/item/front/botanical/index.html index b6d836b30..135a55e6f 100644 --- a/modules/item/front/botanical/index.html +++ b/modules/item/front/botanical/index.html @@ -1,19 +1,17 @@ - - + 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..9fb4586da 100644 --- a/modules/item/front/create/index.html +++ b/modules/item/front/create/index.html @@ -1,27 +1,25 @@ - + insert-mode="true" + form="form">
    - - @@ -34,12 +32,11 @@ -
    {{::id}}
    @@ -48,11 +45,9 @@
    - diff --git a/modules/item/front/tax/index.spec.js b/modules/item/front/tax/index.spec.js index 0aec866b2..9565a861d 100644 --- a/modules/item/front/tax/index.spec.js +++ b/modules/item/front/tax/index.spec.js @@ -46,14 +46,13 @@ describe('Item', () => { 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/route/front/create/index.html b/modules/route/front/create/index.html index 7f6a1f600..0956297cc 100644 --- a/modules/route/front/create/index.html +++ b/modules/route/front/create/index.html @@ -1,50 +1,41 @@ - + insert-mode="true" + form="form"> - + where="{role: 'employee'}"> - + show-field="numberPlate"> + url="AgencyModes"> 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/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/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..e21499cb5 100644 --- a/modules/zone/front/create/index.html +++ b/modules/zone/front/create/index.html @@ -1,9 +1,9 @@ - + insert-mode="true" + form="form"> + rule + vn-focus> @@ -57,7 +48,6 @@ { +// 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%'); });