diff --git a/back/methods/account/change-password.js b/back/methods/account/change-password.js
new file mode 100644
index 000000000..25b63b9a8
--- /dev/null
+++ b/back/methods/account/change-password.js
@@ -0,0 +1,34 @@
+
+module.exports = Self => {
+ Self.remoteMethod('changePassword', {
+ description: 'Changes the user password',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'Number',
+ description: 'The user id',
+ http: {source: 'path'}
+ }, {
+ arg: 'oldPassword',
+ type: 'String',
+ description: 'The old password',
+ required: true
+ }, {
+ arg: 'newPassword',
+ type: 'String',
+ description: 'The new password',
+ required: true
+ }
+ ],
+ http: {
+ path: `/:id/changePassword`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.changePassword = async function(id, oldPassword, newPassword) {
+ await Self.rawSql(`CALL account.user_changePassword(?, ?, ?)`,
+ [id, oldPassword, newPassword]);
+ await Self.app.models.UserAccount.syncById(id, newPassword);
+ };
+};
diff --git a/back/methods/account/login.js b/back/methods/account/login.js
index 075d3669c..340300e23 100644
--- a/back/methods/account/login.js
+++ b/back/methods/account/login.js
@@ -26,9 +26,9 @@ module.exports = Self => {
});
Self.login = async function(user, password) {
+ let $ = Self.app.models;
let token;
let usesEmail = user.indexOf('@') !== -1;
- let User = Self.app.models.User;
let loginInfo = {password};
@@ -38,7 +38,7 @@ module.exports = Self => {
loginInfo.username = user;
try {
- token = await User.login(loginInfo, 'user');
+ token = await $.User.login(loginInfo, 'user');
} catch (err) {
if (err.code != 'LOGIN_FAILED' || usesEmail)
throw err;
@@ -49,17 +49,8 @@ module.exports = Self => {
if (!instance || instance.password !== md5(password || ''))
throw err;
- let where = {id: instance.id};
- let userData = {
- id: instance.id,
- username: user,
- password: password,
- email: instance.email,
- created: instance.created,
- updated: instance.updated
- };
- await User.upsertWithWhere(where, userData);
- token = await User.login(loginInfo, 'user');
+ await $.UserAccount.sync(user, password);
+ token = await $.User.login(loginInfo, 'user');
}
return {token: token.id};
diff --git a/back/methods/account/set-password.js b/back/methods/account/set-password.js
new file mode 100644
index 000000000..fc54b5abe
--- /dev/null
+++ b/back/methods/account/set-password.js
@@ -0,0 +1,29 @@
+
+module.exports = Self => {
+ Self.remoteMethod('setPassword', {
+ description: 'Sets the user password',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'Number',
+ description: 'The user id',
+ http: {source: 'path'}
+ }, {
+ arg: 'newPassword',
+ type: 'String',
+ description: 'The new password',
+ required: true
+ }
+ ],
+ http: {
+ path: `/:id/setPassword`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.setPassword = async function(id, newPassword) {
+ await Self.rawSql(`CALL account.user_setPassword(?, ?)`,
+ [id, newPassword]);
+ await Self.app.models.UserAccount.syncById(id, newPassword);
+ };
+};
diff --git a/back/methods/account/specs/change-password.spec.js b/back/methods/account/specs/change-password.spec.js
new file mode 100644
index 000000000..9f1130df5
--- /dev/null
+++ b/back/methods/account/specs/change-password.spec.js
@@ -0,0 +1,9 @@
+const app = require('vn-loopback/server/server');
+
+describe('account changePassword()', () => {
+ it('should throw an error when old password is wrong', async() => {
+ let req = app.models.Account.changePassword(null, 1, 'wrongOldPass', 'newPass');
+
+ await expectAsync(req).toBeRejected();
+ });
+});
diff --git a/back/methods/account/specs/set-password.spec.js b/back/methods/account/specs/set-password.spec.js
new file mode 100644
index 000000000..c76fd52b8
--- /dev/null
+++ b/back/methods/account/specs/set-password.spec.js
@@ -0,0 +1,15 @@
+const app = require('vn-loopback/server/server');
+
+describe('account changePassword()', () => {
+ it('should throw an error when password does not meet requirements', async() => {
+ let req = app.models.Account.setPassword(1, 'insecurePass');
+
+ await expectAsync(req).toBeRejected();
+ });
+
+ it('should update password when it passes requirements', async() => {
+ let req = app.models.Account.setPassword(1, 'Very$ecurePa22.');
+
+ await expectAsync(req).toBeResolved();
+ });
+});
diff --git a/back/model-config.json b/back/model-config.json
index dc5cde217..22d0e2327 100644
--- a/back/model-config.json
+++ b/back/model-config.json
@@ -38,6 +38,9 @@
"ImageCollectionSize": {
"dataSource": "vn"
},
+ "Language": {
+ "dataSource": "vn"
+ },
"Province": {
"dataSource": "vn"
},
diff --git a/back/models/account.js b/back/models/account.js
index a0b08dd57..1c6ac04f8 100644
--- a/back/models/account.js
+++ b/back/models/account.js
@@ -4,6 +4,8 @@ module.exports = Self => {
require('../methods/account/login')(Self);
require('../methods/account/logout')(Self);
require('../methods/account/acl')(Self);
+ require('../methods/account/change-password')(Self);
+ require('../methods/account/set-password')(Self);
require('../methods/account/validate-token')(Self);
// Validations
@@ -38,17 +40,9 @@ module.exports = Self => {
Self.getCurrentUserData = async function(ctx) {
let userId = ctx.req.accessToken.userId;
-
- let account = await Self.findById(userId, {
+ return await Self.findById(userId, {
fields: ['id', 'name', 'nickname']
});
-
- let worker = await Self.app.models.Worker.findOne({
- fields: ['id'],
- where: {userFk: userId}
- });
-
- return Object.assign(account, {workerId: worker.id});
};
/**
diff --git a/back/models/account.json b/back/models/account.json
index 7186621b4..483973515 100644
--- a/back/models/account.json
+++ b/back/models/account.json
@@ -5,7 +5,7 @@
"mysql": {
"table": "account.user"
}
- },
+ },
"properties": {
"id": {
"type": "number",
@@ -24,10 +24,16 @@
"nickname": {
"type": "string"
},
+ "lang": {
+ "type": "string"
+ },
"password": {
"type": "string",
"required": true
},
+ "bcryptPassword": {
+ "type": "string"
+ },
"active": {
"type": "boolean"
},
@@ -41,44 +47,47 @@
"type": "date"
}
},
- "relations": {
- "role": {
- "type": "belongsTo",
- "model": "Role",
- "foreignKey": "roleFk"
- },
- "emailUser": {
- "type": "hasOne",
- "model": "EmailUser",
- "foreignKey": "userFk"
+ "relations": {
+ "role": {
+ "type": "belongsTo",
+ "model": "Role",
+ "foreignKey": "roleFk"
+ },
+ "roles": {
+ "type": "hasMany",
+ "model": "RoleRole",
+ "foreignKey": "role"
+ },
+ "emailUser": {
+ "type": "hasOne",
+ "model": "EmailUser",
+ "foreignKey": "userFk"
},
"worker": {
"type": "hasOne",
"model": "Worker",
"foreignKey": "userFk"
- }
- },
- "acls": [
- {
+ }
+ },
+ "acls": [
+ {
"property": "login",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
- },
- {
+ }, {
"property": "logout",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
- },
- {
+ }, {
"property": "validateToken",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
- }
- ]
+ }
+ ]
}
diff --git a/back/models/language.json b/back/models/language.json
new file mode 100644
index 000000000..f4e221464
--- /dev/null
+++ b/back/models/language.json
@@ -0,0 +1,34 @@
+{
+ "name": "Language",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "hedera.language"
+ }
+ },
+ "properties": {
+ "code": {
+ "type": "string",
+ "id": true
+ },
+ "name": {
+ "type": "string",
+ "required": true
+ },
+ "orgName": {
+ "type": "string",
+ "required": true
+ },
+ "isActive": {
+ "type": "boolean"
+ }
+ },
+ "acls": [
+ {
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/db/changes/10221-accountModule/00-account.sql b/db/changes/10221-accountModule/00-account.sql
new file mode 100644
index 000000000..d26d61c19
--- /dev/null
+++ b/db/changes/10221-accountModule/00-account.sql
@@ -0,0 +1,127 @@
+
+ALTER TABLE `account`.`role`
+ MODIFY COLUMN `hasLogin` tinyint(3) unsigned DEFAULT 1 NOT NULL;
+
+ALTER TABLE `account`.`roleInherit`
+ ADD UNIQUE( `role`, `inheritsFrom`);
+
+ALTER TABLE `account`.`roleInherit`
+ DROP PRIMARY KEY;
+
+ALTER TABLE `account`.`roleInherit`
+ ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
+ ADD PRIMARY KEY (`id`);
+
+ALTER TABLE `account`.`mailAlias`
+ ADD `description` VARCHAR(255) NULL AFTER `alias`;
+
+ALTER TABLE `account`.`mailAliasAccount`
+ ADD UNIQUE( `mailAlias`, `account`);
+
+ALTER TABLE `account`.`mailAliasAccount`
+ DROP PRIMARY KEY;
+
+ALTER TABLE `account`.`mailAliasAccount`
+ ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
+ ADD PRIMARY KEY (`id`);
+
+ALTER TABLE account.ldapConfig
+ ADD groupDn varchar(255) NULL;
+
+UPDATE account.ldapConfig SET groupDn = 'ou=groups,dc=verdnatura,dc=es';
+
+DROP PROCEDURE IF EXISTS account.user_syncPassword;
+
+ALTER TABLE account.`user`
+ MODIFY COLUMN sync tinyint(4) DEFAULT 0 NOT NULL COMMENT 'Deprecated';
+
+CREATE TABLE account.userSync (
+ name varchar(30) NOT NULL,
+ CONSTRAINT userSync_PK PRIMARY KEY (name)
+)
+ENGINE=InnoDB
+DEFAULT CHARSET=utf8
+COLLATE=utf8_general_ci;
+
+USE account;
+
+DELIMITER $$
+
+DROP TRIGGER IF EXISTS account.user_beforeUpdate$$
+CREATE DEFINER=`root`@`%` TRIGGER `user_beforeUpdate`
+ BEFORE UPDATE ON `user` FOR EACH ROW
+BEGIN
+ IF !(NEW.`name` <=> OLD.`name`) THEN
+ CALL user_checkName (NEW.`name`);
+ END IF;
+
+ IF !(NEW.`password` <=> OLD.`password`) THEN
+ SET NEW.bcryptPassword = NULL;
+ SET NEW.lastPassChange = NOW();
+ END IF;
+END$$
+
+DROP TRIGGER IF EXISTS account.user_afterUpdate$$
+CREATE DEFINER=`root`@`%` TRIGGER `user_afterUpdate`
+ AFTER UPDATE ON `user` FOR EACH ROW
+BEGIN
+ INSERT IGNORE INTO userSync SET `name` = NEW.`name`;
+
+ IF !(OLD.`name` <=> NEW.`name`) THEN
+ INSERT IGNORE INTO userSync SET `name` = OLD.`name`;
+ END IF;
+
+ IF !(NEW.`role` <=> OLD.`role`)
+ THEN
+ INSERT INTO vn.mail SET
+ `sender` = 'jgallego@verdnatura.es',
+ `replyTo` = 'jgallego@verdnatura.es',
+ `subject` = 'Rol modificado',
+ `body` = CONCAT(myUserGetName(), ' ha modificado el rol del usuario ',
+ NEW.`name`, ' de ', OLD.role, ' a ', NEW.role);
+ END IF;
+END$$
+
+CREATE DEFINER=`root`@`%` TRIGGER `user_afterInsert`
+ AFTER INSERT ON `user` FOR EACH ROW
+BEGIN
+ INSERT IGNORE INTO userSync SET `name` = NEW.`name`;
+END$$
+
+CREATE DEFINER=`root`@`%` TRIGGER `user_afterDelete`
+ AFTER DELETE ON `user` FOR EACH ROW
+BEGIN
+ INSERT IGNORE INTO userSync SET `name` = OLD.`name`;
+END$$
+
+DROP TRIGGER IF EXISTS account.account_afterInsert$$
+CREATE DEFINER=`root`@`%` TRIGGER `account_afterInsert`
+ AFTER INSERT ON `account` FOR EACH ROW
+BEGIN
+ INSERT IGNORE INTO userSync (`name`)
+ SELECT `name` FROM `user` WHERE id = NEW.id;
+END$$
+
+DROP TRIGGER IF EXISTS account.account_afterDelete$$
+CREATE DEFINER=`root`@`%` TRIGGER `account_afterDelete`
+ AFTER DELETE ON `account` FOR EACH ROW
+BEGIN
+ INSERT IGNORE INTO userSync (`name`)
+ SELECT `name` FROM `user` WHERE id = OLD.id;
+END$$
+
+CREATE TRIGGER role_beforeInsert
+ BEFORE INSERT ON `role` FOR EACH ROW
+BEGIN
+ CALL role_checkName(NEW.`name`);
+END$$
+
+CREATE TRIGGER role_beforeUpdate
+ BEFORE UPDATE ON `role` FOR EACH ROW
+BEGIN
+ IF !(NEW.`name` <=> OLD.`name`) THEN
+ CALL role_checkName (NEW.`name`);
+ END IF;
+END$$
+
+DELIMITER ;
\ No newline at end of file
diff --git a/db/changes/10221-accountModule/00-myUserChangePassword.sql b/db/changes/10221-accountModule/00-myUserChangePassword.sql
new file mode 100644
index 000000000..94fc02087
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserChangePassword.sql
@@ -0,0 +1,13 @@
+DROP PROCEDURE IF EXISTS account.myUserChangePassword;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUserChangePassword`(vOldPassword VARCHAR(255), vPassword VARCHAR(255))
+BEGIN
+/**
+ * @deprecated Use myUser_changePassword()
+ */
+ CALL myUser_changePassword(vOldPassword, vPassword);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUserChangePassword TO account@localhost;
diff --git a/db/changes/10221-accountModule/00-myUserCheckLogin.sql b/db/changes/10221-accountModule/00-myUserCheckLogin.sql
new file mode 100644
index 000000000..eaa962b63
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserCheckLogin.sql
@@ -0,0 +1,15 @@
+DROP FUNCTION IF EXISTS account.myUserCheckLogin;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserCheckLogin`() RETURNS tinyint(1)
+ READS SQL DATA
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_checkLogin()
+ */
+ RETURN myUser_checkLogin();
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUserCheckLogin TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUserGetId.sql b/db/changes/10221-accountModule/00-myUserGetId.sql
new file mode 100644
index 000000000..f0bb972aa
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserGetId.sql
@@ -0,0 +1,15 @@
+DROP FUNCTION IF EXISTS account.myUserGetId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserGetId`() RETURNS int(11)
+ READS SQL DATA
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_getId()
+ */
+ RETURN myUser_getId();
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUserGetId TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUserGetName.sql b/db/changes/10221-accountModule/00-myUserGetName.sql
new file mode 100644
index 000000000..2f758d0c6
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserGetName.sql
@@ -0,0 +1,15 @@
+DROP FUNCTION IF EXISTS account.myUserGetName;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserGetName`() RETURNS varchar(30) CHARSET utf8
+ NO SQL
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_getName()
+ */
+ RETURN myUser_getName();
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUserGetName TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUserHasRole.sql b/db/changes/10221-accountModule/00-myUserHasRole.sql
new file mode 100644
index 000000000..6d2301328
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserHasRole.sql
@@ -0,0 +1,14 @@
+DROP FUNCTION IF EXISTS account.myUserHasRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserHasRole`(vRoleName VARCHAR(255)) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_hasRole()
+ */
+ RETURN myUser_hasRole(vRoleName);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUserHasRole TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUserHasRoleId.sql b/db/changes/10221-accountModule/00-myUserHasRoleId.sql
new file mode 100644
index 000000000..380bd0641
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUserHasRoleId.sql
@@ -0,0 +1,14 @@
+DROP FUNCTION IF EXISTS account.myUserHasRoleId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUserHasRoleId`(vRoleId INT) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_hasRoleId()
+ */
+ RETURN myUser_hasRoleId(vRoleId);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUserHasRoleId TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_changePassword.sql b/db/changes/10221-accountModule/00-myUser_changePassword.sql
new file mode 100644
index 000000000..3dd86a881
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_changePassword.sql
@@ -0,0 +1,17 @@
+DROP PROCEDURE IF EXISTS account.myUser_changePassword;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_changePassword`(vOldPassword VARCHAR(255), vPassword VARCHAR(255))
+BEGIN
+/**
+ * Changes the current user password, if user is in recovery mode ignores the
+ * current password.
+ *
+ * @param vOldPassword The current password
+ * @param vPassword The new password
+ */
+ CALL user_changePassword(myUser_getId(), vOldPassword, vPassword);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUser_changePassword TO account@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_checkLogin.sql b/db/changes/10221-accountModule/00-myUser_checkLogin.sql
new file mode 100644
index 000000000..843f57fff
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_checkLogin.sql
@@ -0,0 +1,29 @@
+DROP FUNCTION IF EXISTS account.myUser_checkLogin;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_checkLogin`() RETURNS tinyint(1)
+ READS SQL DATA
+ DETERMINISTIC
+BEGIN
+/**
+ * Checks that variables @userId and @userName haven't been altered.
+ *
+ * @return %TRUE if they are unaltered or unset, otherwise %FALSE
+ */
+ DECLARE vSignature VARCHAR(128);
+ DECLARE vKey VARCHAR(255);
+
+ IF @userId IS NOT NULL
+ AND @userName IS NOT NULL
+ AND @userSignature IS NOT NULL
+ THEN
+ SELECT loginKey INTO vKey FROM userConfig;
+ SET vSignature = util.hmacSha2(256, CONCAT_WS('/', @userId, @userName), vKey);
+ RETURN vSignature = @userSignature;
+ END IF;
+
+ RETURN FALSE;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUser_checkLogin TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_getId.sql b/db/changes/10221-accountModule/00-myUser_getId.sql
new file mode 100644
index 000000000..b3d3f1b28
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_getId.sql
@@ -0,0 +1,27 @@
+DROP FUNCTION IF EXISTS account.myUser_getId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_getId`() RETURNS int(11)
+ READS SQL DATA
+ DETERMINISTIC
+BEGIN
+/**
+ * Returns the current user id.
+ *
+ * @return The user id
+ */
+ DECLARE vUser INT DEFAULT NULL;
+
+ IF myUser_checkLogin()
+ THEN
+ SET vUser = @userId;
+ ELSE
+ SELECT id INTO vUser FROM user
+ WHERE name = LEFT(USER(), INSTR(USER(), '@') - 1);
+ END IF;
+
+ RETURN vUser;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUser_getId TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_getName.sql b/db/changes/10221-accountModule/00-myUser_getName.sql
new file mode 100644
index 000000000..b055227d3
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_getName.sql
@@ -0,0 +1,27 @@
+DROP FUNCTION IF EXISTS account.myUser_getName;
+
+DELIMITER $$
+$$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_getName`() RETURNS varchar(30) CHARSET utf8
+ NO SQL
+ DETERMINISTIC
+BEGIN
+/**
+ * Returns the current user name.
+ *
+ * @return The user name
+ */
+ DECLARE vUser VARCHAR(30) DEFAULT NULL;
+
+ IF myUser_checkLogin()
+ THEN
+ SET vUser = @userName;
+ ELSE
+ SET vUser = LEFT(USER(), INSTR(USER(), '@') - 1);
+ END IF;
+
+ RETURN vUser;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUser_getName TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_hasRole.sql b/db/changes/10221-accountModule/00-myUser_hasRole.sql
new file mode 100644
index 000000000..538b58f08
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_hasRole.sql
@@ -0,0 +1,17 @@
+DROP FUNCTION IF EXISTS account.myUser_hasRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_hasRole`(vRoleName VARCHAR(255)) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * Checks if current user has/inherits a role.
+ *
+ * @param vRoleName Role to check
+ * @return %TRUE if it has role, %FALSE otherwise
+ */
+ RETURN user_hasRole(myUser_getName(), vRoleName);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUser_hasRole TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_hasRoleId.sql b/db/changes/10221-accountModule/00-myUser_hasRoleId.sql
new file mode 100644
index 000000000..2931443e1
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_hasRoleId.sql
@@ -0,0 +1,17 @@
+DROP FUNCTION IF EXISTS account.myUser_hasRoleId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`myUser_hasRoleId`(vRoleId INT) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * Checks if current user has/inherits a role.
+ *
+ * @param vRoleName Role id to check
+ * @return %TRUE if it has role, %FALSE otherwise
+ */
+ RETURN user_hasRoleId(myUserGetName(), vRoleId);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.myUser_hasRoleId TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_login.sql b/db/changes/10221-accountModule/00-myUser_login.sql
new file mode 100644
index 000000000..9d92828b0
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_login.sql
@@ -0,0 +1,29 @@
+DROP PROCEDURE IF EXISTS account.myUser_login;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_login`(vUserName VARCHAR(255), vPassword VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * Logs in using the user credentials.
+ *
+ * @param vUserName The user name
+ * @param vPassword The user password
+ */
+ DECLARE vAuthIsOk BOOLEAN DEFAULT FALSE;
+
+ SELECT COUNT(*) = 1 INTO vAuthIsOk FROM user
+ WHERE name = vUserName
+ AND password = MD5(vPassword)
+ AND active;
+
+ IF vAuthIsOk
+ THEN
+ CALL myUser_loginWithName (vUserName);
+ ELSE
+ CALL util.throw ('INVALID_CREDENTIALS');
+ END IF;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUser_login TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_loginWithKey.sql b/db/changes/10221-accountModule/00-myUser_loginWithKey.sql
new file mode 100644
index 000000000..fc12a79d9
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_loginWithKey.sql
@@ -0,0 +1,25 @@
+DROP PROCEDURE IF EXISTS account.myUser_loginWithKey;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_loginWithKey`(vUserName VARCHAR(255), vKey VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * Logs in using the user name and MySQL master key.
+ *
+ * @param vUserName The user name
+ * @param vKey The MySQL master key
+ */
+ DECLARE vLoginKey VARCHAR(255);
+
+ SELECT loginKey INTO vLoginKey FROM userConfig;
+
+ IF vLoginKey = vKey THEN
+ CALL user_loginWithName(vUserName);
+ ELSE
+ CALL util.throw('INVALID_KEY');
+ END IF;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUser_loginWithKey TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-myUser_loginWithName.sql b/db/changes/10221-accountModule/00-myUser_loginWithName.sql
new file mode 100644
index 000000000..6b86a37f3
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_loginWithName.sql
@@ -0,0 +1,26 @@
+DROP PROCEDURE IF EXISTS account.myUser_loginWithName;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_loginWithName`(vUserName VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * Logs in using only the user name. This procedure is intended to be executed
+ * by users with a high level of privileges so that normal users should not have
+ * execute permissions on it.
+ *
+ * @param vUserName The user name
+ */
+ DECLARE vUserId INT DEFAULT NULL;
+ DECLARE vKey VARCHAR(255);
+
+ SELECT id INTO vUserId FROM user
+ WHERE name = vUserName;
+
+ SELECT loginKey INTO vKey FROM userConfig;
+
+ SET @userId = vUserId;
+ SET @userName = vUserName;
+ SET @userSignature = util.hmacSha2(256, CONCAT_WS('/', vUserId, vUserName), vKey);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-myUser_logout.sql b/db/changes/10221-accountModule/00-myUser_logout.sql
new file mode 100644
index 000000000..ffa2c969e
--- /dev/null
+++ b/db/changes/10221-accountModule/00-myUser_logout.sql
@@ -0,0 +1,15 @@
+DROP PROCEDURE IF EXISTS account.myUser_logout;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUser_logout`()
+BEGIN
+/**
+ * Logouts the user.
+ */
+ SET @userId = NULL;
+ SET @userName = NULL;
+ SET @userSignature = NULL;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUser_logout TO account@localhost;
diff --git a/db/changes/10221-accountModule/00-passwordGenerate.sql b/db/changes/10221-accountModule/00-passwordGenerate.sql
new file mode 100644
index 000000000..46048e24d
--- /dev/null
+++ b/db/changes/10221-accountModule/00-passwordGenerate.sql
@@ -0,0 +1,52 @@
+DROP FUNCTION IF EXISTS account.passwordGenerate;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`passwordGenerate`() RETURNS text CHARSET utf8
+BEGIN
+/**
+ * Generates a random password that meets the minimum requirements.
+ *
+ * @return Generated password
+ */
+ DECLARE vMinLength TINYINT;
+ DECLARE vMinAlpha TINYINT;
+ DECLARE vMinUpper TINYINT;
+ DECLARE vMinDigits TINYINT;
+ DECLARE vMinPunct TINYINT;
+ DECLARE vAlpha TINYINT DEFAULT 0;
+ DECLARE vUpper TINYINT DEFAULT 0;
+ DECLARE vDigits TINYINT DEFAULT 0;
+ DECLARE vPunct TINYINT DEFAULT 0;
+ DECLARE vRandIndex INT;
+ DECLARE vPwd TEXT DEFAULT '';
+
+ DECLARE vAlphaChars TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyz';
+ DECLARE vUpperChars TEXT DEFAULT 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ DECLARE vDigitChars TEXT DEFAULT '1234567890';
+ DECLARE vPunctChars TEXT DEFAULT '!$%&()=.';
+
+ SELECT length, nAlpha, nUpper, nDigits, nPunct
+ INTO vMinLength, vMinAlpha, vMinUpper, vMinDigits, vMinPunct FROM userPassword;
+
+ WHILE LENGTH(vPwd) < vMinLength OR vAlpha < vMinAlpha
+ OR vUpper < vMinUpper OR vDigits < vMinDigits OR vPunct < vMinPunct DO
+ SET vRandIndex = FLOOR((RAND() * 4) + 1);
+
+ CASE
+ WHEN vRandIndex = 1 THEN
+ SET vPwd = CONCAT(vPwd, SUBSTRING(vAlphaChars, FLOOR((RAND() * 26) + 1), 1));
+ SET vAlpha = vAlpha + 1;
+ WHEN vRandIndex = 2 THEN
+ SET vPwd = CONCAT(vPwd, SUBSTRING(vUpperChars, FLOOR((RAND() * 26) + 1), 1));
+ SET vUpper = vUpper + 1;
+ WHEN vRandIndex = 3 THEN
+ SET vPwd = CONCAT(vPwd, SUBSTRING(vDigitChars, FLOOR((RAND() * 10) + 1), 1));
+ SET vDigits = vDigits + 1;
+ WHEN vRandIndex = 4 THEN
+ SET vPwd = CONCAT(vPwd, SUBSTRING(vPunctChars, FLOOR((RAND() * LENGTH(vPunctChars)) + 1), 1));
+ SET vPunct = vPunct + 1;
+ END CASE;
+ END WHILE;
+ RETURN vPwd;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-role_checkName.sql b/db/changes/10221-accountModule/00-role_checkName.sql
new file mode 100644
index 000000000..1e4f31767
--- /dev/null
+++ b/db/changes/10221-accountModule/00-role_checkName.sql
@@ -0,0 +1,18 @@
+DROP PROCEDURE IF EXISTS account.role_checkName;
+
+DELIMITER $$
+CREATE PROCEDURE account.role_checkName(vRoleName VARCHAR(255))
+BEGIN
+/**
+ * Checks that role name meets the necessary syntax requirements, otherwise it
+ * throws an exception.
+ * Role name must be written in camelCase.
+ *
+ * @param vRoleName The role name
+ */
+ IF BINARY vRoleName NOT REGEXP '^[a-z][a-zA-Z]+$' THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = 'Role name must be written in camelCase';
+ END IF;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-role_getDescendents.sql b/db/changes/10221-accountModule/00-role_getDescendents.sql
new file mode 100644
index 000000000..9b224f6eb
--- /dev/null
+++ b/db/changes/10221-accountModule/00-role_getDescendents.sql
@@ -0,0 +1,66 @@
+DROP PROCEDURE IF EXISTS account.role_getDescendents;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_getDescendents`(vSelf INT)
+BEGIN
+/**
+ * Gets the identifiers of all the subroles implemented by a role (Including
+ * itself).
+ *
+ * @param vSelf The role identifier
+ * @table tmp.role Subroles implemented by the role
+ */
+ DECLARE vIsRoot BOOL;
+
+ DROP TEMPORARY TABLE IF EXISTS
+ tmp.role, parents, childs;
+
+ CREATE TEMPORARY TABLE tmp.role
+ (UNIQUE (id))
+ ENGINE = MEMORY
+ SELECT vSelf AS id;
+
+ CREATE TEMPORARY TABLE parents
+ ENGINE = MEMORY
+ SELECT vSelf AS id;
+
+ CREATE TEMPORARY TABLE childs
+ LIKE parents;
+
+ REPEAT
+ DELETE FROM childs;
+ INSERT INTO childs
+ SELECT DISTINCT r.inheritsFrom id
+ FROM parents p
+ JOIN roleInherit r ON r.role = p.id
+ LEFT JOIN tmp.role t ON t.id = r.inheritsFrom
+ WHERE t.id IS NULL;
+
+ DELETE FROM parents;
+ INSERT INTO parents
+ SELECT * FROM childs;
+
+ INSERT INTO tmp.role
+ SELECT * FROM childs;
+
+ UNTIL ROW_COUNT() <= 0
+ END REPEAT;
+
+ -- If it is root all the roles are added
+
+ SELECT COUNT(*) > 0 INTO vIsRoot
+ FROM tmp.role t
+ JOIN role r ON r.id = t.id
+ WHERE r.`name` = 'root';
+
+ IF vIsRoot THEN
+ INSERT IGNORE INTO tmp.role (id)
+ SELECT id FROM role;
+ END IF;
+
+ -- Cleaning
+
+ DROP TEMPORARY TABLE
+ parents, childs;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-role_sync.sql b/db/changes/10221-accountModule/00-role_sync.sql
new file mode 100644
index 000000000..8e16ef567
--- /dev/null
+++ b/db/changes/10221-accountModule/00-role_sync.sql
@@ -0,0 +1,53 @@
+DROP PROCEDURE IF EXISTS account.role_sync;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_sync`()
+BEGIN
+/**
+ * Synchronize the @roleRole table with the current role hierarchy. This
+ * procedure must be called every time the @roleInherit table is modified so
+ * that the changes made on it are effective.
+ */
+ DECLARE vRoleId INT;
+ DECLARE vDone BOOL;
+
+ DECLARE cur CURSOR FOR
+ SELECT id FROM role;
+
+ DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
+
+ DROP TEMPORARY TABLE IF EXISTS tRoleRole;
+ CREATE TEMPORARY TABLE tRoleRole
+ ENGINE = MEMORY
+ SELECT * FROM roleRole LIMIT 0;
+
+ OPEN cur;
+
+ l: LOOP
+ SET vDone = FALSE;
+ FETCH cur INTO vRoleId;
+
+ IF vDone THEN
+ LEAVE l;
+ END IF;
+
+ CALL role_getDescendents(vRoleId);
+
+ INSERT INTO tRoleRole (role, inheritsFrom)
+ SELECT vRoleId, id FROM tmp.role;
+
+ DROP TEMPORARY TABLE tmp.role;
+ END LOOP;
+
+ CLOSE cur;
+
+ START TRANSACTION;
+ DELETE FROM roleRole;
+ INSERT INTO roleRole SELECT * FROM tRoleRole;
+ COMMIT;
+
+ DROP TEMPORARY TABLE tRoleRole;
+
+ CALL role_syncPrivileges;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-role_syncPrivileges.sql b/db/changes/10221-accountModule/00-role_syncPrivileges.sql
new file mode 100644
index 000000000..0d6d8975b
--- /dev/null
+++ b/db/changes/10221-accountModule/00-role_syncPrivileges.sql
@@ -0,0 +1,494 @@
+DROP PROCEDURE IF EXISTS account.role_syncPrivileges;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_syncPrivileges`()
+BEGIN
+/**
+ * Synchronizes permissions of MySQL role users based on role hierarchy.
+ * The computed role users of permission mix will be named according to
+ * pattern z-[role_name].
+ *
+ * If any@localhost user exists, it will be taken as a template for basic
+ * attributes.
+ *
+ * Warning! This procedure should only be called when MySQL privileges
+ * are modified. If role hierarchy is modified, you must call the role_sync()
+ * procedure wich calls this internally.
+ */
+ DECLARE vIsMysql BOOL DEFAULT VERSION() NOT LIKE '%MariaDB%';
+ DECLARE vVersion INT DEFAULT SUBSTRING_INDEX(VERSION(), '.', 1);
+ DECLARE vTplUser VARCHAR(255) DEFAULT 'any';
+ DECLARE vTplHost VARCHAR(255) DEFAULT '%';
+ DECLARE vRoleHost VARCHAR(255) DEFAULT 'localhost';
+ DECLARE vAllHost VARCHAR(255) DEFAULT '%';
+ DECLARE vPrefix VARCHAR(2) DEFAULT 'z-';
+ DECLARE vPrefixedLike VARCHAR(255);
+ DECLARE vPassword VARCHAR(255) DEFAULT '';
+
+ -- Deletes computed role users
+
+ SET vPrefixedLike = CONCAT(vPrefix, '%');
+
+ DELETE FROM mysql.user
+ WHERE `User` LIKE vPrefixedLike;
+
+ DELETE FROM mysql.db
+ WHERE `User` LIKE vPrefixedLike;
+
+ DELETE FROM mysql.tables_priv
+ WHERE `User` LIKE vPrefixedLike;
+
+ DELETE FROM mysql.columns_priv
+ WHERE `User` LIKE vPrefixedLike;
+
+ DELETE FROM mysql.procs_priv
+ WHERE `User` LIKE vPrefixedLike;
+
+ DELETE FROM mysql.proxies_priv
+ WHERE `Proxied_user` LIKE vPrefixedLike;
+
+ -- Temporary tables
+
+ DROP TEMPORARY TABLE IF EXISTS tRole;
+ CREATE TEMPORARY TABLE tRole
+ (INDEX (id))
+ ENGINE = MEMORY
+ SELECT
+ id,
+ `name` role,
+ CONCAT(vPrefix, `name`) prefixedRole
+ FROM role
+ WHERE hasLogin;
+
+ DROP TEMPORARY TABLE IF EXISTS tRoleInherit;
+ CREATE TEMPORARY TABLE tRoleInherit
+ (INDEX (inheritsFrom))
+ ENGINE = MEMORY
+ SELECT
+ r.prefixedRole,
+ ri.`name` inheritsFrom
+ FROM tRole r
+ JOIN roleRole rr ON rr.role = r.id
+ JOIN role ri ON ri.id = rr.inheritsFrom;
+
+ -- Recreate role users
+
+ IF vIsMysql THEN
+ DROP TEMPORARY TABLE IF EXISTS tUser;
+ CREATE TEMPORARY TABLE tUser
+ SELECT
+ r.prefixedRole `User`,
+ vTplHost `Host`,
+ IFNULL(t.`authentication_string`,
+ '') `authentication_string`,
+ IFNULL(t.`plugin`,
+ 'mysql_native_password') `plugin`,
+ IFNULL(IF('' != u.`ssl_type`,
+ u.`ssl_type`, t.`ssl_type`),
+ '') `ssl_type`,
+ IFNULL(IF('' != u.`ssl_cipher`,
+ u.`ssl_cipher`, t.`ssl_cipher`),
+ '') `ssl_cipher`,
+ IFNULL(IF('' != u.`x509_issuer`,
+ u.`x509_issuer`, t.`x509_issuer`),
+ '') `x509_issuer`,
+ IFNULL(IF('' != u.`x509_subject`,
+ u.`x509_subject`, t.`x509_subject`),
+ '') `x509_subject`,
+ IFNULL(IF(0 != u.`max_questions`,
+ u.`max_questions`, t.`max_questions`),
+ 0) `max_questions`,
+ IFNULL(IF(0 != u.`max_updates`,
+ u.`max_updates`, t.`max_updates`),
+ 0) `max_updates`,
+ IFNULL(IF(0 != u.`max_connections`,
+ u.`max_connections`, t.`max_connections`),
+ 0) `max_connections`,
+ IFNULL(IF(0 != u.`max_user_connections`,
+ u.`max_user_connections`, t.`max_user_connections`),
+ 0) `max_user_connections`
+ FROM tRole r
+ LEFT JOIN mysql.user t
+ ON t.`User` = vTplUser
+ AND t.`Host` = vRoleHost
+ LEFT JOIN mysql.user u
+ ON u.`User` = r.role
+ AND u.`Host` = vRoleHost;
+
+ IF vVersion <= 5 THEN
+ SELECT `Password` INTO vPassword
+ FROM mysql.user
+ WHERE `User` = vTplUser
+ AND `Host` = vRoleHost;
+
+ INSERT INTO mysql.user (
+ `User`,
+ `Host`,
+ `Password`,
+ `authentication_string`,
+ `plugin`,
+ `ssl_type`,
+ `ssl_cipher`,
+ `x509_issuer`,
+ `x509_subject`,
+ `max_questions`,
+ `max_updates`,
+ `max_connections`,
+ `max_user_connections`
+ )
+ SELECT
+ `User`,
+ `Host`,
+ vPassword,
+ `authentication_string`,
+ `plugin`,
+ `ssl_type`,
+ `ssl_cipher`,
+ `x509_issuer`,
+ `x509_subject`,
+ `max_questions`,
+ `max_updates`,
+ `max_connections`,
+ `max_user_connections`
+ FROM tUser;
+ ELSE
+ INSERT INTO mysql.user (
+ `User`,
+ `Host`,
+ `authentication_string`,
+ `plugin`,
+ `ssl_type`,
+ `ssl_cipher`,
+ `x509_issuer`,
+ `x509_subject`,
+ `max_questions`,
+ `max_updates`,
+ `max_connections`,
+ `max_user_connections`
+ )
+ SELECT
+ `User`,
+ `Host`,
+ `authentication_string`,
+ `plugin`,
+ `ssl_type`,
+ `ssl_cipher`,
+ `x509_issuer`,
+ `x509_subject`,
+ `max_questions`,
+ `max_updates`,
+ `max_connections`,
+ `max_user_connections`
+ FROM tUser;
+ END IF;
+
+ DROP TEMPORARY TABLE IF EXISTS tUser;
+ ELSE
+ INSERT INTO mysql.global_priv (
+ `User`,
+ `Host`,
+ `Priv`
+ )
+ SELECT
+ r.prefixedRole,
+ vTplHost,
+ JSON_MERGE_PATCH(
+ IFNULL(t.`Priv`, '{}'),
+ IFNULL(u.`Priv`, '{}')
+ )
+ FROM tRole r
+ LEFT JOIN mysql.global_priv t
+ ON t.`User` = vTplUser
+ AND t.`Host` = vRoleHost
+ LEFT JOIN mysql.global_priv u
+ ON u.`User` = r.role
+ AND u.`Host` = vRoleHost;
+ END IF;
+
+ INSERT INTO mysql.proxies_priv (
+ `User`,
+ `Host`,
+ `Proxied_user`,
+ `Proxied_host`,
+ `Grantor`
+ )
+ SELECT
+ '',
+ vAllHost,
+ prefixedRole,
+ vTplHost,
+ CONCAT(prefixedRole, '@', vTplHost)
+ FROM tRole;
+
+ -- Copies global privileges
+
+ DROP TEMPORARY TABLE IF EXISTS tUserPriv;
+
+ IF vIsMysql THEN
+ CREATE TEMPORARY TABLE tUserPriv
+ (INDEX (prefixedRole))
+ ENGINE = MEMORY
+ SELECT
+ r.prefixedRole,
+ MAX(u.`Select_priv`) `Select_priv`,
+ MAX(u.`Insert_priv`) `Insert_priv`,
+ MAX(u.`Update_priv`) `Update_priv`,
+ MAX(u.`Delete_priv`) `Delete_priv`,
+ MAX(u.`Create_priv`) `Create_priv`,
+ MAX(u.`Drop_priv`) `Drop_priv`,
+ MAX(u.`Reload_priv`) `Reload_priv`,
+ MAX(u.`Shutdown_priv`) `Shutdown_priv`,
+ MAX(u.`Process_priv`) `Process_priv`,
+ MAX(u.`File_priv`) `File_priv`,
+ MAX(u.`Grant_priv`) `Grant_priv`,
+ MAX(u.`References_priv`) `References_priv`,
+ MAX(u.`Index_priv`) `Index_priv`,
+ MAX(u.`Alter_priv`) `Alter_priv`,
+ MAX(u.`Show_db_priv`) `Show_db_priv`,
+ MAX(u.`Super_priv`) `Super_priv`,
+ MAX(u.`Create_tmp_table_priv`) `Create_tmp_table_priv`,
+ MAX(u.`Lock_tables_priv`) `Lock_tables_priv`,
+ MAX(u.`Execute_priv`) `Execute_priv`,
+ MAX(u.`Repl_slave_priv`) `Repl_slave_priv`,
+ MAX(u.`Repl_client_priv`) `Repl_client_priv`,
+ MAX(u.`Create_view_priv`) `Create_view_priv`,
+ MAX(u.`Show_view_priv`) `Show_view_priv`,
+ MAX(u.`Create_routine_priv`) `Create_routine_priv`,
+ MAX(u.`Alter_routine_priv`) `Alter_routine_priv`,
+ MAX(u.`Create_user_priv`) `Create_user_priv`,
+ MAX(u.`Event_priv`) `Event_priv`,
+ MAX(u.`Trigger_priv`) `Trigger_priv`,
+ MAX(u.`Create_tablespace_priv`) `Create_tablespace_priv`
+ FROM tRoleInherit r
+ JOIN mysql.user u
+ ON u.`User` = r.inheritsFrom
+ AND u.`Host`= vRoleHost
+ GROUP BY r.prefixedRole;
+
+ UPDATE mysql.user u
+ JOIN tUserPriv t
+ ON u.`User` = t.prefixedRole
+ AND u.`Host` = vTplHost
+ SET
+ u.`Select_priv`
+ = t.`Select_priv`,
+ u.`Insert_priv`
+ = t.`Insert_priv`,
+ u.`Update_priv`
+ = t.`Update_priv`,
+ u.`Delete_priv`
+ = t.`Delete_priv`,
+ u.`Create_priv`
+ = t.`Create_priv`,
+ u.`Drop_priv`
+ = t.`Drop_priv`,
+ u.`Reload_priv`
+ = t.`Reload_priv`,
+ u.`Shutdown_priv`
+ = t.`Shutdown_priv`,
+ u.`Process_priv`
+ = t.`Process_priv`,
+ u.`File_priv`
+ = t.`File_priv`,
+ u.`Grant_priv`
+ = t.`Grant_priv`,
+ u.`References_priv`
+ = t.`References_priv`,
+ u.`Index_priv`
+ = t.`Index_priv`,
+ u.`Alter_priv`
+ = t.`Alter_priv`,
+ u.`Show_db_priv`
+ = t.`Show_db_priv`,
+ u.`Super_priv`
+ = t.`Super_priv`,
+ u.`Create_tmp_table_priv`
+ = t.`Create_tmp_table_priv`,
+ u.`Lock_tables_priv`
+ = t.`Lock_tables_priv`,
+ u.`Execute_priv`
+ = t.`Execute_priv`,
+ u.`Repl_slave_priv`
+ = t.`Repl_slave_priv`,
+ u.`Repl_client_priv`
+ = t.`Repl_client_priv`,
+ u.`Create_view_priv`
+ = t.`Create_view_priv`,
+ u.`Show_view_priv`
+ = t.`Show_view_priv`,
+ u.`Create_routine_priv`
+ = t.`Create_routine_priv`,
+ u.`Alter_routine_priv`
+ = t.`Alter_routine_priv`,
+ u.`Create_user_priv`
+ = t.`Create_user_priv`,
+ u.`Event_priv`
+ = t.`Event_priv`,
+ u.`Trigger_priv`
+ = t.`Trigger_priv`,
+ u.`Create_tablespace_priv`
+ = t.`Create_tablespace_priv`;
+ ELSE
+ CREATE TEMPORARY TABLE tUserPriv
+ (INDEX (prefixedRole))
+ SELECT
+ r.prefixedRole,
+ BIT_OR(JSON_VALUE(p.`Priv`, '$.access')) access
+ FROM tRoleInherit r
+ JOIN mysql.global_priv p
+ ON p.`User` = r.inheritsFrom
+ AND p.`Host`= vRoleHost
+ GROUP BY r.prefixedRole;
+
+ UPDATE mysql.global_priv p
+ JOIN tUserPriv t
+ ON p.`User` = t.prefixedRole
+ AND p.`Host` = vTplHost
+ SET
+ p.`Priv` = JSON_SET(p.`Priv`, '$.access', t.access);
+ END IF;
+
+ DROP TEMPORARY TABLE tUserPriv;
+
+ -- Copy schema level privileges
+
+ INSERT INTO mysql.db (
+ `User`,
+ `Host`,
+ `Db`,
+ `Select_priv`,
+ `Insert_priv`,
+ `Update_priv`,
+ `Delete_priv`,
+ `Create_priv`,
+ `Drop_priv`,
+ `Grant_priv`,
+ `References_priv`,
+ `Index_priv`,
+ `Alter_priv`,
+ `Create_tmp_table_priv`,
+ `Lock_tables_priv`,
+ `Create_view_priv`,
+ `Show_view_priv`,
+ `Create_routine_priv`,
+ `Alter_routine_priv`,
+ `Execute_priv`,
+ `Event_priv`,
+ `Trigger_priv`
+ )
+ SELECT
+ r.prefixedRole,
+ vTplHost,
+ t.`Db`,
+ MAX(t.`Select_priv`),
+ MAX(t.`Insert_priv`),
+ MAX(t.`Update_priv`),
+ MAX(t.`Delete_priv`),
+ MAX(t.`Create_priv`),
+ MAX(t.`Drop_priv`),
+ MAX(t.`Grant_priv`),
+ MAX(t.`References_priv`),
+ MAX(t.`Index_priv`),
+ MAX(t.`Alter_priv`),
+ MAX(t.`Create_tmp_table_priv`),
+ MAX(t.`Lock_tables_priv`),
+ MAX(t.`Create_view_priv`),
+ MAX(t.`Show_view_priv`),
+ MAX(t.`Create_routine_priv`),
+ MAX(t.`Alter_routine_priv`),
+ MAX(t.`Execute_priv`),
+ MAX(t.`Event_priv`),
+ MAX(t.`Trigger_priv`)
+ FROM tRoleInherit r
+ JOIN mysql.db t
+ ON t.`User` = r.inheritsFrom
+ AND t.`Host`= vRoleHost
+ GROUP BY r.prefixedRole, t.`Db`;
+
+ -- Copy table level privileges
+
+ INSERT INTO mysql.tables_priv (
+ `User`,
+ `Host`,
+ `Db`,
+ `Table_name`,
+ `Grantor`,
+ `Timestamp`,
+ `Table_priv`,
+ `Column_priv`
+ )
+ SELECT
+ r.prefixedRole,
+ vTplHost,
+ t.`Db`,
+ t.`Table_name`,
+ t.`Grantor`,
+ MAX(t.`Timestamp`),
+ IFNULL(GROUP_CONCAT(NULLIF(t.`Table_priv`, '')), ''),
+ IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '')
+ FROM tRoleInherit r
+ JOIN mysql.tables_priv t
+ ON t.`User` = r.inheritsFrom
+ AND t.`Host`= vRoleHost
+ GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`;
+
+ -- Copy column level privileges
+
+ INSERT INTO mysql.columns_priv (
+ `User`,
+ `Host`,
+ `Db`,
+ `Table_name`,
+ `Column_name`,
+ `Timestamp`,
+ `Column_priv`
+ )
+ SELECT
+ r.prefixedRole,
+ vTplHost,
+ t.`Db`,
+ t.`Table_name`,
+ t.`Column_name`,
+ MAX(t.`Timestamp`),
+ IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '')
+ FROM tRoleInherit r
+ JOIN mysql.columns_priv t
+ ON t.`User` = r.inheritsFrom
+ AND t.`Host`= vRoleHost
+ GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`, t.`Column_name`;
+
+ -- Copy routine privileges
+
+ INSERT IGNORE INTO mysql.procs_priv (
+ `User`,
+ `Host`,
+ `Db`,
+ `Routine_name`,
+ `Routine_type`,
+ `Grantor`,
+ `Timestamp`,
+ `Proc_priv`
+ )
+ SELECT
+ r.prefixedRole,
+ vTplHost,
+ t.`Db`,
+ t.`Routine_name`,
+ t.`Routine_type`,
+ t.`Grantor`,
+ t.`Timestamp`,
+ t.`Proc_priv`
+ FROM tRoleInherit r
+ JOIN mysql.procs_priv t
+ ON t.`User` = r.inheritsFrom
+ AND t.`Host`= vRoleHost;
+
+ -- Free memory
+
+ DROP TEMPORARY TABLE
+ tRole,
+ tRoleInherit;
+
+ FLUSH PRIVILEGES;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-userGetId.sql b/db/changes/10221-accountModule/00-userGetId.sql
new file mode 100644
index 000000000..219ea680c
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userGetId.sql
@@ -0,0 +1,15 @@
+DROP FUNCTION IF EXISTS account.userGetId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetId`() RETURNS int(11)
+ READS SQL DATA
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_getId()
+ */
+ RETURN myUser_getId();
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.userGetId TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-userGetMysqlRole.sql b/db/changes/10221-accountModule/00-userGetMysqlRole.sql
new file mode 100644
index 000000000..673f1aac9
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userGetMysqlRole.sql
@@ -0,0 +1,11 @@
+DROP FUNCTION IF EXISTS account.userGetMysqlRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetMysqlRole`(vUserName VARCHAR(255)) RETURNS varchar(255) CHARSET utf8
+BEGIN
+/**
+ * @deprecated Use user_getMysqlRole()
+ */
+ RETURN user_getMysqlRole();
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-userGetName.sql b/db/changes/10221-accountModule/00-userGetName.sql
new file mode 100644
index 000000000..f49f2dbef
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userGetName.sql
@@ -0,0 +1,15 @@
+DROP FUNCTION IF EXISTS account.userGetName;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetName`() RETURNS varchar(30) CHARSET utf8
+ NO SQL
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use myUser_getName()
+ */
+ RETURN myUser_getName();
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON FUNCTION account.userGetName TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-userGetNameFromId.sql b/db/changes/10221-accountModule/00-userGetNameFromId.sql
new file mode 100644
index 000000000..f8e9333cb
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userGetNameFromId.sql
@@ -0,0 +1,11 @@
+DROP FUNCTION IF EXISTS account.userGetNameFromId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userGetNameFromId`(vSelf INT) RETURNS varchar(30) CHARSET utf8
+BEGIN
+/**
+ * @deprecated Use user_getNameFromId();
+ */
+ RETURN user_getNameFromId(vSelf);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-userHasRole.sql b/db/changes/10221-accountModule/00-userHasRole.sql
new file mode 100644
index 000000000..3e09d27bf
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userHasRole.sql
@@ -0,0 +1,12 @@
+DROP FUNCTION IF EXISTS account.userHasRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userHasRole`(vUserName VARCHAR(255), vRoleName VARCHAR(255)) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use user_hasRole()
+ */
+ RETURN user_hasRole(vUserName, vRoleName);
+END$$
+DELIMITER ;
\ No newline at end of file
diff --git a/db/changes/10221-accountModule/00-userHasRoleId.sql b/db/changes/10221-accountModule/00-userHasRoleId.sql
new file mode 100644
index 000000000..9fcd9f073
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userHasRoleId.sql
@@ -0,0 +1,12 @@
+DROP FUNCTION IF EXISTS account.userHasRoleId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`userHasRoleId`(vUser VARCHAR(255), vRoleId INT) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * @deprecated Use user_hasRoleId()
+ */
+ RETURN user_hasRoleId(vUser, vRoleId);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-userLogin.sql b/db/changes/10221-accountModule/00-userLogin.sql
new file mode 100644
index 000000000..63a332254
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userLogin.sql
@@ -0,0 +1,14 @@
+DROP PROCEDURE IF EXISTS account.userLogin;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLogin`(vUserName VARCHAR(255), vPassword VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * @deprecated Use myUser_login()
+ */
+ CALL myUser_login(vUserName, vPassword);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.userLogin TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-userLoginWithKey.sql b/db/changes/10221-accountModule/00-userLoginWithKey.sql
new file mode 100644
index 000000000..45a490c71
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userLoginWithKey.sql
@@ -0,0 +1,14 @@
+DROP PROCEDURE IF EXISTS account.userLoginWithKey;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLoginWithKey`(vUserName VARCHAR(255), vKey VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * @deprecated Use myUser_loginWithKey()
+ */
+ CALL myUser_loginWithKey(vUserName, vKey);
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.userLoginWithKey TO guest@localhost;
diff --git a/db/changes/10221-accountModule/00-userLoginWithName.sql b/db/changes/10221-accountModule/00-userLoginWithName.sql
new file mode 100644
index 000000000..4053970e4
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userLoginWithName.sql
@@ -0,0 +1,12 @@
+DROP PROCEDURE IF EXISTS account.userLoginWithName;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userLoginWithName`(vUserName VARCHAR(255))
+ READS SQL DATA
+BEGIN
+/**
+ * @deprecated Use myUser_loginWithName()
+ */
+ CALL myUser_loginWithName(vUserName);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-userLogout.sql b/db/changes/10221-accountModule/00-userLogout.sql
new file mode 100644
index 000000000..7d0d68324
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userLogout.sql
@@ -0,0 +1,14 @@
+DROP PROCEDURE IF EXISTS account.myUserLogout;
+
+DELIMITER $$
+$$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`myUserLogout`()
+BEGIN
+/**
+ * @deprecated Use myUser_Logout()
+ */
+ CALL myUser_logout;
+END$$
+DELIMITER ;
+
+GRANT EXECUTE ON PROCEDURE account.myUserLogout TO account@localhost;
diff --git a/db/changes/10221-accountModule/00-userSetPassword.sql b/db/changes/10221-accountModule/00-userSetPassword.sql
new file mode 100644
index 000000000..fd3daec53
--- /dev/null
+++ b/db/changes/10221-accountModule/00-userSetPassword.sql
@@ -0,0 +1,16 @@
+DROP PROCEDURE IF EXISTS account.userSetPassword;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`userSetPassword`(vUserName VARCHAR(255), vPassword VARCHAR(255))
+BEGIN
+/**
+ * @deprecated Use user_setPassword()
+ */
+ DECLARE vUserId INT;
+
+ SELECT id INTO vUserId
+ FROM user WHERE `name` = vUserName;
+
+ CALL user_setPassword(vUserId, vPassword);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_changePassword.sql b/db/changes/10221-accountModule/00-user_changePassword.sql
new file mode 100644
index 000000000..c137213e0
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_changePassword.sql
@@ -0,0 +1,27 @@
+DROP PROCEDURE IF EXISTS account.user_changePassword;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE account.user_changePassword(vSelf INT, vOldPassword VARCHAR(255), vPassword VARCHAR(255))
+BEGIN
+/**
+ * Changes the user password.
+ *
+ * @param vSelf The user id
+ * @param vOldPassword The current password
+ * @param vPassword The new password
+ */
+ DECLARE vPasswordOk BOOL;
+ DECLARE vUserName VARCHAR(255);
+
+ SELECT `password` = MD5(vOldPassword), `name`
+ INTO vPasswordOk, vUserName
+ FROM user WHERE id = vSelf;
+
+ IF NOT vPasswordOk THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = 'Invalid password';
+ END IF;
+
+ CALL user_setPassword(vSelf, vPassword);
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_checkName.sql b/db/changes/10221-accountModule/00-user_checkName.sql
new file mode 100644
index 000000000..9b54d6175
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_checkName.sql
@@ -0,0 +1,17 @@
+DROP PROCEDURE IF EXISTS account.user_checkName;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE `account`.`user_checkName`(vUserName VARCHAR(255))
+BEGIN
+/**
+ * Checks that username meets the necessary syntax requirements, otherwise it
+ * throws an exception.
+ * The user name must only contain lowercase letters or, starting with second
+ * character, numbers or underscores.
+ */
+ IF vUserName NOT REGEXP '^[a-z0-9_-]*$' THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = 'INVALID_USER_NAME';
+ END IF;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_getMysqlRole.sql b/db/changes/10221-accountModule/00-user_getMysqlRole.sql
new file mode 100644
index 000000000..4088ea8a4
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_getMysqlRole.sql
@@ -0,0 +1,22 @@
+DROP FUNCTION IF EXISTS account.user_getMysqlRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_getMysqlRole`(vUserName VARCHAR(255)) RETURNS varchar(255) CHARSET utf8
+BEGIN
+/**
+ * From a username, it returns the associated MySQL wich should be used when
+ * using external authentication systems.
+ *
+ * @param vUserName The user name
+ * @return The associated MySQL role
+ */
+ DECLARE vRole VARCHAR(255);
+
+ SELECT CONCAT(IF(r.hasLogin, 'z-', ''), r.name) INTO vRole
+ FROM role r
+ JOIN user u ON u.role = r.id
+ WHERE u.name = vUserName;
+
+ RETURN vRole;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_getNameFromId.sql b/db/changes/10221-accountModule/00-user_getNameFromId.sql
new file mode 100644
index 000000000..ae9ae5941
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_getNameFromId.sql
@@ -0,0 +1,20 @@
+DROP FUNCTION IF EXISTS account.user_getNameFromId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_getNameFromId`(vSelf INT) RETURNS varchar(30) CHARSET utf8
+BEGIN
+/**
+ * Gets user name from it's id.
+ *
+ * @param vSelf The user id
+ * @return The user name
+ */
+ DECLARE vName VARCHAR(30);
+
+ SELECT `name` INTO vName
+ FROM user
+ WHERE id = vId;
+
+ RETURN vSelf;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_hasRole.sql b/db/changes/10221-accountModule/00-user_hasRole.sql
new file mode 100644
index 000000000..d42c81deb
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_hasRole.sql
@@ -0,0 +1,25 @@
+DROP FUNCTION IF EXISTS account.user_hasRole;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_hasRole`(vUserName VARCHAR(255), vRoleName VARCHAR(255)) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * Checks if user has/inherits a role.
+ *
+ * @param vUserName The user name
+ * @param vRoleName Role to check
+ * @return %TRUE if it has role, %FALSE otherwise
+ */
+ DECLARE vHasRole BOOL DEFAULT FALSE;
+
+ SELECT COUNT(*) > 0 INTO vHasRole
+ FROM user u
+ JOIN roleRole rr ON rr.role = u.role
+ JOIN role r ON r.id = rr.inheritsFrom
+ WHERE u.`name` = vUserName
+ AND r.`name` = vRoleName COLLATE 'utf8_unicode_ci';
+
+ RETURN vHasRole;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_hasRoleId.sql b/db/changes/10221-accountModule/00-user_hasRoleId.sql
new file mode 100644
index 000000000..b2f523e8c
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_hasRoleId.sql
@@ -0,0 +1,25 @@
+DROP FUNCTION IF EXISTS account.user_hasRoleId;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` FUNCTION `account`.`user_hasRoleId`(vUser VARCHAR(255), vRoleId INT) RETURNS tinyint(1)
+ DETERMINISTIC
+BEGIN
+/**
+ * Checks if user has/inherits a role.
+ *
+ * @param vUserName The user name
+ * @param vRoleId Role id to check
+ * @return %TRUE if it has role, %FALSE otherwise
+ */
+ DECLARE vHasRole BOOL DEFAULT FALSE;
+
+ SELECT COUNT(*) > 0 INTO vHasRole
+ FROM user u
+ JOIN roleRole rr ON rr.role = u.role
+ JOIN role r ON r.id = rr.inheritsFrom
+ WHERE u.`name` = vUser
+ AND r.id = vRoleId;
+
+ RETURN vHasRole;
+END$$
+DELIMITER ;
diff --git a/db/changes/10221-accountModule/00-user_setPassword.sql b/db/changes/10221-accountModule/00-user_setPassword.sql
new file mode 100644
index 000000000..3dcbb1653
--- /dev/null
+++ b/db/changes/10221-accountModule/00-user_setPassword.sql
@@ -0,0 +1,21 @@
+DROP PROCEDURE IF EXISTS account.user_setPassword;
+
+DELIMITER $$
+CREATE DEFINER=`root`@`%` PROCEDURE account.user_setPassword(vSelf INT, vPassword VARCHAR(255))
+BEGIN
+/**
+ * Change the password of the passed as a parameter. Only administrators should
+ * have execute privileges on the procedure since it does not request the user's
+ * current password.
+ *
+ * @param vSelf The user id
+ * @param vPassword New password
+ */
+ CALL user_checkPassword(vPassword);
+
+ UPDATE user SET
+ `password` = MD5(vPassword),
+ `recoverPass` = FALSE
+ WHERE id = vSelf;
+END$$
+DELIMITER ;
diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql
index 9c05923cf..374c1ed50 100644
--- a/db/dump/dumpedFixtures.sql
+++ b/db/dump/dumpedFixtures.sql
@@ -84,6 +84,36 @@ LOCK TABLES `roleRole` WRITE;
INSERT INTO `roleRole` VALUES (0,0),(0,1),(0,2),(0,3),(0,5),(0,6),(0,9),(0,11),(0,13),(0,15),(0,16),(0,17),(0,18),(0,19),(0,20),(0,21),(0,22),(0,30),(0,31),(0,32),(0,33),(0,34),(0,35),(0,36),(0,37),(0,38),(0,39),(0,40),(0,41),(0,42),(0,43),(0,44),(0,45),(0,47),(0,48),(0,49),(0,50),(0,51),(0,52),(0,53),(0,54),(0,55),(0,56),(0,57),(0,58),(0,59),(0,60),(0,61),(0,62),(0,64),(0,65),(0,66),(0,67),(0,69),(0,70),(1,1),(1,2),(1,3),(1,6),(1,11),(1,70),(2,2),(2,6),(2,11),(3,3),(3,6),(3,11),(5,1),(5,2),(5,3),(5,5),(5,6),(5,11),(5,13),(5,18),(5,21),(5,33),(5,53),(5,70),(6,6),(9,0),(9,1),(9,2),(9,3),(9,5),(9,6),(9,9),(9,11),(9,13),(9,15),(9,16),(9,17),(9,18),(9,19),(9,20),(9,21),(9,22),(9,30),(9,31),(9,32),(9,33),(9,34),(9,35),(9,36),(9,37),(9,38),(9,39),(9,40),(9,41),(9,42),(9,43),(9,44),(9,45),(9,47),(9,48),(9,49),(9,50),(9,51),(9,52),(9,53),(9,54),(9,55),(9,56),(9,57),(9,58),(9,59),(9,60),(9,61),(9,62),(9,64),(9,65),(9,66),(9,67),(9,69),(9,70),(11,6),(11,11),(13,1),(13,2),(13,3),(13,6),(13,11),(13,13),(13,70),(15,1),(15,2),(15,3),(15,6),(15,11),(15,13),(15,15),(15,35),(15,56),(15,57),(15,70),(16,1),(16,2),(16,3),(16,6),(16,11),(16,13),(16,15),(16,16),(16,35),(16,56),(16,57),(16,70),(17,1),(17,2),(17,3),(17,5),(17,6),(17,11),(17,13),(17,15),(17,16),(17,17),(17,18),(17,19),(17,20),(17,21),(17,33),(17,35),(17,36),(17,37),(17,39),(17,44),(17,47),(17,49),(17,50),(17,53),(17,56),(17,57),(17,58),(17,59),(17,64),(17,65),(17,70),(18,1),(18,2),(18,3),(18,6),(18,11),(18,18),(18,70),(19,1),(19,2),(19,3),(19,6),(19,11),(19,13),(19,18),(19,19),(19,21),(19,53),(19,70),(20,1),(20,2),(20,3),(20,6),(20,11),(20,13),(20,15),(20,16),(20,18),(20,19),(20,20),(20,21),(20,35),(20,36),(20,44),(20,47),(20,49),(20,50),(20,53),(20,56),(20,57),(20,58),(20,59),(20,65),(20,70),(21,1),(21,2),(21,3),(21,6),(21,11),(21,13),(21,18),(21,21),(21,53),(21,70),(22,1),(22,2),(22,3),(22,6),(22,11),(22,13),(22,18),(22,21),(22,22),(22,53),(22,70),(30,1),(30,2),(30,3),(30,5),(30,6),(30,11),(30,13),(30,15),(30,16),(30,18),(30,19),(30,20),(30,21),(30,22),(30,30),(30,33),(30,35),(30,36),(30,44),(30,47),(30,49),(30,50),(30,53),(30,56),(30,57),(30,58),(30,59),(30,64),(30,65),(30,70),(31,1),(31,2),(31,3),(31,6),(31,11),(31,31),(31,70),(32,1),(32,2),(32,3),(32,6),(32,11),(32,32),(32,70),(33,33),(34,1),(34,2),(34,3),(34,6),(34,11),(34,13),(34,33),(34,34),(34,70),(35,1),(35,2),(35,3),(35,6),(35,11),(35,35),(35,70),(36,1),(36,2),(36,3),(36,6),(36,11),(36,36),(36,44),(36,47),(36,70),(37,1),(37,2),(37,3),(37,6),(37,11),(37,37),(37,70),(38,1),(38,2),(38,3),(38,6),(38,11),(38,37),(38,38),(38,64),(38,70),(39,1),(39,2),(39,3),(39,5),(39,6),(39,11),(39,13),(39,18),(39,21),(39,33),(39,39),(39,53),(39,56),(39,57),(39,70),(40,1),(40,2),(40,3),(40,6),(40,11),(40,36),(40,40),(40,44),(40,47),(40,49),(40,58),(40,70),(41,1),(41,2),(41,3),(41,6),(41,11),(41,13),(41,35),(41,36),(41,40),(41,41),(41,44),(41,47),(41,49),(41,58),(41,70),(42,1),(42,2),(42,3),(42,6),(42,11),(42,35),(42,36),(42,42),(42,44),(42,47),(42,49),(42,58),(42,70),(43,1),(43,2),(43,3),(43,6),(43,11),(43,13),(43,35),(43,36),(43,42),(43,43),(43,44),(43,47),(43,49),(43,58),(43,70),(44,1),(44,2),(44,3),(44,6),(44,11),(44,44),(44,70),(45,1),(45,2),(45,3),(45,6),(45,11),(45,13),(45,44),(45,45),(45,70),(47,1),(47,2),(47,3),(47,6),(47,11),(47,47),(47,70),(48,1),(48,2),(48,3),(48,6),(48,11),(48,13),(48,47),(48,48),(48,70),(49,1),(49,2),(49,3),(49,6),(49,11),(49,36),(49,44),(49,47),(49,49),(49,58),(49,70),(50,1),(50,2),(50,3),(50,6),(50,11),(50,13),(50,18),(50,21),(50,35),(50,36),(50,44),(50,47),(50,49),(50,50),(50,53),(50,56),(50,57),(50,58),(50,59),(50,70),(51,1),(51,2),(51,3),(51,6),(51,11),(51,51),(51,70),(52,1),(52,2),(52,3),(52,6),(52,11),(52,13),(52,18),(52,19),(52,21),(52,35),(52,51),(52,52),(52,53),(52,70),(53,1),(53,2),(53,3),(53,6),(53,11),(53,53),(53,70),(54,1),(54,2),(54,3),(54,6),(54,11),(54,54),(54,70),(55,1),(55,2),(55,3),(55,6),(55,11),(55,13),(55,54),(55,55),(55,70),(56,1),(56,2),(56,3),(56,6),(56,11),(56,56),(56,70),(57,1),(57,2),(57,3),(57,6),(57,11),(57,13),(57,56),(57,57),(57,70),(58,1),(58,2),(58,3),(58,6),(58,11),(58,58),(58,70),(59,1),(59,2),(59,3),(59,6),(59,11),(59,13),(59,36),(59,44),(59,47),(59,49),(59,58),(59,59),(59,70),(60,1),(60,2),(60,3),(60,5),(60,6),(60,11),(60,13),(60,18),(60,21),(60,33),(60,35),(60,36),(60,37),(60,44),(60,47),(60,49),(60,50),(60,53),(60,56),(60,57),(60,58),(60,59),(60,60),(60,70),(61,1),(61,2),(61,3),(61,6),(61,11),(61,13),(61,36),(61,44),(61,47),(61,61),(61,70),(62,62),(64,64),(65,1),(65,2),(65,3),(65,6),(65,11),(65,13),(65,18),(65,19),(65,21),(65,35),(65,36),(65,44),(65,47),(65,49),(65,50),(65,53),(65,56),(65,57),(65,58),(65,59),(65,65),(65,70),(66,0),(66,1),(66,2),(66,3),(66,5),(66,6),(66,9),(66,11),(66,13),(66,15),(66,16),(66,17),(66,18),(66,19),(66,20),(66,21),(66,22),(66,30),(66,31),(66,32),(66,33),(66,34),(66,35),(66,36),(66,37),(66,38),(66,39),(66,40),(66,41),(66,42),(66,43),(66,44),(66,45),(66,47),(66,48),(66,49),(66,50),(66,51),(66,52),(66,53),(66,54),(66,55),(66,56),(66,57),(66,58),(66,59),(66,60),(66,61),(66,62),(66,64),(66,65),(66,66),(66,67),(66,69),(66,70),(67,1),(67,2),(67,3),(67,5),(67,6),(67,11),(67,13),(67,18),(67,21),(67,33),(67,37),(67,53),(67,67),(67,70),(69,1),(69,2),(69,3),(69,6),(69,11),(69,35),(69,47),(69,69),(69,70),(70,6),(70,11),(70,70);
/*!40000 ALTER TABLE `roleRole` ENABLE KEYS */;
UNLOCK TABLES;
+
+--
+-- Dumping data for table `userPassword`
+--
+
+LOCK TABLES `userPassword` WRITE;
+/*!40000 ALTER TABLE `userPassword` DISABLE KEYS */;
+INSERT INTO `userPassword` VALUES (1,7,1,0,1,1);
+/*!40000 ALTER TABLE `userPassword` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Dumping data for table `accountConfig`
+--
+
+LOCK TABLES `accountConfig` WRITE;
+/*!40000 ALTER TABLE `accountConfig` DISABLE KEYS */;
+INSERT INTO `accountConfig` VALUES (1,'/mnt/storage/homes','/bin/bash',10000,5,60,5,30);
+/*!40000 ALTER TABLE `accountConfig` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Dumping data for table `mailConfig`
+--
+
+LOCK TABLES `mailConfig` WRITE;
+/*!40000 ALTER TABLE `mailConfig` DISABLE KEYS */;
+INSERT INTO `mailConfig` VALUES (1,'verdnatura.es');
+/*!40000 ALTER TABLE `mailConfig` ENABLE KEYS */;
+UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 61587b99d..9074f3763 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -12,10 +12,6 @@ INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
VALUES
('1', '6');
-INSERT INTO `account`.`mailConfig` (`id`, `domain`)
- VALUES
- ('1', 'verdnatura.es');
-
INSERT INTO `vn`.`bionicConfig` (`generalInflationCoeficient`, `minimumDensityVolumetricWeight`, `verdnaturaVolumeBox`, `itemCarryBox`)
VALUES
(1.30, 167.00, 138000, 71);
@@ -33,12 +29,16 @@ INSERT INTO `vn`.`packagingConfig`(`upperGap`)
('10');
UPDATE `account`.`role` SET id = 100 WHERE id = 0;
+CALL `account`.`role_sync`;
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`)
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en'
FROM `account`.`role` WHERE id <> 20
ORDER BY id;
+INSERT INTO `account`.`account`(`id`)
+ SELECT id FROM `account`.`user`;
+
INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `userFk`, `bossFk`)
SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9
FROM `vn`.`user`;
@@ -52,7 +52,7 @@ DELETE FROM `vn`.`worker` WHERE firstName ='customer';
INSERT INTO `hedera`.`tpvConfig`(`id`, `currency`, `terminal`, `transactionType`, `maxAmount`, `employeeFk`, `testUrl`)
VALUES
(1, 978, 1, 0, 2000, 9, 0);
-
+
INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,`email`,`lang`)
VALUES
(101, 'BruceWayne', 'Bruce Wayne', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es'),
@@ -68,6 +68,24 @@ INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,
(111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'),
(112, 'Trash', 'Trash', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en');
+INSERT INTO `account`.`mailAlias`(`id`, `alias`, `description`, `isPublic`)
+ VALUES
+ (1, 'general', 'General mailing list', FALSE),
+ (2, 'it' , 'IT department' , TRUE),
+ (3, 'sales' , 'Sales department' , TRUE);
+
+INSERT INTO `account`.`mailAliasAccount`(`mailAlias`, `account`)
+ VALUES
+ (1, 1),
+ (1, 18),
+ (3, 18),
+ (1, 9),
+ (2, 9);
+
+INSERT INTO `account`.`mailForward`(`account`, `forwardTo`)
+ VALUES
+ (1, 'employee@domain.local');
+
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`)
VALUES
(106, 'LGN', 'David Charles', 'Haller', 106, 19, 432978106),
@@ -88,6 +106,15 @@ INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`,
(19,'Francia', 1, 'FR', 1, 27),
(30,'Canarias', 1, 'IC', 1, 24);
+INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`)
+ VALUES
+ ('ca', 'Català' , 'Catalan' , TRUE),
+ ('en', 'English' , 'English' , TRUE),
+ ('es', 'Español' , 'Spanish' , TRUE),
+ ('fr', 'Français' , 'French' , TRUE),
+ ('mn', 'Португалий', 'Mongolian' , TRUE),
+ ('pt', 'Português' , 'Portuguese', TRUE);
+
INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
VALUES
(1, 'Main Warehouse'),
diff --git a/db/export-data.sh b/db/export-data.sh
index 9f3997cf7..0aa9fb319 100755
--- a/db/export-data.sh
+++ b/db/export-data.sh
@@ -22,6 +22,9 @@ TABLES=(
role
roleInherit
roleRole
+ userPassword
+ accountConfig
+ mailConfig
)
dump_tables ${TABLES[@]}
diff --git a/front/core/components/crud-model/crud-model.js b/front/core/components/crud-model/crud-model.js
index 9cfa3b410..4994e1547 100644
--- a/front/core/components/crud-model/crud-model.js
+++ b/front/core/components/crud-model/crud-model.js
@@ -134,7 +134,7 @@ export default class CrudModel extends ModelProxy {
*/
save() {
if (!this.isChanged)
- return null;
+ return this.$q.resolve();
let deletes = [];
let updates = [];
diff --git a/front/core/components/data-viewer/index.html b/front/core/components/data-viewer/index.html
index 8c843d869..3488b7b0b 100644
--- a/front/core/components/data-viewer/index.html
+++ b/front/core/components/data-viewer/index.html
@@ -1,6 +1,7 @@
diff --git a/front/core/components/list/style.scss b/front/core/components/list/style.scss
index 223748f4a..adbc496c7 100644
--- a/front/core/components/list/style.scss
+++ b/front/core/components/list/style.scss
@@ -32,7 +32,6 @@ vn-list,
vn-item,
.vn-item {
- @extend %clickable;
display: flex;
align-items: center;
color: inherit;
@@ -85,4 +84,6 @@ vn-item,
}
}
-
+a.vn-item {
+ @extend %clickable;
+}
diff --git a/front/core/components/menu/menu.js b/front/core/components/menu/menu.js
index 3eb169926..d0c649004 100755
--- a/front/core/components/menu/menu.js
+++ b/front/core/components/menu/menu.js
@@ -1,5 +1,6 @@
import ngModule from '../../module';
import Popover from '../popover';
+import './style.scss';
export default class Menu extends Popover {
show(parent) {
diff --git a/front/core/components/menu/style.scss b/front/core/components/menu/style.scss
new file mode 100644
index 000000000..92f437243
--- /dev/null
+++ b/front/core/components/menu/style.scss
@@ -0,0 +1,7 @@
+@import "./effects";
+
+.vn-menu {
+ vn-item, .vn-item {
+ @extend %clickable;
+ }
+}
diff --git a/front/core/components/model-proxy/model-proxy.js b/front/core/components/model-proxy/model-proxy.js
index 592b25710..26c28c803 100644
--- a/front/core/components/model-proxy/model-proxy.js
+++ b/front/core/components/model-proxy/model-proxy.js
@@ -107,15 +107,16 @@ export default class ModelProxy extends DataModel {
* Removes a row from the model and emits the 'rowRemove' event.
*
* @param {Number} index The row index
+ * @return {Promise} The save request promise
*/
remove(index) {
- let [item] = this.data.splice(index, 1);
+ let [row] = this.data.splice(index, 1);
- let proxiedIndex = this.proxiedData.indexOf(item);
+ let proxiedIndex = this.proxiedData.indexOf(row);
this.proxiedData.splice(proxiedIndex, 1);
- if (!item.$isNew)
- this.removed.push(item);
+ if (!row.$isNew)
+ this.removed.push(row);
this.isChanged = true;
if (!this.data.length)
@@ -125,7 +126,19 @@ export default class ModelProxy extends DataModel {
this.emit('dataUpdate');
if (this.autoSave)
- this.save();
+ return this.save();
+ else
+ return this.$q.resolve();
+ }
+
+ /**
+ * Removes a row from the model and emits the 'rowRemove' event.
+ *
+ * @param {Object} row The row object
+ * @return {Promise} The save request promise
+ */
+ removeRow(row) {
+ return this.remove(this.data.indexOf(row));
}
/**
diff --git a/front/core/components/searchbar/locale/en.yml b/front/core/components/searchbar/locale/en.yml
index 52ab6a184..e5b562c24 100644
--- a/front/core/components/searchbar/locale/en.yml
+++ b/front/core/components/searchbar/locale/en.yml
@@ -1 +1 @@
-Search by: Search by {{module | translate}}
\ No newline at end of file
+Search for: Search {{module}}
\ No newline at end of file
diff --git a/front/core/components/searchbar/locale/es.yml b/front/core/components/searchbar/locale/es.yml
index 730564a7b..53a5bb289 100644
--- a/front/core/components/searchbar/locale/es.yml
+++ b/front/core/components/searchbar/locale/es.yml
@@ -1 +1 @@
-Search by: Buscar por {{module | translate}}
\ No newline at end of file
+Search for: Buscar {{module}}
\ No newline at end of file
diff --git a/front/core/components/searchbar/searchbar.js b/front/core/components/searchbar/searchbar.js
index 0b7bd8cd2..8adc40b67 100644
--- a/front/core/components/searchbar/searchbar.js
+++ b/front/core/components/searchbar/searchbar.js
@@ -16,6 +16,7 @@ import './style.scss';
* @property {Function} onSearch Function to call when search is submited
* @property {CrudModel} model The model used for searching
* @property {Function} exprBuilder If defined, is used to build each non-null param expresion
+ * @property {String} baseState The base state for searchs
*/
export default class Searchbar extends Component {
constructor($element, $) {
@@ -23,6 +24,8 @@ export default class Searchbar extends Component {
this.searchState = '.';
this.placeholder = 'Search';
this.autoState = true;
+ this.separateIndex = true;
+ this.entityState = 'card.summary';
this.deregisterCallback = this.$transitions.onSuccess(
{}, transition => this.onStateChange(transition));
@@ -33,11 +36,13 @@ export default class Searchbar extends Component {
if (!this.baseState) {
let stateParts = this.$state.current.name.split('.');
this.baseState = stateParts[0];
- }
+ this.searchState = `${this.baseState}.index`;
+ } else
+ this.searchState = this.baseState;
- this.searchState = `${this.baseState}.index`;
- this.placeholder = this.$t('Search by', {
- module: this.baseState
+ let description = this.$state.get(this.baseState).description;
+ this.placeholder = this.$t('Search for', {
+ module: this.$t(description).toLowerCase()
});
}
@@ -222,7 +227,7 @@ export default class Searchbar extends Component {
subState += '.index';
break;
default:
- subState = 'card.summary';
+ subState = this.entityState;
}
if (this.stateParams)
@@ -292,8 +297,10 @@ ngModule.vnComponent('vnSearchbar', {
panel: '@',
info: '@?',
onSearch: '&?',
- baseState: '@?',
autoState: '',
+ baseState: '@?',
+ entityState: '@?',
+ separateIndex: '',
stateParams: '&?',
model: '',
exprBuilder: '&?',
diff --git a/front/core/components/tooltip/tooltip.js b/front/core/components/tooltip/tooltip.js
index e2dba2d03..b045491f2 100644
--- a/front/core/components/tooltip/tooltip.js
+++ b/front/core/components/tooltip/tooltip.js
@@ -1,5 +1,6 @@
import ngModule from '../../module';
import Component from '../../lib/component';
+import {kebabToCamel} from '../../lib/string';
import './style.scss';
let positions = ['left', 'right', 'up', 'down'];
@@ -206,8 +207,8 @@ ngModule.vnComponent('vnTooltip', {
}
});
-directive.$inject = ['$document', '$compile', '$templateRequest'];
-export function directive($document, $compile, $templateRequest) {
+directive.$inject = ['$document', '$compile'];
+export function directive($document, $compile) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
diff --git a/front/core/components/watcher/watcher.js b/front/core/components/watcher/watcher.js
index 9de9d8c1b..8b52be69c 100644
--- a/front/core/components/watcher/watcher.js
+++ b/front/core/components/watcher/watcher.js
@@ -1,70 +1,220 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import getModifiedData from '../../lib/modified';
-import copyObject from '../../lib/copy';
import isEqual from '../../lib/equals';
import isFullEmpty from '../../lib/full-empty';
import UserError from '../../lib/user-error';
+import {mergeFilters} from 'vn-loopback/util/filter';
/**
- * Component that checks for changes on a specific model property and
- * asks the user to save or discard it when the state changes.
- * Also it can save the data to the server when the @url and @idField
- * properties are provided.
+ * Component that checks for changes on a specific model property and asks the
+ * user to save or discard it when the state changes.
+ * Also it can save the data to the server when the @url and @idField properties
+ * are provided.
+ *
+ * @property {String} idField The id field name, 'id' if not specified
+ * @property {*} idValue The id field value
+ * @property {Boolean} isNew Whether is a new instance
+ * @property {Boolean} insertMode Whether to enable insert mode
+ * @property {String} url The base HTTP request path
+ * @property {Boolean} get Whether to fetch initial data
*/
export default class Watcher extends Component {
- constructor($element, $, $state, $stateParams, $transitions, $http, vnApp, $translate, $attrs, $q) {
- super($element);
- Object.assign(this, {
- $,
- $state,
- $stateParams,
- $http,
- _: $translate,
- $attrs,
- vnApp,
- $q
- });
-
+ constructor(...args) {
+ super(...args);
+ this.idField = 'id';
+ this.get = true;
+ this.insertMode = false;
this.state = null;
- this.deregisterCallback = $transitions.onStart({},
+ this.deregisterCallback = this.$transitions.onStart({},
transition => 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: '',
+ data: '=',
form: '<',
- save: '<',
- get: ''
+ save: '',
+ get: '',
+ insertMode: '',
+ autoFill: '',
+ filter: '',
+ fields: '',
+ where: '',
+ include: ''
},
controller: Watcher
});
diff --git a/front/core/components/watcher/watcher.spec.js b/front/core/components/watcher/watcher.spec.js
index e19592387..544817446 100644
--- a/front/core/components/watcher/watcher.spec.js
+++ b/front/core/components/watcher/watcher.spec.js
@@ -1,5 +1,4 @@
import './watcher.js';
-import getModifiedData from '../../lib/modified';
describe('Component vnWatcher', () => {
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 @@
My account
diff --git a/front/salix/locale/es.yml b/front/salix/locale/es.yml
index 287f840a5..75c65ef64 100644
--- a/front/salix/locale/es.yml
+++ b/front/salix/locale/es.yml
@@ -44,6 +44,7 @@ Routes: Rutas
Locator: Localizador
Invoices out: Facturas emitidas
Entries: Entradas
+Users: Usuarios
# Common
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index d28cceb41..ea5a09952 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -134,5 +134,14 @@
"This ticket is deleted": "Este ticket está eliminado",
"A travel with this data already exists": "Ya existe un travel con estos datos",
"This thermograph id already exists": "La id del termógrafo ya existe",
- "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante"
+ "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante",
+ "ORDER_ALREADY_CONFIRMED": "ORDER_ALREADY_CONFIRMED",
+ "Invalid password": "Invalid password",
+ "Password does not meet requirements": "Password does not meet requirements",
+ "Role already assigned": "Role already assigned",
+ "Invalid role name": "Invalid role name",
+ "Role name must be written in camelCase": "Role name must be written in camelCase",
+ "can't be set": "can't be set",
+ "Email already exists": "Email already exists",
+ "User already exists": "User already exists"
}
\ No newline at end of file
diff --git a/modules/account/back/methods/role-inherit/sync.js b/modules/account/back/methods/role-inherit/sync.js
new file mode 100644
index 000000000..1b6a3cd83
--- /dev/null
+++ b/modules/account/back/methods/role-inherit/sync.js
@@ -0,0 +1,114 @@
+const ldap = require('../../util/ldapjs-extra');
+
+module.exports = Self => {
+ Self.remoteMethod('sync', {
+ description: 'Synchronizes the user with the other user databases',
+ http: {
+ path: `/sync`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.sync = async function() {
+ let $ = Self.app.models;
+
+ let ldapConfig = await $.LdapConfig.findOne({
+ fields: ['host', 'rdn', 'password', 'groupDn']
+ });
+ let accountConfig = await $.AccountConfig.findOne({
+ fields: ['idBase']
+ });
+
+ if (!ldapConfig) return;
+
+ // Connect
+
+ let client = ldap.createClient({
+ url: `ldap://${ldapConfig.host}:389`
+ });
+
+ let ldapPassword = Buffer
+ .from(ldapConfig.password, 'base64')
+ .toString('ascii');
+ await client.bind(ldapConfig.rdn, ldapPassword);
+
+ let err;
+ try {
+ // Delete roles
+
+ let opts = {
+ scope: 'sub',
+ attributes: ['dn'],
+ filter: 'objectClass=posixGroup'
+ };
+ res = await client.search(ldapConfig.groupDn, opts);
+
+ let reqs = [];
+ await new Promise((resolve, reject) => {
+ res.on('error', err => {
+ if (err.name === 'NoSuchObjectError')
+ err = new Error(`Object '${ldapConfig.groupDn}' does not exist`);
+ reject(err);
+ });
+ res.on('searchEntry', e => {
+ reqs.push(client.del(e.object.dn));
+ });
+ res.on('end', resolve);
+ });
+ await Promise.all(reqs);
+
+ // Recreate roles
+
+ let roles = await $.Role.find({
+ fields: ['id', 'name']
+ });
+ let accounts = await $.UserAccount.find({
+ fields: ['id'],
+ include: {
+ relation: 'user',
+ scope: {
+ fields: ['name'],
+ include: {
+ relation: 'roles',
+ scope: {
+ fields: ['inheritsFrom']
+ }
+ }
+ }
+ }
+ });
+
+ let map = new Map();
+ for (let account of accounts) {
+ let user = account.user();
+ for (let inherit of user.roles()) {
+ let roleId = inherit.inheritsFrom;
+ if (!map.has(roleId)) map.set(roleId, []);
+ map.get(roleId).push(user.name);
+ }
+ }
+
+ reqs = [];
+ for (let role of roles) {
+ let newEntry = {
+ objectClass: ['top', 'posixGroup'],
+ cn: role.name,
+ gidNumber: accountConfig.idBase + role.id
+ };
+
+ let memberUid = map.get(role.id);
+ if (memberUid) newEntry.memberUid = memberUid;
+
+ let dn = `cn=${role.name},${ldapConfig.groupDn}`;
+ reqs.push(client.add(dn, newEntry));
+ }
+ await Promise.all(reqs);
+ } catch (e) {
+ err = e;
+ }
+
+ // FIXME: Cannot disconnect, hangs on undind() call
+ // await client.unbind();
+ if (err) throw err;
+ };
+};
diff --git a/modules/account/back/methods/user-account/sync-by-id.js b/modules/account/back/methods/user-account/sync-by-id.js
new file mode 100644
index 000000000..3c292dd4c
--- /dev/null
+++ b/modules/account/back/methods/user-account/sync-by-id.js
@@ -0,0 +1,27 @@
+
+module.exports = Self => {
+ Self.remoteMethod('syncById', {
+ description: 'Synchronizes the user with the other user databases',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'number',
+ description: 'The user id',
+ required: true
+ }, {
+ arg: 'password',
+ type: 'string',
+ description: 'The password'
+ }
+ ],
+ http: {
+ path: `/:id/syncById`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.syncById = async function(id, password) {
+ let user = await Self.app.models.Account.findById(id, {fields: ['name']});
+ await Self.sync(user.name, password);
+ };
+};
diff --git a/modules/account/back/methods/user-account/sync.js b/modules/account/back/methods/user-account/sync.js
new file mode 100644
index 000000000..202411e35
--- /dev/null
+++ b/modules/account/back/methods/user-account/sync.js
@@ -0,0 +1,267 @@
+const ldap = require('../../util/ldapjs-extra');
+const nthash = require('smbhash').nthash;
+const ssh = require('node-ssh');
+const crypto = require('crypto');
+
+module.exports = Self => {
+ Self.remoteMethod('sync', {
+ description: 'Synchronizes the user with the other user databases',
+ accepts: [
+ {
+ arg: 'userName',
+ type: 'string',
+ description: 'The user name',
+ required: true
+ }, {
+ arg: 'password',
+ type: 'string',
+ description: 'The password'
+ }
+ ],
+ http: {
+ path: `/sync`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.sync = async function(userName, password) {
+ let $ = Self.app.models;
+
+ let user = await $.Account.findOne({
+ fields: ['id'],
+ where: {name: userName}
+ });
+ let isSync = !await $.UserSync.exists(userName);
+
+ if (user && isSync) return;
+
+ let accountConfig;
+ let mailConfig;
+ let extraParams;
+ let hasAccount = false;
+
+ if (user) {
+ accountConfig = await $.AccountConfig.findOne({
+ fields: ['homedir', 'shell', 'idBase']
+ });
+ mailConfig = await $.MailConfig.findOne({
+ fields: ['domain']
+ });
+
+ user = await $.Account.findById(user.id, {
+ fields: [
+ 'id',
+ 'nickname',
+ 'email',
+ 'lang',
+ 'roleFk',
+ 'sync',
+ 'active',
+ 'created',
+ 'updated'
+ ],
+ where: {name: userName},
+ include: {
+ relation: 'roles',
+ scope: {
+ include: {
+ relation: 'inherits',
+ scope: {
+ fields: ['name']
+ }
+ }
+ }
+ }
+ });
+
+ extraParams = {
+ corporateMail: `${userName}@${mailConfig.domain}`,
+ uidNumber: accountConfig.idBase + user.id
+ };
+
+ hasAccount = user.active
+ && await $.UserAccount.exists(user.id);
+ }
+
+ if (user) {
+ let bcryptPassword = $.User.hashPassword(password);
+
+ await $.Account.upsertWithWhere({id: user.id},
+ {bcryptPassword}
+ );
+ await $.user.upsert({
+ id: user.id,
+ username: userName,
+ password: bcryptPassword,
+ email: user.email,
+ created: user.created,
+ updated: user.updated
+ });
+ }
+
+ // SIP
+
+ if (hasAccount) {
+ await Self.rawSql('CALL pbx.sip_setPassword(?, ?)',
+ [user.id, password]
+ );
+ }
+
+ // LDAP
+
+ let ldapConfig = await $.LdapConfig.findOne({
+ fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
+ });
+
+ if (ldapConfig) {
+ let ldapClient = ldap.createClient({
+ url: `ldap://${ldapConfig.host}:389`
+ });
+
+ let ldapPassword = Buffer
+ .from(ldapConfig.password, 'base64')
+ .toString('ascii');
+ await ldapClient.bind(ldapConfig.rdn, ldapPassword);
+
+ let err;
+ try {
+ // Deletes user
+
+ try {
+ let dn = `uid=${userName},${ldapConfig.baseDn}`;
+ await ldapClient.del(dn);
+ } catch (e) {
+ if (e.name !== 'NoSuchObjectError') throw e;
+ }
+
+ // Removes user from groups
+
+ let opts = {
+ scope: 'sub',
+ attributes: ['dn'],
+ filter: `&(memberUid=${userName})(objectClass=posixGroup)`
+ };
+ res = await ldapClient.search(ldapConfig.groupDn, opts);
+
+ let oldGroups = [];
+ await new Promise((resolve, reject) => {
+ res.on('error', reject);
+ res.on('searchEntry', e => oldGroups.push(e.object));
+ res.on('end', resolve);
+ });
+
+ let reqs = [];
+ for (oldGroup of oldGroups) {
+ let change = new ldap.Change({
+ operation: 'delete',
+ modification: {memberUid: userName}
+ });
+ reqs.push(ldapClient.modify(oldGroup.dn, change));
+ }
+ await Promise.all(reqs);
+
+ if (hasAccount) {
+ // Recreates user
+
+ let nameArgs = user.nickname.split(' ');
+ let sshaPassword = crypto
+ .createHash('sha1')
+ .update(password)
+ .digest('base64');
+
+ let dn = `uid=${userName},${ldapConfig.baseDn}`;
+ let newEntry = {
+ uid: userName,
+ objectClass: [
+ 'inetOrgPerson',
+ 'posixAccount',
+ 'sambaSamAccount'
+ ],
+ cn: user.nickname || userName,
+ displayName: user.nickname,
+ givenName: nameArgs[0],
+ sn: nameArgs[1] || 'Empty',
+ mail: extraParams.corporateMail,
+ userPassword: `{SSHA}${sshaPassword}`,
+ preferredLanguage: user.lang,
+ homeDirectory: `${accountConfig.homedir}/${userName}`,
+ loginShell: accountConfig.shell,
+ uidNumber: extraParams.uidNumber,
+ gidNumber: accountConfig.idBase + user.roleFk,
+ sambaSID: '-',
+ sambaNTPassword: nthash(password)
+ };
+ await ldapClient.add(dn, newEntry);
+
+ // Adds user to groups
+
+ let reqs = [];
+ for (let role of user.roles()) {
+ let change = new ldap.Change({
+ operation: 'add',
+ modification: {memberUid: userName}
+ });
+ let roleName = role.inherits().name;
+ let dn = `cn=${roleName},${ldapConfig.groupDn}`;
+ reqs.push(ldapClient.modify(dn, change));
+ }
+ await Promise.all(reqs);
+ }
+ } catch (e) {
+ err = e;
+ }
+
+ // FIXME: Cannot disconnect, hangs on undind() call
+ // await ldapClient.unbind();
+ if (err) throw err;
+ }
+
+ // Samba
+
+ let sambaConfig = await $.SambaConfig.findOne({
+ fields: ['host', 'sshUser', 'sshPass']
+ });
+
+ if (sambaConfig) {
+ let sshPassword = Buffer
+ .from(sambaConfig.sshPass, 'base64')
+ .toString('ascii');
+
+ let sshClient = new ssh.NodeSSH();
+ await sshClient.connect({
+ host: sambaConfig.host,
+ username: sambaConfig.sshUser,
+ password: sshPassword
+ });
+
+ let commands;
+
+ if (hasAccount) {
+ commands = [
+ `samba-tool user create "${userName}" `
+ + `--uid-number=${extraParams.uidNumber} `
+ + `--mail-address="${extraParams.corporateMail}" `
+ + `--random-password`,
+ `samba-tool user setexpiry "${userName}" `
+ + `--noexpiry`,
+ `samba-tool user setpassword "${userName}" `
+ + `--newpassword="${password}"`,
+ `mkhomedir_helper "${userName}" 0027`
+ ];
+ } else {
+ commands = [
+ `samba-tool user delete "${userName}"`
+ ];
+ }
+
+ for (let command of commands)
+ await sshClient.execCommand(command);
+
+ await sshClient.dispose();
+ }
+
+ // Mark as synchronized
+
+ await $.UserSync.destroyById(userName);
+ };
+};
diff --git a/modules/account/back/model-config.json b/modules/account/back/model-config.json
new file mode 100644
index 000000000..d243a2cca
--- /dev/null
+++ b/modules/account/back/model-config.json
@@ -0,0 +1,38 @@
+{
+ "AccountConfig": {
+ "dataSource": "vn"
+ },
+ "LdapConfig": {
+ "dataSource": "vn"
+ },
+ "MailAlias": {
+ "dataSource": "vn"
+ },
+ "MailAliasAccount": {
+ "dataSource": "vn"
+ },
+ "MailConfig": {
+ "dataSource": "vn"
+ },
+ "MailForward": {
+ "dataSource": "vn"
+ },
+ "RoleInherit": {
+ "dataSource": "vn"
+ },
+ "RoleRole": {
+ "dataSource": "vn"
+ },
+ "SambaConfig": {
+ "dataSource": "vn"
+ },
+ "UserAccount": {
+ "dataSource": "vn"
+ },
+ "UserPassword": {
+ "dataSource": "vn"
+ },
+ "UserSync": {
+ "dataSource": "vn"
+ }
+}
\ No newline at end of file
diff --git a/modules/account/back/models/account-config.json b/modules/account/back/models/account-config.json
new file mode 100644
index 000000000..a2a405610
--- /dev/null
+++ b/modules/account/back/models/account-config.json
@@ -0,0 +1,43 @@
+{
+ "name": "AccountConfig",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.accountConfig"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ },
+ "homedir": {
+ "type": "string",
+ "required": true
+ },
+ "shell": {
+ "type": "string",
+ "required": true
+ },
+ "idBase": {
+ "type": "number",
+ "required": true
+ },
+ "min": {
+ "type": "number",
+ "required": true
+ },
+ "max": {
+ "type": "number",
+ "required": true
+ },
+ "warn": {
+ "type": "number",
+ "required": true
+ },
+ "inact": {
+ "type": "number",
+ "required": true
+ }
+ }
+}
diff --git a/modules/account/back/models/ldap-config.json b/modules/account/back/models/ldap-config.json
new file mode 100644
index 000000000..e3061d651
--- /dev/null
+++ b/modules/account/back/models/ldap-config.json
@@ -0,0 +1,36 @@
+{
+ "name": "LdapConfig",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.ldapConfig"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ },
+ "host": {
+ "type": "string",
+ "required": true
+ },
+ "rdn": {
+ "type": "string",
+ "required": true
+ },
+ "password": {
+ "type": "string",
+ "required": true
+ },
+ "baseDn": {
+ "type": "string"
+ },
+ "filter": {
+ "type": "string"
+ },
+ "groupDn": {
+ "type": "string"
+ }
+ }
+}
diff --git a/modules/account/back/models/mail-alias-account.json b/modules/account/back/models/mail-alias-account.json
new file mode 100644
index 000000000..114d401e0
--- /dev/null
+++ b/modules/account/back/models/mail-alias-account.json
@@ -0,0 +1,27 @@
+{
+ "name": "MailAliasAccount",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.mailAliasAccount"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ }
+ },
+ "relations": {
+ "alias": {
+ "type": "belongsTo",
+ "model": "MailAlias",
+ "foreignKey": "mailAlias"
+ },
+ "user": {
+ "type": "belongsTo",
+ "model": "Account",
+ "foreignKey": "account"
+ }
+ }
+}
diff --git a/modules/account/back/models/mail-alias.json b/modules/account/back/models/mail-alias.json
new file mode 100644
index 000000000..a5970bd3f
--- /dev/null
+++ b/modules/account/back/models/mail-alias.json
@@ -0,0 +1,33 @@
+{
+ "name": "MailAlias",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.mailAlias"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ },
+ "alias": {
+ "type": "string",
+ "required": true
+ },
+ "description": {
+ "type": "string"
+ },
+ "isPublic": {
+ "type": "boolean"
+ }
+ },
+ "relations": {
+ "accounts": {
+ "type": "hasMany",
+ "model": "MailAliasAccount",
+ "foreignKey": "mailAlias",
+ "property": "id"
+ }
+ }
+}
diff --git a/modules/account/back/models/mail-config.json b/modules/account/back/models/mail-config.json
new file mode 100644
index 000000000..1b3d31fd8
--- /dev/null
+++ b/modules/account/back/models/mail-config.json
@@ -0,0 +1,19 @@
+{
+ "name": "MailConfig",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.mailConfig"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ },
+ "domain": {
+ "type": "string",
+ "required": true
+ }
+ }
+}
diff --git a/modules/account/back/models/mail-forward.json b/modules/account/back/models/mail-forward.json
new file mode 100644
index 000000000..a3e0eafd9
--- /dev/null
+++ b/modules/account/back/models/mail-forward.json
@@ -0,0 +1,25 @@
+{
+ "name": "MailForward",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.mailForward"
+ }
+ },
+ "properties": {
+ "account": {
+ "id": true
+ },
+ "forwardTo": {
+ "type": "string",
+ "required": true
+ }
+ },
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "Account",
+ "foreignKey": "account"
+ }
+ }
+}
diff --git a/modules/account/back/models/role-inherit.js b/modules/account/back/models/role-inherit.js
new file mode 100644
index 000000000..7d31e62b1
--- /dev/null
+++ b/modules/account/back/models/role-inherit.js
@@ -0,0 +1,22 @@
+const app = require('vn-loopback/server/server');
+
+module.exports = Self => {
+ require('../methods/role-inherit/sync')(Self);
+
+ app.on('started', function() {
+ let hooks = ['after save', 'after delete'];
+ for (let hook of hooks) {
+ Self.observe(hook, async() => {
+ try {
+ await Self.rawSql(`
+ CREATE EVENT account.role_sync
+ ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 5 SECOND
+ DO CALL role_sync;
+ `);
+ } catch (err) {
+ if (err.code != 'ER_EVENT_ALREADY_EXISTS') throw err;
+ }
+ });
+ }
+ });
+};
diff --git a/modules/account/back/models/role-inherit.json b/modules/account/back/models/role-inherit.json
new file mode 100644
index 000000000..4b69ffdc2
--- /dev/null
+++ b/modules/account/back/models/role-inherit.json
@@ -0,0 +1,27 @@
+{
+ "name": "RoleInherit",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.roleInherit"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ }
+ },
+ "relations": {
+ "owner": {
+ "type": "belongsTo",
+ "model": "Role",
+ "foreignKey": "role"
+ },
+ "inherits": {
+ "type": "belongsTo",
+ "model": "Role",
+ "foreignKey": "inheritsFrom"
+ }
+ }
+}
diff --git a/modules/account/back/models/role-role.json b/modules/account/back/models/role-role.json
new file mode 100644
index 000000000..f8f16e9e7
--- /dev/null
+++ b/modules/account/back/models/role-role.json
@@ -0,0 +1,26 @@
+{
+ "name": "RoleRole",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.roleRole"
+ }
+ },
+ "properties": {
+ "role": {
+ "id": true
+ }
+ },
+ "relations": {
+ "owner": {
+ "type": "belongsTo",
+ "model": "Role",
+ "foreignKey": "role"
+ },
+ "inherits": {
+ "type": "belongsTo",
+ "model": "Role",
+ "foreignKey": "inheritsFrom"
+ }
+ }
+}
diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json
new file mode 100644
index 000000000..ffbcce4eb
--- /dev/null
+++ b/modules/account/back/models/samba-config.json
@@ -0,0 +1,25 @@
+{
+ "name": "SambaConfig",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.sambaConfig"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true
+ },
+ "host": {
+ "type": "string",
+ "required": true
+ },
+ "sshUser": {
+ "type": "string"
+ },
+ "sshPass": {
+ "type": "string"
+ }
+ }
+}
diff --git a/modules/account/back/models/user-account.js b/modules/account/back/models/user-account.js
new file mode 100644
index 000000000..6fb8fd103
--- /dev/null
+++ b/modules/account/back/models/user-account.js
@@ -0,0 +1,5 @@
+
+module.exports = Self => {
+ require('../methods/user-account/sync')(Self);
+ require('../methods/user-account/sync-by-id')(Self);
+};
diff --git a/modules/account/back/models/user-account.json b/modules/account/back/models/user-account.json
new file mode 100644
index 000000000..fc0526388
--- /dev/null
+++ b/modules/account/back/models/user-account.json
@@ -0,0 +1,26 @@
+{
+ "name": "UserAccount",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.account"
+ }
+ },
+ "properties": {
+ "id": {
+ "id": true
+ }
+ },
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "Account",
+ "foreignKey": "id"
+ },
+ "aliases": {
+ "type": "hasMany",
+ "model": "MailAliasAccount",
+ "foreignKey": "account"
+ }
+ }
+}
diff --git a/modules/account/back/models/user-password.json b/modules/account/back/models/user-password.json
new file mode 100644
index 000000000..1b7e49edd
--- /dev/null
+++ b/modules/account/back/models/user-password.json
@@ -0,0 +1,34 @@
+{
+ "name": "UserPassword",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.userPassword"
+ }
+ },
+ "properties": {
+ "id": {
+ "id": true
+ },
+ "length": {
+ "type": "number",
+ "required": true
+ },
+ "nAlpha": {
+ "type": "number",
+ "required": true
+ },
+ "nUpper": {
+ "type": "number",
+ "required": true
+ },
+ "nDigits": {
+ "type": "number",
+ "required": true
+ },
+ "nPunct": {
+ "type": "number",
+ "required": true
+ }
+ }
+}
diff --git a/modules/account/back/models/user-sync.json b/modules/account/back/models/user-sync.json
new file mode 100644
index 000000000..60fd3fb3d
--- /dev/null
+++ b/modules/account/back/models/user-sync.json
@@ -0,0 +1,15 @@
+{
+ "name": "UserSync",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.userSync"
+ }
+ },
+ "properties": {
+ "name": {
+ "type": "string",
+ "id": true
+ }
+ }
+}
diff --git a/modules/account/back/util/ldapjs-extra.js b/modules/account/back/util/ldapjs-extra.js
new file mode 100644
index 000000000..381eebb6f
--- /dev/null
+++ b/modules/account/back/util/ldapjs-extra.js
@@ -0,0 +1,30 @@
+const ldap = require('ldapjs');
+const promisifyObject = require('./promisify').promisifyObject;
+
+module.exports = {
+ createClient,
+ Change: ldap.Change
+};
+
+/**
+ * Creates a promisified version of LDAP client.
+ *
+ * @param {Object} opts Client options
+ * @return {Client} The promisified LDAP client
+ */
+function createClient(opts) {
+ let client = ldap.createClient(opts);
+ promisifyObject(client, [
+ 'bind',
+ 'add',
+ 'compare',
+ 'del',
+ 'exop',
+ 'modify',
+ 'modifyDN',
+ 'search',
+ 'starttls',
+ 'unbind'
+ ]);
+ return client;
+}
diff --git a/modules/account/back/util/promisify.js b/modules/account/back/util/promisify.js
new file mode 100644
index 000000000..c23cbb36d
--- /dev/null
+++ b/modules/account/back/util/promisify.js
@@ -0,0 +1,47 @@
+
+module.exports = {
+ promisify,
+ promisifyObject
+};
+
+/**
+ * Promisifies a function wich follows the (err, res) => {} pattern as last
+ * function argument and returns the promisified version.
+ *
+ * @param {Function} fn Function to promisify
+ * @return {Function} The promisified function
+ */
+function promisify(fn) {
+ return function(...args) {
+ let thisArg = this;
+ let orgCb = args[args.length - 1];
+ if (typeof orgCb !== 'function') orgCb = null;
+
+ return new Promise(function(resolve, reject) {
+ function cb(err, res) {
+ if (orgCb) orgCb(err, res);
+ err ? reject(err) : resolve(res);
+ }
+
+ if (orgCb)
+ args[args.length - 1] = cb;
+ else
+ args.push(cb);
+
+ fn.apply(thisArg, args);
+ });
+ };
+}
+
+/**
+ * Promisifies object methods.
+ *
+ * @param {Object} obj Object to promisify
+ * @param {Array} methods Array of method names to promisify
+ */
+function promisifyObject(obj, methods) {
+ for (let method of methods) {
+ let orgMethod = obj[method];
+ obj[method] = promisify(orgMethod);
+ }
+}
diff --git a/modules/account/front/acl/create/index.html b/modules/account/front/acl/create/index.html
new file mode 100644
index 000000000..96fe5abad
--- /dev/null
+++ b/modules/account/front/acl/create/index.html
@@ -0,0 +1,65 @@
+
+
+
diff --git a/modules/account/front/acl/create/index.js b/modules/account/front/acl/create/index.js
new file mode 100644
index 000000000..fea71991f
--- /dev/null
+++ b/modules/account/front/acl/create/index.js
@@ -0,0 +1,33 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ constructor(...args) {
+ super(...args);
+ this.accessTypes = [
+ {name: '*'},
+ {name: 'READ'},
+ {name: 'WRITE'}
+ ];
+ this.permissions = [
+ {name: 'ALLOW'},
+ {name: 'DENY'}
+ ];
+
+ this.models = [];
+ for (let model in window.validations)
+ this.models.push({name: model});
+
+ this.acl = {
+ property: '*',
+ principalType: 'ROLE',
+ accessType: 'READ',
+ permission: 'ALLOW'
+ };
+ }
+}
+
+ngModule.component('vnAclCreate', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/acl/index.js b/modules/account/front/acl/index.js
new file mode 100644
index 000000000..8393859a5
--- /dev/null
+++ b/modules/account/front/acl/index.js
@@ -0,0 +1,4 @@
+import './main';
+import './index/';
+import './create';
+import './search-panel';
diff --git a/modules/account/front/acl/index/index.html b/modules/account/front/acl/index/index.html
new file mode 100644
index 000000000..af06ec481
--- /dev/null
+++ b/modules/account/front/acl/index/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ {{::row.model}}.{{::row.property}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/acl/index/index.js b/modules/account/front/acl/index/index.js
new file mode 100644
index 000000000..a2aec534a
--- /dev/null
+++ b/modules/account/front/acl/index/index.js
@@ -0,0 +1,15 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ onDelete(row) {
+ return this.$http.delete(`ACLs/${row.id}`)
+ .then(() => this.$.model.refresh())
+ .then(() => this.vnApp.showSuccess(this.$t('ACL removed')));
+ }
+}
+
+ngModule.component('vnAclIndex', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/acl/index/locale/es.yml b/modules/account/front/acl/index/locale/es.yml
new file mode 100644
index 000000000..8024f804c
--- /dev/null
+++ b/modules/account/front/acl/index/locale/es.yml
@@ -0,0 +1,4 @@
+New ACL: Nuevo ACL
+Edit ACL: Editar ACL
+ACL will be removed: El ACL será eliminado
+ACL removed: ACL eliminado
diff --git a/modules/account/front/acl/locale/es.yml b/modules/account/front/acl/locale/es.yml
new file mode 100644
index 000000000..ff6a1b41c
--- /dev/null
+++ b/modules/account/front/acl/locale/es.yml
@@ -0,0 +1,4 @@
+Model: Modelo
+Property: Propiedad
+Access type: Tipo de acceso
+Permission: Permiso
\ No newline at end of file
diff --git a/modules/account/front/acl/main/index.html b/modules/account/front/acl/main/index.html
new file mode 100644
index 000000000..7767768d9
--- /dev/null
+++ b/modules/account/front/acl/main/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/acl/main/index.js b/modules/account/front/acl/main/index.js
new file mode 100644
index 000000000..a91a71cb7
--- /dev/null
+++ b/modules/account/front/acl/main/index.js
@@ -0,0 +1,18 @@
+import ngModule from '../../module';
+import ModuleMain from 'salix/components/module-main';
+
+export default class ACL extends ModuleMain {
+ exprBuilder(param, value) {
+ switch (param) {
+ case 'search':
+ return {model: {like: `%${value}%`}};
+ default:
+ return {[param]: value};
+ }
+ }
+}
+
+ngModule.vnComponent('vnAclComponent', {
+ controller: ACL,
+ template: require('./index.html')
+});
diff --git a/modules/account/front/acl/search-panel/index.html b/modules/account/front/acl/search-panel/index.html
new file mode 100644
index 000000000..b83b9c255
--- /dev/null
+++ b/modules/account/front/acl/search-panel/index.html
@@ -0,0 +1,39 @@
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/acl/search-panel/index.js b/modules/account/front/acl/search-panel/index.js
new file mode 100644
index 000000000..4f571059e
--- /dev/null
+++ b/modules/account/front/acl/search-panel/index.js
@@ -0,0 +1,26 @@
+import ngModule from '../../module';
+import SearchPanel from 'core/components/searchbar/search-panel';
+
+export default class Controller extends SearchPanel {
+ constructor(...args) {
+ super(...args);
+ this.accessTypes = [
+ {name: '*'},
+ {name: 'READ'},
+ {name: 'WRITE'}
+ ];
+ this.permissions = [
+ {name: 'ALLOW'},
+ {name: 'DENY'}
+ ];
+
+ this.models = [];
+ for (let model in window.validations)
+ this.models.push({name: model});
+ }
+}
+
+ngModule.component('vnAclSearchPanel', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/alias/basic-data/index.html b/modules/account/front/alias/basic-data/index.html
new file mode 100644
index 000000000..ede77f929
--- /dev/null
+++ b/modules/account/front/alias/basic-data/index.html
@@ -0,0 +1,44 @@
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/alias/basic-data/index.js b/modules/account/front/alias/basic-data/index.js
new file mode 100644
index 000000000..b7c2db089
--- /dev/null
+++ b/modules/account/front/alias/basic-data/index.js
@@ -0,0 +1,12 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {}
+
+ngModule.component('vnAliasBasicData', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ alias: '<'
+ }
+});
diff --git a/modules/account/front/alias/card/index.html b/modules/account/front/alias/card/index.html
new file mode 100644
index 000000000..712147a24
--- /dev/null
+++ b/modules/account/front/alias/card/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/modules/account/front/alias/card/index.js b/modules/account/front/alias/card/index.js
new file mode 100644
index 000000000..fd1a18f6a
--- /dev/null
+++ b/modules/account/front/alias/card/index.js
@@ -0,0 +1,14 @@
+import ngModule from '../../module';
+import ModuleCard from 'salix/components/module-card';
+
+class Controller extends ModuleCard {
+ reload() {
+ this.$http.get(`MailAliases/${this.$params.id}`)
+ .then(res => this.alias = res.data);
+ }
+}
+
+ngModule.vnComponent('vnAliasCard', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/alias/create/index.html b/modules/account/front/alias/create/index.html
new file mode 100644
index 000000000..dee59d26e
--- /dev/null
+++ b/modules/account/front/alias/create/index.html
@@ -0,0 +1,33 @@
+
+
+
diff --git a/modules/account/front/alias/create/index.js b/modules/account/front/alias/create/index.js
new file mode 100644
index 000000000..c058c3adf
--- /dev/null
+++ b/modules/account/front/alias/create/index.js
@@ -0,0 +1,15 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ onSubmit() {
+ return this.$.watcher.submit().then(res =>
+ this.$state.go('account.alias.card.basicData', {id: res.data.id})
+ );
+ }
+}
+
+ngModule.component('vnAliasCreate', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/alias/descriptor/index.html b/modules/account/front/alias/descriptor/index.html
new file mode 100644
index 000000000..71b98c6a3
--- /dev/null
+++ b/modules/account/front/alias/descriptor/index.html
@@ -0,0 +1,27 @@
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/alias/descriptor/index.js b/modules/account/front/alias/descriptor/index.js
new file mode 100644
index 000000000..a21baae5a
--- /dev/null
+++ b/modules/account/front/alias/descriptor/index.js
@@ -0,0 +1,26 @@
+import ngModule from '../../module';
+import Descriptor from 'salix/components/descriptor';
+
+class Controller extends Descriptor {
+ get alias() {
+ return this.entity;
+ }
+
+ set alias(value) {
+ this.entity = value;
+ }
+
+ onDelete() {
+ return this.$http.delete(`MailAliases/${this.id}`)
+ .then(() => this.$state.go('account.alias'))
+ .then(() => this.vnApp.showSuccess(this.$t('Alias removed')));
+ }
+}
+
+ngModule.component('vnAliasDescriptor', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ alias: '<'
+ }
+});
diff --git a/modules/account/front/alias/descriptor/locale/es.yml b/modules/account/front/alias/descriptor/locale/es.yml
new file mode 100644
index 000000000..9c6fa0e73
--- /dev/null
+++ b/modules/account/front/alias/descriptor/locale/es.yml
@@ -0,0 +1,2 @@
+Alias will be removed: El alias será eliminado
+Alias removed: Alias eliminado
\ No newline at end of file
diff --git a/modules/account/front/alias/index.js b/modules/account/front/alias/index.js
new file mode 100644
index 000000000..8eed3a3d3
--- /dev/null
+++ b/modules/account/front/alias/index.js
@@ -0,0 +1,9 @@
+import './main';
+import './index/';
+import './create';
+import './summary';
+import './card';
+import './descriptor';
+import './create';
+import './basic-data';
+import './users';
diff --git a/modules/account/front/alias/index/index.html b/modules/account/front/alias/index/index.html
new file mode 100644
index 000000000..d140973ba
--- /dev/null
+++ b/modules/account/front/alias/index/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ {{::alias.alias}}
+ {{::alias.description}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/alias/index/index.js b/modules/account/front/alias/index/index.js
new file mode 100644
index 000000000..44e146fb4
--- /dev/null
+++ b/modules/account/front/alias/index/index.js
@@ -0,0 +1,14 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ preview(alias) {
+ this.selectedAlias = alias;
+ this.$.summary.show();
+ }
+}
+
+ngModule.component('vnAliasIndex', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/alias/index/locale/es.yml b/modules/account/front/alias/index/locale/es.yml
new file mode 100644
index 000000000..4df41c0be
--- /dev/null
+++ b/modules/account/front/alias/index/locale/es.yml
@@ -0,0 +1,2 @@
+New alias: Nuevo alias
+View alias: Ver alias
\ No newline at end of file
diff --git a/modules/account/front/alias/locale/es.yml b/modules/account/front/alias/locale/es.yml
new file mode 100644
index 000000000..ecc856fcf
--- /dev/null
+++ b/modules/account/front/alias/locale/es.yml
@@ -0,0 +1 @@
+Public: Público
\ No newline at end of file
diff --git a/modules/account/front/alias/main/index.html b/modules/account/front/alias/main/index.html
new file mode 100644
index 000000000..43f6e2f51
--- /dev/null
+++ b/modules/account/front/alias/main/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/alias/main/index.js b/modules/account/front/alias/main/index.js
new file mode 100644
index 000000000..21eed3d85
--- /dev/null
+++ b/modules/account/front/alias/main/index.js
@@ -0,0 +1,18 @@
+import ngModule from '../../module';
+import ModuleMain from 'salix/components/module-main';
+
+export default class Alias extends ModuleMain {
+ exprBuilder(param, value) {
+ switch (param) {
+ case 'search':
+ return /^\d+$/.test(value)
+ ? {id: value}
+ : {alias: {like: `%${value}%`}};
+ }
+ }
+}
+
+ngModule.vnComponent('vnAlias', {
+ controller: Alias,
+ template: require('./index.html')
+});
diff --git a/modules/account/front/alias/summary/index.html b/modules/account/front/alias/summary/index.html
new file mode 100644
index 000000000..52ee2813d
--- /dev/null
+++ b/modules/account/front/alias/summary/index.html
@@ -0,0 +1,16 @@
+
+ {{summary.alias}}
+
+
+ Basic data
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/alias/summary/index.js b/modules/account/front/alias/summary/index.js
new file mode 100644
index 000000000..21bc8d9ba
--- /dev/null
+++ b/modules/account/front/alias/summary/index.js
@@ -0,0 +1,25 @@
+import ngModule from '../../module';
+import Component from 'core/lib/component';
+
+class Controller extends Component {
+ set alias(value) {
+ this._alias = value;
+ this.$.summary = null;
+ if (!value) return;
+
+ this.$http.get(`MailAliases/${value.id}`)
+ .then(res => this.$.summary = res.data);
+ }
+
+ get alias() {
+ return this._alias;
+ }
+}
+
+ngModule.component('vnAliasSummary', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ alias: '<'
+ }
+});
diff --git a/modules/account/front/alias/users/index.html b/modules/account/front/alias/users/index.html
new file mode 100644
index 000000000..048a702ea
--- /dev/null
+++ b/modules/account/front/alias/users/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {{::row.user.name}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/account/front/alias/users/index.js b/modules/account/front/alias/users/index.js
new file mode 100644
index 000000000..b2446d71b
--- /dev/null
+++ b/modules/account/front/alias/users/index.js
@@ -0,0 +1,31 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ $onInit() {
+ let filter = {
+ include: {
+ relation: 'user',
+ scope: {
+ fields: ['id', 'name']
+ }
+ }
+ };
+ this.$http.get(`MailAliases/${this.$params.id}/accounts`, {filter})
+ .then(res => this.$.data = res.data);
+ }
+
+ onRemove(row) {
+ return this.$http.delete(`MailAliases/${this.$params.id}/accounts/${row.id}`)
+ .then(() => {
+ let index = this.$.data.indexOf(row);
+ if (index !== -1) this.$.data.splice(index, 1);
+ this.vnApp.showSuccess(this.$t('User removed'));
+ });
+ }
+}
+
+ngModule.component('vnAliasUsers', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/alias/users/index.spec.js b/modules/account/front/alias/users/index.spec.js
new file mode 100644
index 000000000..d618f1de1
--- /dev/null
+++ b/modules/account/front/alias/users/index.spec.js
@@ -0,0 +1,42 @@
+import './index';
+
+describe('component vnAliasUsers', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnAliasUsers', {$element: null});
+ controller.$params.id = 1;
+ }));
+
+ describe('$onInit()', () => {
+ it('should delete entity and go to index', () => {
+ $httpBackend.expectGET('MailAliases/1/accounts').respond('foo');
+ controller.$onInit();
+ $httpBackend.flush();
+
+ expect(controller.$.data).toBe('foo');
+ });
+ });
+
+ describe('onRemove()', () => {
+ it('should call backend method to change role', () => {
+ jest.spyOn(controller.vnApp, 'showSuccess');
+
+ controller.$.data = [
+ {id: 1, alias: 'foo'},
+ {id: 2, alias: 'bar'}
+ ];
+
+ $httpBackend.expectDELETE('MailAliases/1/accounts/1').respond();
+ controller.onRemove(controller.$.data[0]);
+ $httpBackend.flush();
+
+ expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]);
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/modules/account/front/alias/users/locale/es.yml b/modules/account/front/alias/users/locale/es.yml
new file mode 100644
index 000000000..dc24eb318
--- /dev/null
+++ b/modules/account/front/alias/users/locale/es.yml
@@ -0,0 +1,2 @@
+User will be removed from alias: El usuario será borrado del alias
+User removed: Usuario borrado
\ No newline at end of file
diff --git a/modules/account/front/aliases/index.html b/modules/account/front/aliases/index.html
new file mode 100644
index 000000000..9f4ba857f
--- /dev/null
+++ b/modules/account/front/aliases/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+ {{::row.alias.alias}}
+
+
+ {{::row.alias.description}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account not enabled
+
diff --git a/modules/account/front/aliases/index.js b/modules/account/front/aliases/index.js
new file mode 100644
index 000000000..0fc806a71
--- /dev/null
+++ b/modules/account/front/aliases/index.js
@@ -0,0 +1,51 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ $onInit() {
+ this.refresh();
+ }
+
+ refresh() {
+ let filter = {
+ where: {account: this.$params.id},
+ include: {
+ relation: 'alias',
+ scope: {
+ fields: ['id', 'alias', 'description']
+ }
+ }
+ };
+ return this.$http.get(`MailAliasAccounts`, {filter})
+ .then(res => this.$.data = res.data);
+ }
+
+ onAddClick() {
+ this.addData = {account: this.$params.id};
+ this.$.dialog.show();
+ }
+
+ onAddSave() {
+ return this.$http.post(`MailAliasAccounts`, this.addData)
+ .then(() => this.refresh())
+ .then(() => this.vnApp.showSuccess(
+ this.$t('Subscribed to alias!'))
+ );
+ }
+
+ onRemove(row) {
+ return this.$http.delete(`MailAliasAccounts/${row.id}`)
+ .then(() => {
+ this.$.data.splice(this.$.data.indexOf(row), 1);
+ this.vnApp.showSuccess(this.$t('Unsubscribed from alias!'));
+ });
+ }
+}
+
+ngModule.component('vnUserAliases', {
+ template: require('./index.html'),
+ controller: Controller,
+ require: {
+ card: '^vnUserCard'
+ }
+});
diff --git a/modules/account/front/aliases/index.spec.js b/modules/account/front/aliases/index.spec.js
new file mode 100644
index 000000000..466f1e1e9
--- /dev/null
+++ b/modules/account/front/aliases/index.spec.js
@@ -0,0 +1,53 @@
+import './index';
+
+describe('component vnUserAliases', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnUserAliases', {$element: null});
+ jest.spyOn(controller.vnApp, 'showSuccess');
+ }));
+
+ describe('refresh()', () => {
+ it('should refresh the controller data', () => {
+ $httpBackend.expectGET('MailAliasAccounts').respond('foo');
+ controller.refresh();
+ $httpBackend.flush();
+
+ expect(controller.$.data).toBe('foo');
+ });
+ });
+
+ describe('onAddSave()', () => {
+ it('should add the new row', () => {
+ controller.addData = {account: 1};
+
+ $httpBackend.expectPOST('MailAliasAccounts').respond();
+ $httpBackend.expectGET('MailAliasAccounts').respond('foo');
+ controller.onAddSave();
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+
+ describe('onRemove()', () => {
+ it('shoud remove the passed row remote and locally', () => {
+ controller.$.data = [
+ {id: 1, alias: 'foo'},
+ {id: 2, alias: 'bar'}
+ ];
+
+ $httpBackend.expectDELETE('MailAliasAccounts/1').respond();
+ controller.onRemove(controller.$.data[0]);
+ $httpBackend.flush();
+
+ expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]);
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/modules/account/front/aliases/locale/es.yml b/modules/account/front/aliases/locale/es.yml
new file mode 100644
index 000000000..4d1ad76a7
--- /dev/null
+++ b/modules/account/front/aliases/locale/es.yml
@@ -0,0 +1,3 @@
+Unsubscribe: Desuscribir
+Subscribed to alias!: ¡Suscrito al alias!
+Unsubscribed from alias!: ¡Desuscrito del alias!
\ No newline at end of file
diff --git a/modules/account/front/basic-data/index.html b/modules/account/front/basic-data/index.html
new file mode 100644
index 000000000..d21dda45a
--- /dev/null
+++ b/modules/account/front/basic-data/index.html
@@ -0,0 +1,52 @@
+
+
+
diff --git a/modules/account/front/basic-data/index.js b/modules/account/front/basic-data/index.js
new file mode 100644
index 000000000..243a46068
--- /dev/null
+++ b/modules/account/front/basic-data/index.js
@@ -0,0 +1,20 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ onSubmit() {
+ this.$.watcher.submit()
+ .then(() => this.card.reload());
+ }
+}
+
+ngModule.component('vnUserBasicData', {
+ template: require('./index.html'),
+ controller: Controller,
+ require: {
+ card: '^vnUserCard'
+ },
+ bindings: {
+ user: '<'
+ }
+});
diff --git a/modules/account/front/card/index.html b/modules/account/front/card/index.html
new file mode 100644
index 000000000..cba6b93c6
--- /dev/null
+++ b/modules/account/front/card/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/modules/account/front/card/index.js b/modules/account/front/card/index.js
new file mode 100644
index 000000000..5266592f3
--- /dev/null
+++ b/modules/account/front/card/index.js
@@ -0,0 +1,28 @@
+import ngModule from '../module';
+import ModuleCard from 'salix/components/module-card';
+import './style.scss';
+
+class Controller extends ModuleCard {
+ reload() {
+ const filter = {
+ include: {
+ relation: 'role',
+ scope: {
+ fields: ['id', 'name']
+ }
+ }
+ };
+
+ return Promise.all([
+ this.$http.get(`Accounts/${this.$params.id}`, {filter})
+ .then(res => this.user = res.data),
+ this.$http.get(`UserAccounts/${this.$params.id}/exists`)
+ .then(res => this.hasAccount = res.data.exists)
+ ]);
+ }
+}
+
+ngModule.vnComponent('vnUserCard', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/card/index.spec.js b/modules/account/front/card/index.spec.js
new file mode 100644
index 000000000..cd28c458a
--- /dev/null
+++ b/modules/account/front/card/index.spec.js
@@ -0,0 +1,27 @@
+import './index';
+
+describe('component vnUserCard', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnUserCard', {$element: null});
+ }));
+
+ describe('reload()', () => {
+ it('should reload the controller data', () => {
+ controller.$params.id = 1;
+
+ $httpBackend.expectGET('Accounts/1').respond('foo');
+ $httpBackend.expectGET('UserAccounts/1/exists').respond({exists: true});
+ controller.reload();
+ $httpBackend.flush();
+
+ expect(controller.user).toBe('foo');
+ expect(controller.hasAccount).toBeTruthy();
+ });
+ });
+});
diff --git a/modules/account/front/card/style.scss b/modules/account/front/card/style.scss
new file mode 100644
index 000000000..4d9d108a0
--- /dev/null
+++ b/modules/account/front/card/style.scss
@@ -0,0 +1,10 @@
+@import "variables";
+
+.bg-title {
+ display: block;
+ text-align: center;
+ padding: 24px;
+ box-sizing: border-box;
+ color: $color-font-secondary;
+ font-size: 1.375rem;
+}
diff --git a/modules/account/front/connections/index.html b/modules/account/front/connections/index.html
new file mode 100644
index 000000000..d634b7a9f
--- /dev/null
+++ b/modules/account/front/connections/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ {{::row.user.username}}
+ {{::row.created | date:'dd/MM HH:mm'}}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/connections/index.js b/modules/account/front/connections/index.js
new file mode 100644
index 000000000..c4ddd5615
--- /dev/null
+++ b/modules/account/front/connections/index.js
@@ -0,0 +1,29 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ constructor(...args) {
+ super(...args);
+ this.filter = {
+ fields: ['id', 'created', 'userId'],
+ include: {
+ relation: 'user',
+ scope: {
+ fields: ['username']
+ }
+ },
+ order: 'created DESC'
+ };
+ }
+
+ onDisconnect(row) {
+ return this.$http.delete(`AccessTokens/${row.id}`)
+ .then(() => this.$.model.refresh())
+ .then(() => this.vnApp.showSuccess(this.$t('Session killed')));
+ }
+}
+
+ngModule.component('vnConnections', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/connections/locale/es.yml b/modules/account/front/connections/locale/es.yml
new file mode 100644
index 000000000..41ef18b45
--- /dev/null
+++ b/modules/account/front/connections/locale/es.yml
@@ -0,0 +1,5 @@
+Go to user: Ir al usuario
+Refresh: Actualizar
+Session will be killed: Se va a matar la sesión
+Kill session: Matar sesión
+Session killed: Sesión matada
\ No newline at end of file
diff --git a/modules/account/front/create/index.html b/modules/account/front/create/index.html
new file mode 100644
index 000000000..407ac0e3c
--- /dev/null
+++ b/modules/account/front/create/index.html
@@ -0,0 +1,53 @@
+
+
+
diff --git a/modules/account/front/create/index.js b/modules/account/front/create/index.js
new file mode 100644
index 000000000..41fd718f6
--- /dev/null
+++ b/modules/account/front/create/index.js
@@ -0,0 +1,15 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ onSubmit() {
+ return this.$.watcher.submit().then(res => {
+ this.$state.go('account.card.basicData', {id: res.data.id});
+ });
+ }
+}
+
+ngModule.component('vnUserCreate', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/descriptor/__snapshots__/index.spec.js.snap b/modules/account/front/descriptor/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..de5f8e8c2
--- /dev/null
+++ b/modules/account/front/descriptor/__snapshots__/index.spec.js.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component vnUserDescriptor onPassChange() should throw an error when password is empty 1`] = `"You must enter a new password"`;
+
+exports[`component vnUserDescriptor onPassChange() should throw an error when repeat password not matches new password 1`] = `"Passwords don't match"`;
diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html
new file mode 100644
index 000000000..88b1a9c6d
--- /dev/null
+++ b/modules/account/front/descriptor/index.html
@@ -0,0 +1,164 @@
+
+
+
+ Delete
+
+
+ Change role
+
+
+ Change password
+
+
+ Set password
+
+
+ Enable account
+
+
+ Disable account
+
+
+ Activate user
+
+
+ Deactivate user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js
new file mode 100644
index 000000000..3f27b1f76
--- /dev/null
+++ b/modules/account/front/descriptor/index.js
@@ -0,0 +1,123 @@
+import ngModule from '../module';
+import Descriptor from 'salix/components/descriptor';
+import UserError from 'core/lib/user-error';
+
+class Controller extends Descriptor {
+ get user() {
+ return this.entity;
+ }
+
+ set user(value) {
+ this.entity = value;
+ }
+
+ get entity() {
+ return super.entity;
+ }
+
+ set entity(value) {
+ super.entity = value;
+ this.hasAccount = null;
+ if (!value) return;
+
+ this.$http.get(`UserAccounts/${value.id}/exists`)
+ .then(res => this.hasAccount = res.data.exists);
+ }
+
+ onDelete() {
+ return this.$http.delete(`Accounts/${this.id}`)
+ .then(() => this.$state.go('account.index'))
+ .then(() => this.vnApp.showSuccess(this.$t('User removed')));
+ }
+
+ onChangeRole() {
+ this.newRole = this.user.role.id;
+ this.$.changeRole.show();
+ }
+
+ onChangeRoleAccept() {
+ const params = {roleFk: this.newRole};
+ return this.$http.patch(`Accounts/${this.id}`, params)
+ .then(() => {
+ this.emit('change');
+ this.vnApp.showSuccess(this.$t('Role changed succesfully!'));
+ });
+ }
+
+ onChangePassClick(askOldPass) {
+ this.$http.get('UserPasswords/findOne')
+ .then(res => {
+ this.passRequirements = res.data;
+ this.askOldPass = askOldPass;
+ this.$.changePass.show();
+ });
+ }
+
+ onPassChange() {
+ if (!this.newPassword)
+ throw new UserError(`You must enter a new password`);
+ if (this.newPassword != this.repeatPassword)
+ throw new UserError(`Passwords don't match`);
+
+ let method;
+ const params = {newPassword: this.newPassword};
+
+ if (this.askOldPass) {
+ method = 'changePassword';
+ params.oldPassword = this.oldPassword;
+ } else
+ method = 'setPassword';
+
+ return this.$http.patch(`Accounts/${this.id}/${method}`, params)
+ .then(() => {
+ this.emit('change');
+ this.vnApp.showSuccess(this.$t('Password changed succesfully!'));
+ });
+ }
+
+ onPassClose() {
+ this.oldPassword = '';
+ this.newPassword = '';
+ this.repeatPassword = '';
+ this.$.$apply();
+ }
+
+ onEnableAccount() {
+ return this.$http.post(`UserAccounts`, {id: this.id})
+ .then(() => this.onSwitchAccount(true));
+ }
+
+ onDisableAccount() {
+ return this.$http.delete(`UserAccounts/${this.id}`)
+ .then(() => this.onSwitchAccount(false));
+ }
+
+ onSwitchAccount(enable) {
+ this.hasAccount = enable;
+ const message = enable
+ ? 'Account enabled!'
+ : 'Account disabled!';
+ this.emit('change');
+ this.vnApp.showSuccess(this.$t(message));
+ }
+
+ onSetActive(active) {
+ return this.$http.patch(`Accounts/${this.id}`, {active})
+ .then(() => {
+ this.user.active = active;
+ const message = active
+ ? 'User activated!'
+ : 'User deactivated!';
+ this.emit('change');
+ this.vnApp.showSuccess(this.$t(message));
+ });
+ }
+}
+
+ngModule.component('vnUserDescriptor', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ user: '<'
+ }
+});
diff --git a/modules/account/front/descriptor/index.spec.js b/modules/account/front/descriptor/index.spec.js
new file mode 100644
index 000000000..8ee67a304
--- /dev/null
+++ b/modules/account/front/descriptor/index.spec.js
@@ -0,0 +1,108 @@
+import './index';
+
+describe('component vnUserDescriptor', () => {
+ let controller;
+ let $httpBackend;
+
+ let user = {id: 1, name: 'foo'};
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.whenGET('UserAccounts/1/exists').respond({exists: true});
+
+ controller = $componentController('vnUserDescriptor', {$element: null}, {user});
+ jest.spyOn(controller, 'emit');
+ jest.spyOn(controller.vnApp, 'showSuccess');
+ }));
+
+ describe('onDelete()', () => {
+ it('should delete entity and go to index', () => {
+ controller.$state.go = jest.fn();
+
+ $httpBackend.expectDELETE('Accounts/1').respond();
+ controller.onDelete();
+ $httpBackend.flush();
+
+ expect(controller.$state.go).toHaveBeenCalledWith('account.index');
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+
+ describe('onChangeRoleAccept()', () => {
+ it('should call backend method to change role', () => {
+ $httpBackend.expectPATCH('Accounts/1').respond();
+ controller.onChangeRoleAccept();
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ expect(controller.emit).toHaveBeenCalledWith('change');
+ });
+ });
+
+ describe('onPassChange()', () => {
+ it('should throw an error when password is empty', () => {
+ expect(() => {
+ controller.onPassChange();
+ }).toThrowErrorMatchingSnapshot();
+ });
+
+ it('should throw an error when repeat password not matches new password', () => {
+ controller.newPassword = 'foo';
+ controller.repeatPassword = 'bar';
+
+ expect(() => {
+ controller.onPassChange();
+ }).toThrowErrorMatchingSnapshot();
+ });
+
+ it('should make a request when password checks passes', () => {
+ controller.newPassword = 'foo';
+ controller.repeatPassword = 'foo';
+
+ $httpBackend.expectPATCH('Accounts/1/setPassword').respond();
+ controller.onPassChange();
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ expect(controller.emit).toHaveBeenCalledWith('change');
+ });
+ });
+
+ describe('onEnableAccount()', () => {
+ it('should make request to enable account', () => {
+ $httpBackend.expectPOST('UserAccounts', {id: 1}).respond();
+ controller.onEnableAccount();
+ $httpBackend.flush();
+
+ expect(controller.hasAccount).toBeTruthy();
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ expect(controller.emit).toHaveBeenCalledWith('change');
+ });
+ });
+
+ describe('onDisableAccount()', () => {
+ it('should make request to disable account', () => {
+ $httpBackend.expectDELETE('UserAccounts/1').respond();
+ controller.onDisableAccount();
+ $httpBackend.flush();
+
+ expect(controller.hasAccount).toBeFalsy();
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ expect(controller.emit).toHaveBeenCalledWith('change');
+ });
+ });
+
+ describe('onSetActive()', () => {
+ it('should make request to activate/deactivate the user', () => {
+ $httpBackend.expectPATCH('Accounts/1', {active: true}).respond();
+ controller.onSetActive(true);
+ $httpBackend.flush();
+
+ expect(controller.user.active).toBeTruthy();
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ expect(controller.emit).toHaveBeenCalledWith('change');
+ });
+ });
+});
diff --git a/modules/account/front/descriptor/locale/es.yml b/modules/account/front/descriptor/locale/es.yml
new file mode 100644
index 000000000..5e8242819
--- /dev/null
+++ b/modules/account/front/descriptor/locale/es.yml
@@ -0,0 +1,31 @@
+User will be removed: El usuario será eliminado
+User removed: Usuario eliminado
+Are you sure you want to continue?: ¿Seguro que quieres continuar?
+Account will be enabled: La cuenta será habilitada
+Account will be disabled: La cuenta será deshabilitada
+Account enabled!: ¡Cuenta habilitada!
+Account disabled!: ¡Cuenta deshabilitada!
+User will activated: El usuario será activado
+User will be deactivated: El usuario será desactivado
+User activated!: ¡Usuario activado!
+User deactivated!: ¡Usuario desactivado!
+Account enabled: Cuenta habilitada
+User deactivated: Usuario desactivado
+Change role: Modificar rol
+Change password: Cambiar contraseña
+Set password: Establecer contraseña
+Enable account: Habilitar cuenta
+Disable account: Deshabilitar cuenta
+Activate user: Activar usuario
+Deactivate user: Desactivar usuario
+Old password: Contraseña antigua
+New password: Nueva contraseña
+Repeat password: Repetir contraseña
+Password changed succesfully!: ¡Contraseña modificada correctamente!
+Role changed succesfully!: ¡Rol modificado correctamente!
+Password requirements: >
+ La contraseña debe tener al menos {{ length }} caracteres de longitud,
+ {{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}}
+ dígitos y {{nPunct}} símbolos (Ej: $%&.)
+You must enter a new password: Debes introducir la nueva contraseña
+Passwords don't match: Las contraseñas no coinciden
diff --git a/modules/account/front/index.js b/modules/account/front/index.js
new file mode 100644
index 000000000..359fdff72
--- /dev/null
+++ b/modules/account/front/index.js
@@ -0,0 +1,17 @@
+export * from './module';
+
+import './main';
+import './index/';
+import './role';
+import './alias';
+import './connections';
+import './acl';
+import './summary';
+import './card';
+import './descriptor';
+import './search-panel';
+import './create';
+import './basic-data';
+import './mail-forwarding';
+import './aliases';
+import './roles';
diff --git a/modules/account/front/index/index.html b/modules/account/front/index/index.html
new file mode 100644
index 000000000..3c967250d
--- /dev/null
+++ b/modules/account/front/index/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/index/index.js b/modules/account/front/index/index.js
new file mode 100644
index 000000000..9324ca740
--- /dev/null
+++ b/modules/account/front/index/index.js
@@ -0,0 +1,14 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ preview(user) {
+ this.selectedUser = user;
+ this.$.summary.show();
+ }
+}
+
+ngModule.component('vnUserIndex', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/index/locale/es.yml b/modules/account/front/index/locale/es.yml
new file mode 100644
index 000000000..074fb054e
--- /dev/null
+++ b/modules/account/front/index/locale/es.yml
@@ -0,0 +1,2 @@
+New user: Nuevo usuario
+View user: Ver usuario
\ No newline at end of file
diff --git a/modules/account/front/locale/es.yml b/modules/account/front/locale/es.yml
new file mode 100644
index 000000000..02c58a8b0
--- /dev/null
+++ b/modules/account/front/locale/es.yml
@@ -0,0 +1,10 @@
+Active: Activo
+Connections: Conexiones
+Description: Descripción
+Name: Nombre
+Nickname: Nombre mostrado
+Personal email: Correo personal
+Role: Rol
+Mail aliases: Alias de correo
+Account not enabled: Cuenta no habilitada
+Inherited roles: Roles heredados
diff --git a/modules/account/front/mail-forwarding/index.html b/modules/account/front/mail-forwarding/index.html
new file mode 100644
index 000000000..a6be2782a
--- /dev/null
+++ b/modules/account/front/mail-forwarding/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ Account not enabled
+
diff --git a/modules/account/front/mail-forwarding/index.js b/modules/account/front/mail-forwarding/index.js
new file mode 100644
index 000000000..5118e8eab
--- /dev/null
+++ b/modules/account/front/mail-forwarding/index.js
@@ -0,0 +1,12 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {}
+
+ngModule.component('vnUserMailForwarding', {
+ template: require('./index.html'),
+ controller: Controller,
+ require: {
+ card: '^vnUserCard'
+ },
+});
diff --git a/modules/account/front/mail-forwarding/locale/es.yml b/modules/account/front/mail-forwarding/locale/es.yml
new file mode 100644
index 000000000..0322e3e42
--- /dev/null
+++ b/modules/account/front/mail-forwarding/locale/es.yml
@@ -0,0 +1,6 @@
+Mail forwarding: Reenvío de correo
+Forward email: Dirección de reenvío
+Enable mail forwarding: Habilitar redirección de correo
+All emails will be forwarded to the specified address.: >
+ Todos los correos serán reenviados a la dirección especificada, no se
+ mantendrá copia de los mismos en el buzón del usuario.
diff --git a/modules/account/front/main/index.html b/modules/account/front/main/index.html
new file mode 100644
index 000000000..5736b3a3b
--- /dev/null
+++ b/modules/account/front/main/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/main/index.js b/modules/account/front/main/index.js
new file mode 100644
index 000000000..a43ffb76b
--- /dev/null
+++ b/modules/account/front/main/index.js
@@ -0,0 +1,39 @@
+import ngModule from '../module';
+import ModuleMain from 'salix/components/module-main';
+
+export default class User extends ModuleMain {
+ constructor($element, $) {
+ super($element, $);
+ this.filter = {
+ fields: ['id', 'nickname', 'name', 'role'],
+ include: {
+ relation: 'role',
+ scope: {
+ fields: ['id', 'name']
+ }
+ }
+ };
+ }
+
+ exprBuilder(param, value) {
+ switch (param) {
+ case 'search':
+ return /^\d+$/.test(value)
+ ? {id: value}
+ : {or: [
+ {name: {like: `%${value}%`}},
+ {nickname: {like: `%${value}%`}}
+ ]};
+ case 'name':
+ case 'nickname':
+ return {[param]: {like: `%${value}%`}};
+ case 'roleFk':
+ return {[param]: value};
+ }
+ }
+}
+
+ngModule.vnComponent('vnUser', {
+ controller: User,
+ template: require('./index.html')
+});
diff --git a/modules/account/front/main/index.spec.js b/modules/account/front/main/index.spec.js
new file mode 100644
index 000000000..c232aa849
--- /dev/null
+++ b/modules/account/front/main/index.spec.js
@@ -0,0 +1,28 @@
+import './index';
+
+describe('component vnUser', () => {
+ let controller;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject($componentController => {
+ controller = $componentController('vnUser', {$element: null});
+ }));
+
+ describe('exprBuilder()', () => {
+ it('should search by id when only digits string is passed', () => {
+ let expr = controller.exprBuilder('search', '1');
+
+ expect(expr).toEqual({id: '1'});
+ });
+
+ it('should search by name when non-only digits string is passed', () => {
+ let expr = controller.exprBuilder('search', '1foo');
+
+ expect(expr).toEqual({or: [
+ {name: {like: '%1foo%'}},
+ {nickname: {like: '%1foo%'}}
+ ]});
+ });
+ });
+});
diff --git a/modules/account/front/module.js b/modules/account/front/module.js
new file mode 100644
index 000000000..0002f0b7a
--- /dev/null
+++ b/modules/account/front/module.js
@@ -0,0 +1,3 @@
+import {ng} from 'core/vendor';
+
+export default ng.module('account', ['vnCore']);
diff --git a/modules/account/front/role/basic-data/index.html b/modules/account/front/role/basic-data/index.html
new file mode 100644
index 000000000..e79601dcc
--- /dev/null
+++ b/modules/account/front/role/basic-data/index.html
@@ -0,0 +1,39 @@
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/basic-data/index.js b/modules/account/front/role/basic-data/index.js
new file mode 100644
index 000000000..4e26906ee
--- /dev/null
+++ b/modules/account/front/role/basic-data/index.js
@@ -0,0 +1,12 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {}
+
+ngModule.component('vnRoleBasicData', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ role: '<'
+ }
+});
diff --git a/modules/account/front/role/card/index.html b/modules/account/front/role/card/index.html
new file mode 100644
index 000000000..2f51f88b5
--- /dev/null
+++ b/modules/account/front/role/card/index.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/modules/account/front/role/card/index.js b/modules/account/front/role/card/index.js
new file mode 100644
index 000000000..6f888211d
--- /dev/null
+++ b/modules/account/front/role/card/index.js
@@ -0,0 +1,14 @@
+import ngModule from '../../module';
+import ModuleCard from 'salix/components/module-card';
+
+class Controller extends ModuleCard {
+ reload() {
+ this.$http.get(`Roles/${this.$params.id}`)
+ .then(res => this.role = res.data);
+ }
+}
+
+ngModule.vnComponent('vnRoleCard', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/role/card/index.spec.js b/modules/account/front/role/card/index.spec.js
new file mode 100644
index 000000000..f39840e5f
--- /dev/null
+++ b/modules/account/front/role/card/index.spec.js
@@ -0,0 +1,25 @@
+import './index';
+
+describe('component vnRoleCard', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnRoleCard', {$element: null});
+ }));
+
+ describe('reload()', () => {
+ it('should reload the controller data', () => {
+ controller.$params.id = 1;
+
+ $httpBackend.expectGET('Roles/1').respond('foo');
+ controller.reload();
+ $httpBackend.flush();
+
+ expect(controller.role).toBe('foo');
+ });
+ });
+});
diff --git a/modules/account/front/role/create/index.html b/modules/account/front/role/create/index.html
new file mode 100644
index 000000000..f610f6d23
--- /dev/null
+++ b/modules/account/front/role/create/index.html
@@ -0,0 +1,33 @@
+
+
+
diff --git a/modules/account/front/role/create/index.js b/modules/account/front/role/create/index.js
new file mode 100644
index 000000000..3f7fcc9cf
--- /dev/null
+++ b/modules/account/front/role/create/index.js
@@ -0,0 +1,15 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ onSubmit() {
+ return this.$.watcher.submit().then(res =>
+ this.$state.go('account.role.card.basicData', {id: res.data.id})
+ );
+ }
+}
+
+ngModule.component('vnRoleCreate', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/role/descriptor/index.html b/modules/account/front/role/descriptor/index.html
new file mode 100644
index 000000000..4cd4ac822
--- /dev/null
+++ b/modules/account/front/role/descriptor/index.html
@@ -0,0 +1,27 @@
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/descriptor/index.js b/modules/account/front/role/descriptor/index.js
new file mode 100644
index 000000000..a1b578133
--- /dev/null
+++ b/modules/account/front/role/descriptor/index.js
@@ -0,0 +1,26 @@
+import ngModule from '../../module';
+import Descriptor from 'salix/components/descriptor';
+
+class Controller extends Descriptor {
+ get role() {
+ return this.entity;
+ }
+
+ set role(value) {
+ this.entity = value;
+ }
+
+ onDelete() {
+ return this.$http.delete(`Roles/${this.id}`)
+ .then(() => this.$state.go('account.role'))
+ .then(() => this.vnApp.showSuccess(this.$t('Role removed')));
+ }
+}
+
+ngModule.component('vnRoleDescriptor', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ role: '<'
+ }
+});
diff --git a/modules/account/front/role/descriptor/index.spec.js b/modules/account/front/role/descriptor/index.spec.js
new file mode 100644
index 000000000..e2761c639
--- /dev/null
+++ b/modules/account/front/role/descriptor/index.spec.js
@@ -0,0 +1,29 @@
+import './index';
+
+describe('component vnRoleDescriptor', () => {
+ let controller;
+ let $httpBackend;
+
+ let role = {id: 1, name: 'foo'};
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnRoleDescriptor', {$element: null}, {role});
+ }));
+
+ describe('onDelete()', () => {
+ it('should delete entity and go to index', () => {
+ controller.$state.go = jest.fn();
+ jest.spyOn(controller.vnApp, 'showSuccess');
+
+ $httpBackend.expectDELETE('Roles/1').respond();
+ controller.onDelete();
+ $httpBackend.flush();
+
+ expect(controller.$state.go).toHaveBeenCalledWith('account.role');
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/modules/account/front/role/descriptor/locale/es.yml b/modules/account/front/role/descriptor/locale/es.yml
new file mode 100644
index 000000000..1ca512e4f
--- /dev/null
+++ b/modules/account/front/role/descriptor/locale/es.yml
@@ -0,0 +1,2 @@
+Role will be removed: El rol va a ser eliminado
+Role removed: Rol eliminado
\ No newline at end of file
diff --git a/modules/account/front/role/index.js b/modules/account/front/role/index.js
new file mode 100644
index 000000000..97a20d3bc
--- /dev/null
+++ b/modules/account/front/role/index.js
@@ -0,0 +1,10 @@
+import './main';
+import './index/';
+import './summary';
+import './card';
+import './descriptor';
+import './search-panel';
+import './create';
+import './basic-data';
+import './subroles';
+import './inherited';
diff --git a/modules/account/front/role/index/index.html b/modules/account/front/role/index/index.html
new file mode 100644
index 000000000..92cca58d9
--- /dev/null
+++ b/modules/account/front/role/index/index.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/index/index.js b/modules/account/front/role/index/index.js
new file mode 100644
index 000000000..40773b23b
--- /dev/null
+++ b/modules/account/front/role/index/index.js
@@ -0,0 +1,14 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ preview(role) {
+ this.selectedRole = role;
+ this.$.summary.show();
+ }
+}
+
+ngModule.component('vnRoleIndex', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/role/index/locale/es.yml b/modules/account/front/role/index/locale/es.yml
new file mode 100644
index 000000000..70932e983
--- /dev/null
+++ b/modules/account/front/role/index/locale/es.yml
@@ -0,0 +1,2 @@
+New role: Nuevo rol
+View role: Ver rol
\ No newline at end of file
diff --git a/modules/account/front/role/inherited/index.html b/modules/account/front/role/inherited/index.html
new file mode 100644
index 000000000..83ecbbff4
--- /dev/null
+++ b/modules/account/front/role/inherited/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ {{::row.inherits.name}}
+
+
+ {{::row.inherits.description}}
+
+
+
+
+
+
diff --git a/modules/account/front/role/inherited/index.js b/modules/account/front/role/inherited/index.js
new file mode 100644
index 000000000..5927493ee
--- /dev/null
+++ b/modules/account/front/role/inherited/index.js
@@ -0,0 +1,23 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ $onInit() {
+ let filter = {
+ where: {role: this.$params.id},
+ include: {
+ relation: 'inherits',
+ scope: {
+ fields: ['id', 'name', 'description']
+ }
+ }
+ };
+ this.$http.get('RoleRoles', {filter})
+ .then(res => this.$.data = res.data);
+ }
+}
+
+ngModule.component('vnRoleInherited', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/role/inherited/index.spec.js b/modules/account/front/role/inherited/index.spec.js
new file mode 100644
index 000000000..16b0c53b2
--- /dev/null
+++ b/modules/account/front/role/inherited/index.spec.js
@@ -0,0 +1,23 @@
+import './index';
+
+describe('component vnRoleInherited', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnRoleInherited', {$element: null});
+ }));
+
+ describe('$onInit()', () => {
+ it('should delete entity and go to index', () => {
+ $httpBackend.expectGET('RoleRoles').respond('foo');
+ controller.$onInit();
+ $httpBackend.flush();
+
+ expect(controller.$.data).toBe('foo');
+ });
+ });
+});
diff --git a/modules/account/front/role/locale/es.yml b/modules/account/front/role/locale/es.yml
new file mode 100644
index 000000000..159fc7f16
--- /dev/null
+++ b/modules/account/front/role/locale/es.yml
@@ -0,0 +1 @@
+Subroles: Subroles
diff --git a/modules/account/front/role/main/index.html b/modules/account/front/role/main/index.html
new file mode 100644
index 000000000..9d7e6e053
--- /dev/null
+++ b/modules/account/front/role/main/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/main/index.js b/modules/account/front/role/main/index.js
new file mode 100644
index 000000000..77d15cf17
--- /dev/null
+++ b/modules/account/front/role/main/index.js
@@ -0,0 +1,24 @@
+import ngModule from '../../module';
+import ModuleMain from 'salix/components/module-main';
+
+export default class Role extends ModuleMain {
+ exprBuilder(param, value) {
+ switch (param) {
+ case 'search':
+ return /^\d+$/.test(value)
+ ? {id: value}
+ : {or: [
+ {name: {like: `%${value}%`}},
+ {nickname: {like: `%${value}%`}}
+ ]};
+ case 'name':
+ case 'description':
+ return {[param]: {like: `%${value}%`}};
+ }
+ }
+}
+
+ngModule.vnComponent('vnRole', {
+ controller: Role,
+ template: require('./index.html')
+});
diff --git a/modules/account/front/role/search-panel/index.html b/modules/account/front/role/search-panel/index.html
new file mode 100644
index 000000000..dfea9f01c
--- /dev/null
+++ b/modules/account/front/role/search-panel/index.html
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/search-panel/index.js b/modules/account/front/role/search-panel/index.js
new file mode 100644
index 000000000..35da591ad
--- /dev/null
+++ b/modules/account/front/role/search-panel/index.js
@@ -0,0 +1,7 @@
+import ngModule from '../../module';
+import SearchPanel from 'core/components/searchbar/search-panel';
+
+ngModule.component('vnRoleSearchPanel', {
+ template: require('./index.html'),
+ controller: SearchPanel
+});
diff --git a/modules/account/front/role/subroles/index.html b/modules/account/front/role/subroles/index.html
new file mode 100644
index 000000000..bc554b9f9
--- /dev/null
+++ b/modules/account/front/role/subroles/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ {{::row.inherits.name}}
+
+
+ {{::row.inherits.description}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/account/front/role/subroles/index.js b/modules/account/front/role/subroles/index.js
new file mode 100644
index 000000000..b7e1caaa4
--- /dev/null
+++ b/modules/account/front/role/subroles/index.js
@@ -0,0 +1,51 @@
+import ngModule from '../../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ $onInit() {
+ this.refresh();
+ }
+
+ get path() {
+ return `RoleInherits`;
+ }
+
+ refresh() {
+ let filter = {
+ where: {role: this.$params.id},
+ include: {
+ relation: 'inherits',
+ scope: {
+ fields: ['id', 'name', 'description']
+ }
+ }
+ };
+ this.$http.get(this.path, {filter})
+ .then(res => this.$.data = res.data);
+ }
+
+ onAddClick() {
+ this.addData = {role: this.$params.id};
+ this.$.dialog.show();
+ }
+
+ onAddSave() {
+ return this.$http.post(this.path, this.addData)
+ .then(() => this.refresh())
+ .then(() => this.vnApp.showSuccess(this.$t('Role added! Changes will take a while to fully propagate.')));
+ }
+
+ onRemove(row) {
+ return this.$http.delete(`${this.path}/${row.id}`)
+ .then(() => {
+ let index = this.$.data.indexOf(row);
+ if (index !== -1) this.$.data.splice(index, 1);
+ this.vnApp.showSuccess(this.$t('Role removed. Changes will take a while to fully propagate.'));
+ });
+ }
+}
+
+ngModule.component('vnRoleSubroles', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/role/subroles/index.spec.js b/modules/account/front/role/subroles/index.spec.js
new file mode 100644
index 000000000..e7d9a4d0e
--- /dev/null
+++ b/modules/account/front/role/subroles/index.spec.js
@@ -0,0 +1,53 @@
+import './index';
+
+describe('component vnRoleSubroles', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('account'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ controller = $componentController('vnRoleSubroles', {$element: null});
+ jest.spyOn(controller.vnApp, 'showSuccess');
+ }));
+
+ describe('refresh()', () => {
+ it('should delete entity and go to index', () => {
+ $httpBackend.expectGET('RoleInherits').respond('foo');
+ controller.refresh();
+ $httpBackend.flush();
+
+ expect(controller.$.data).toBe('foo');
+ });
+ });
+
+ describe('onAddSave()', () => {
+ it('should add a subrole', () => {
+ controller.addData = {role: 'foo'};
+
+ $httpBackend.expectPOST('RoleInherits', {role: 'foo'}).respond();
+ $httpBackend.expectGET('RoleInherits').respond();
+ controller.onAddSave();
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+
+ describe('onRemove()', () => {
+ it('should remove a subrole', () => {
+ controller.$.data = [
+ {id: 1, name: 'foo'},
+ {id: 2, name: 'bar'}
+ ];
+
+ $httpBackend.expectDELETE('RoleInherits/1').respond();
+ controller.onRemove(controller.$.data[0]);
+ $httpBackend.flush();
+
+ expect(controller.$.data).toEqual([{id: 2, name: 'bar'}]);
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/modules/account/front/role/subroles/locale/es.yml b/modules/account/front/role/subroles/locale/es.yml
new file mode 100644
index 000000000..170882405
--- /dev/null
+++ b/modules/account/front/role/subroles/locale/es.yml
@@ -0,0 +1,4 @@
+Role added! Changes will take a while to fully propagate.: >
+ ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente.
+Role removed. Changes will take a while to fully propagate.: >
+ Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente.
diff --git a/modules/account/front/role/summary/index.html b/modules/account/front/role/summary/index.html
new file mode 100644
index 000000000..f7971190c
--- /dev/null
+++ b/modules/account/front/role/summary/index.html
@@ -0,0 +1,20 @@
+
+ {{summary.name}}
+
+
+ Basic data
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/role/summary/index.js b/modules/account/front/role/summary/index.js
new file mode 100644
index 000000000..0a08fe8b2
--- /dev/null
+++ b/modules/account/front/role/summary/index.js
@@ -0,0 +1,25 @@
+import ngModule from '../../module';
+import Component from 'core/lib/component';
+
+class Controller extends Component {
+ set role(value) {
+ this._role = value;
+ this.$.summary = null;
+ if (!value) return;
+
+ this.$http.get(`Roles/${value.id}`)
+ .then(res => this.$.summary = res.data);
+ }
+
+ get role() {
+ return this._role;
+ }
+}
+
+ngModule.component('vnRoleSummary', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ role: '<'
+ }
+});
diff --git a/modules/account/front/roles/index.html b/modules/account/front/roles/index.html
new file mode 100644
index 000000000..8c8583929
--- /dev/null
+++ b/modules/account/front/roles/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ {{::row.role.name}}
+
+
+ {{::row.role.description}}
+
+
+
+
+
+
diff --git a/modules/account/front/roles/index.js b/modules/account/front/roles/index.js
new file mode 100644
index 000000000..0982dcf10
--- /dev/null
+++ b/modules/account/front/roles/index.js
@@ -0,0 +1,26 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+
+export default class Controller extends Section {
+ $onInit() {
+ let filter = {
+ where: {
+ prindicpalType: 'USER',
+ principalId: this.$params.id
+ },
+ include: {
+ relation: 'role',
+ scope: {
+ fields: ['id', 'name', 'description']
+ }
+ }
+ };
+ this.$http.get('RoleMappings', {filter})
+ .then(res => this.$.data = res.data);
+ }
+}
+
+ngModule.component('vnUserRoles', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json
new file mode 100644
index 000000000..7316fff94
--- /dev/null
+++ b/modules/account/front/routes.json
@@ -0,0 +1,199 @@
+{
+ "module": "account",
+ "name": "Users",
+ "icon" : "face",
+ "validations" : true,
+ "dependencies": [],
+ "menus": {
+ "main": [
+ {"state": "account.index", "icon": "face"},
+ {"state": "account.role", "icon": "group"},
+ {"state": "account.alias", "icon": "email"},
+ {"state": "account.acl", "icon": "check"},
+ {"state": "account.connections", "icon": "share"}
+ ],
+ "card": [
+ {"state": "account.card.basicData", "icon": "settings"},
+ {"state": "account.card.roles", "icon": "group"},
+ {"state": "account.card.mailForwarding", "icon": "forward"},
+ {"state": "account.card.aliases", "icon": "email"}
+ ],
+ "role": [
+ {"state": "account.role.card.basicData", "icon": "settings"},
+ {"state": "account.role.card.subroles", "icon": "groups"},
+ {"state": "account.role.card.inherited", "icon": "account_tree"}
+ ],
+ "alias": [
+ {"state": "account.alias.card.basicData", "icon": "settings"},
+ {"state": "account.alias.card.users", "icon": "groups"}
+ ]
+ },
+ "routes": [
+ {
+ "url": "/account",
+ "state": "account",
+ "component": "vn-user",
+ "description": "Users",
+ "abstract": true
+ }, {
+ "url": "/index?q",
+ "state": "account.index",
+ "component": "vn-user-index",
+ "description": "Users"
+ }, {
+ "url": "/create",
+ "state": "account.create",
+ "component": "vn-user-create",
+ "description": "New user"
+ }, {
+ "url": "/:id",
+ "state": "account.card",
+ "component": "vn-user-card",
+ "abstract": true,
+ "description": "Detail"
+ }, {
+ "url": "/summary",
+ "state": "account.card.summary",
+ "component": "vn-user-summary",
+ "description": "Summary",
+ "params": {
+ "user": "$ctrl.user"
+ }
+ }, {
+ "url": "/basic-data",
+ "state": "account.card.basicData",
+ "component": "vn-user-basic-data",
+ "description": "Basic data",
+ "params": {
+ "user": "$ctrl.user"
+ }
+ }, {
+ "url": "/roles",
+ "state": "account.card.roles",
+ "component": "vn-user-roles",
+ "description": "Inherited roles",
+ "params": {
+ "user": "$ctrl.user"
+ }
+ }, {
+ "url": "/mail-forwarding",
+ "state": "account.card.mailForwarding",
+ "component": "vn-user-mail-forwarding",
+ "description": "Mail forwarding",
+ "params": {
+ "user": "$ctrl.user"
+ }
+ }, {
+ "url": "/aliases",
+ "state": "account.card.aliases",
+ "component": "vn-user-aliases",
+ "description": "Mail aliases",
+ "params": {
+ "user": "$ctrl.user"
+ }
+ }, {
+ "url": "/role?q",
+ "state": "account.role",
+ "component": "vn-role",
+ "description": "Roles"
+ }, {
+ "url": "/create",
+ "state": "account.role.create",
+ "component": "vn-role-create",
+ "description": "New role"
+ }, {
+ "url": "/:id",
+ "state": "account.role.card",
+ "component": "vn-role-card",
+ "abstract": true,
+ "description": "Detail"
+ }, {
+ "url": "/summary",
+ "state": "account.role.card.summary",
+ "component": "vn-role-summary",
+ "description": "Summary",
+ "params": {
+ "role": "$ctrl.role"
+ }
+ }, {
+ "url": "/basic-data",
+ "state": "account.role.card.basicData",
+ "component": "vn-role-basic-data",
+ "description": "Basic data",
+ "acl": ["developer"],
+ "params": {
+ "role": "$ctrl.role"
+ }
+ }, {
+ "url": "/subroles",
+ "state": "account.role.card.subroles",
+ "component": "vn-role-subroles",
+ "acl": ["developer"],
+ "description": "Subroles"
+ }, {
+ "url": "/inherited",
+ "state": "account.role.card.inherited",
+ "component": "vn-role-inherited",
+ "description": "Inherited roles"
+ }, {
+ "url": "/alias?q",
+ "state": "account.alias",
+ "component": "vn-alias",
+ "description": "Mail aliases",
+ "acl": ["developer"]
+ }, {
+ "url": "/create",
+ "state": "account.alias.create",
+ "component": "vn-alias-create",
+ "description": "New alias"
+ }, {
+ "url": "/:id",
+ "state": "account.alias.card",
+ "component": "vn-alias-card",
+ "abstract": true,
+ "description": "Detail"
+ }, {
+ "url": "/summary",
+ "state": "account.alias.card.summary",
+ "component": "vn-alias-summary",
+ "description": "Summary",
+ "params": {
+ "alias": "$ctrl.alias"
+ }
+ }, {
+ "url": "/basic-data",
+ "state": "account.alias.card.basicData",
+ "component": "vn-alias-basic-data",
+ "description": "Basic data",
+ "params": {
+ "alias": "$ctrl.alias"
+ }
+ }, {
+ "url": "/users",
+ "state": "account.alias.card.users",
+ "component": "vn-alias-users",
+ "description": "Users"
+ }, {
+ "url": "/acl?q",
+ "state": "account.acl",
+ "component": "vn-acl-component",
+ "description": "ACLs",
+ "acl": ["developer"]
+ }, {
+ "url": "/create",
+ "state": "account.acl.create",
+ "component": "vn-acl-create",
+ "description": "New ACL"
+ }, {
+ "url": "/:id/edit",
+ "state": "account.acl.edit",
+ "component": "vn-acl-create",
+ "description": "Edit ACL"
+ }, {
+ "url": "/connections",
+ "state": "account.connections",
+ "component": "vn-connections",
+ "description": "Connections"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/modules/account/front/search-panel/index.html b/modules/account/front/search-panel/index.html
new file mode 100644
index 000000000..f80b537aa
--- /dev/null
+++ b/modules/account/front/search-panel/index.html
@@ -0,0 +1,31 @@
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/search-panel/index.js b/modules/account/front/search-panel/index.js
new file mode 100644
index 000000000..fff3bf7b9
--- /dev/null
+++ b/modules/account/front/search-panel/index.js
@@ -0,0 +1,7 @@
+import ngModule from '../module';
+import SearchPanel from 'core/components/searchbar/search-panel';
+
+ngModule.component('vnUserSearchPanel', {
+ template: require('./index.html'),
+ controller: SearchPanel
+});
diff --git a/modules/account/front/summary/index.html b/modules/account/front/summary/index.html
new file mode 100644
index 000000000..7f390f17c
--- /dev/null
+++ b/modules/account/front/summary/index.html
@@ -0,0 +1,20 @@
+
+ {{summary.nickname}}
+
+
+ Basic data
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/account/front/summary/index.js b/modules/account/front/summary/index.js
new file mode 100644
index 000000000..31c2d7d69
--- /dev/null
+++ b/modules/account/front/summary/index.js
@@ -0,0 +1,33 @@
+import ngModule from '../module';
+import Component from 'core/lib/component';
+
+class Controller extends Component {
+ set user(value) {
+ this._user = value;
+ this.$.summary = null;
+ if (!value) return;
+
+ const filter = {
+ include: {
+ relation: 'role',
+ scope: {
+ fields: ['id', 'name']
+ }
+ }
+ };
+ this.$http.get(`Accounts/${value.id}`, {filter})
+ .then(res => this.$.summary = res.data);
+ }
+
+ get user() {
+ return this._user;
+ }
+}
+
+ngModule.component('vnUserSummary', {
+ template: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ user: '<'
+ }
+});
diff --git a/modules/claim/front/basic-data/index.html b/modules/claim/front/basic-data/index.html
index e6c9ec2f9..c73e04b24 100644
--- a/modules/claim/front/basic-data/index.html
+++ b/modules/claim/front/basic-data/index.html
@@ -1,9 +1,9 @@
+ data="$ctrl.claim"
+ insert-mode="true"
+ form="form">
-
-
diff --git a/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js b/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js
index 7eb8e7ed9..5dbf6cb48 100644
--- a/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js
+++ b/modules/client/back/methods/client/specs/activeWorkersWithRole.spec.js
@@ -7,7 +7,7 @@ describe('Client activeWorkersWithRole', () => {
let isSalesPerson = await app.models.Account.hasRole(result[0].id, 'salesPerson');
- expect(result.length).toEqual(15);
+ expect(result.length).toEqual(16);
expect(isSalesPerson).toBeTruthy();
});
@@ -17,7 +17,7 @@ describe('Client activeWorkersWithRole', () => {
let isBuyer = await app.models.Account.hasRole(result[0].id, 'buyer');
- expect(result.length).toEqual(15);
+ expect(result.length).toEqual(16);
expect(isBuyer).toBeTruthy();
});
});
diff --git a/modules/client/back/methods/client/specs/listWorkers.spec.js b/modules/client/back/methods/client/specs/listWorkers.spec.js
index 97f4b591d..329a27aa5 100644
--- a/modules/client/back/methods/client/specs/listWorkers.spec.js
+++ b/modules/client/back/methods/client/specs/listWorkers.spec.js
@@ -6,7 +6,7 @@ describe('Client listWorkers', () => {
.then(result => {
let amountOfEmployees = Object.keys(result).length;
- expect(amountOfEmployees).toEqual(50);
+ expect(amountOfEmployees).toEqual(51);
done();
})
.catch(done.fail);
diff --git a/modules/client/back/methods/client/specs/sendSms.spec.js b/modules/client/back/methods/client/specs/sendSms.spec.js
index b299ac3c1..673a95cae 100644
--- a/modules/client/back/methods/client/specs/sendSms.spec.js
+++ b/modules/client/back/methods/client/specs/sendSms.spec.js
@@ -1,6 +1,7 @@
const app = require('vn-loopback/server/server');
-describe('client sendSms()', () => {
+// Issue #2471
+xdescribe('client sendSms()', () => {
let createdLog;
afterAll(async done => {
diff --git a/modules/client/back/methods/sms/send.spec.js b/modules/client/back/methods/sms/send.spec.js
index 612a16cf1..06288ffb5 100644
--- a/modules/client/back/methods/sms/send.spec.js
+++ b/modules/client/back/methods/sms/send.spec.js
@@ -1,7 +1,8 @@
const app = require('vn-loopback/server/server');
const soap = require('soap');
-describe('sms send()', () => {
+// Issue #2471
+xdescribe('sms send()', () => {
it('should return the expected message and status code', async() => {
const code = 200;
const smsConfig = await app.models.SmsConfig.findOne();
diff --git a/modules/client/front/address/create/index.html b/modules/client/front/address/create/index.html
index 929be2802..d6b5bcf8e 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">
-
+ where="{role: 'employee'}">
{{firstName}} {{lastName}}
@@ -40,7 +39,6 @@
rule>
@@ -55,7 +53,7 @@
-
-
{{name}}, {{province.name}}
@@ -94,34 +92,31 @@
-
{{name}} ({{country.country}})
-
+ show-field="country">
diff --git a/modules/client/front/credit-insurance/insurance/create/index.html b/modules/client/front/credit-insurance/insurance/create/index.html
index 2331cf40b..f3de7fbe7 100644
--- a/modules/client/front/credit-insurance/insurance/create/index.html
+++ b/modules/client/front/credit-insurance/insurance/create/index.html
@@ -1,15 +1,14 @@
-
+ insert-mode="true"
+ form="form">
+
+
diff --git a/modules/client/front/greuge/create/index.html b/modules/client/front/greuge/create/index.html
index 45ca9f9c8..dc53f9e2b 100644
--- a/modules/client/front/greuge/create/index.html
+++ b/modules/client/front/greuge/create/index.html
@@ -1,9 +1,9 @@
-
+ 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">