2177-accountModule #373

Merged
juan merged 8 commits from 2177-userModule into dev 2020-09-21 15:58:12 +00:00
228 changed files with 6257 additions and 596 deletions

View File

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

View File

@ -26,9 +26,9 @@ module.exports = Self => {
}); });
Self.login = async function(user, password) { Self.login = async function(user, password) {
let $ = Self.app.models;
let token; let token;
let usesEmail = user.indexOf('@') !== -1; let usesEmail = user.indexOf('@') !== -1;
let User = Self.app.models.User;
let loginInfo = {password}; let loginInfo = {password};
@ -38,7 +38,7 @@ module.exports = Self => {
loginInfo.username = user; loginInfo.username = user;
try { try {
token = await User.login(loginInfo, 'user'); token = await $.User.login(loginInfo, 'user');
} catch (err) { } catch (err) {
if (err.code != 'LOGIN_FAILED' || usesEmail) if (err.code != 'LOGIN_FAILED' || usesEmail)
throw err; throw err;
@ -49,17 +49,8 @@ module.exports = Self => {
if (!instance || instance.password !== md5(password || '')) if (!instance || instance.password !== md5(password || ''))
throw err; throw err;
let where = {id: instance.id}; await $.UserAccount.sync(user, password);
let userData = { token = await $.User.login(loginInfo, 'user');
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');
} }
return {token: token.id}; return {token: token.id};

View File

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

View File

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

View File

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

View File

@ -38,6 +38,9 @@
"ImageCollectionSize": { "ImageCollectionSize": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Language": {
"dataSource": "vn"
},
"Province": { "Province": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -4,6 +4,8 @@ module.exports = Self => {
require('../methods/account/login')(Self); require('../methods/account/login')(Self);
require('../methods/account/logout')(Self); require('../methods/account/logout')(Self);
require('../methods/account/acl')(Self); require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/validate-token')(Self); require('../methods/account/validate-token')(Self);
// Validations // Validations
@ -38,17 +40,9 @@ module.exports = Self => {
Self.getCurrentUserData = async function(ctx) { Self.getCurrentUserData = async function(ctx) {
let userId = ctx.req.accessToken.userId; let userId = ctx.req.accessToken.userId;
return await Self.findById(userId, {
let account = await Self.findById(userId, {
fields: ['id', 'name', 'nickname'] fields: ['id', 'name', 'nickname']
}); });
let worker = await Self.app.models.Worker.findOne({
fields: ['id'],
where: {userFk: userId}
});
return Object.assign(account, {workerId: worker.id});
}; };
/** /**

View File

@ -24,10 +24,16 @@
"nickname": { "nickname": {
"type": "string" "type": "string"
}, },
"lang": {
"type": "string"
},
"password": { "password": {
"type": "string", "type": "string",
"required": true "required": true
}, },
"bcryptPassword": {
"type": "string"
},
"active": { "active": {
"type": "boolean" "type": "boolean"
}, },
@ -47,6 +53,11 @@
"model": "Role", "model": "Role",
"foreignKey": "roleFk" "foreignKey": "roleFk"
}, },
"roles": {
"type": "hasMany",
"model": "RoleRole",
"foreignKey": "role"
},
"emailUser": { "emailUser": {
"type": "hasOne", "type": "hasOne",
"model": "EmailUser", "model": "EmailUser",
@ -65,15 +76,13 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{
"property": "logout", "property": "logout",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{
"property": "validateToken", "property": "validateToken",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",

34
back/models/language.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
VALUES VALUES
('1', '6'); ('1', '6');
INSERT INTO `account`.`mailConfig` (`id`, `domain`)
VALUES
('1', 'verdnatura.es');
INSERT INTO `vn`.`bionicConfig` (`generalInflationCoeficient`, `minimumDensityVolumetricWeight`, `verdnaturaVolumeBox`, `itemCarryBox`) INSERT INTO `vn`.`bionicConfig` (`generalInflationCoeficient`, `minimumDensityVolumetricWeight`, `verdnaturaVolumeBox`, `itemCarryBox`)
VALUES VALUES
(1.30, 167.00, 138000, 71); (1.30, 167.00, 138000, 71);
@ -33,12 +29,16 @@ INSERT INTO `vn`.`packagingConfig`(`upperGap`)
('10'); ('10');
UPDATE `account`.`role` SET id = 100 WHERE id = 0; 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`) 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' SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en'
FROM `account`.`role` WHERE id <> 20 FROM `account`.`role` WHERE id <> 20
ORDER BY id; ORDER BY id;
INSERT INTO `account`.`account`(`id`)
SELECT id FROM `account`.`user`;
INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `userFk`, `bossFk`) INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `userFk`, `bossFk`)
SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9 SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9
FROM `vn`.`user`; FROM `vn`.`user`;
@ -68,6 +68,24 @@ INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,
(111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'), (111, 'Missing', 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en'),
(112, 'Trash', 'Trash', '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`) INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`)
VALUES VALUES
(106, 'LGN', 'David Charles', 'Haller', 106, 19, 432978106), (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), (19,'Francia', 1, 'FR', 1, 27),
(30,'Canarias', 1, 'IC', 1, 24); (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`) INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
VALUES VALUES
(1, 'Main Warehouse'), (1, 'Main Warehouse'),

View File

@ -22,6 +22,9 @@ TABLES=(
role role
roleInherit roleInherit
roleRole roleRole
userPassword
accountConfig
mailConfig
) )
dump_tables ${TABLES[@]} dump_tables ${TABLES[@]}

View File

@ -134,7 +134,7 @@ export default class CrudModel extends ModelProxy {
*/ */
save() { save() {
if (!this.isChanged) if (!this.isChanged)
return null; return this.$q.resolve();
let deletes = []; let deletes = [];
let updates = []; let updates = [];

View File

@ -1,6 +1,7 @@
<div ng-if="$ctrl.isReady"> <div ng-if="$ctrl.isReady">
<div ng-transclude></div> <div ng-transclude></div>
<vn-pagination <vn-pagination
ng-if="$ctrl.model"
model="$ctrl.model" model="$ctrl.model"
class="vn-pt-md"> class="vn-pt-md">
</vn-pagination> </vn-pagination>

View File

@ -32,7 +32,6 @@ vn-list,
vn-item, vn-item,
.vn-item { .vn-item {
@extend %clickable;
display: flex; display: flex;
align-items: center; align-items: center;
color: inherit; color: inherit;
@ -85,4 +84,6 @@ vn-item,
} }
} }
a.vn-item {
@extend %clickable;
}

View File

@ -1,5 +1,6 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Popover from '../popover'; import Popover from '../popover';
import './style.scss';
export default class Menu extends Popover { export default class Menu extends Popover {
show(parent) { show(parent) {

View File

@ -0,0 +1,7 @@
@import "./effects";
.vn-menu {
vn-item, .vn-item {
@extend %clickable;
}
}

View File

@ -107,15 +107,16 @@ export default class ModelProxy extends DataModel {
* Removes a row from the model and emits the 'rowRemove' event. * Removes a row from the model and emits the 'rowRemove' event.
* *
* @param {Number} index The row index * @param {Number} index The row index
* @return {Promise} The save request promise
*/ */
remove(index) { 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); this.proxiedData.splice(proxiedIndex, 1);
if (!item.$isNew) if (!row.$isNew)
this.removed.push(item); this.removed.push(row);
this.isChanged = true; this.isChanged = true;
if (!this.data.length) if (!this.data.length)
@ -125,7 +126,19 @@ export default class ModelProxy extends DataModel {
this.emit('dataUpdate'); this.emit('dataUpdate');
if (this.autoSave) 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));
} }
/** /**

View File

@ -1 +1 @@
Search by: Search by {{module | translate}} Search for: Search {{module}}

View File

@ -1 +1 @@
Search by: Buscar por {{module | translate}} Search for: Buscar {{module}}

View File

@ -16,6 +16,7 @@ import './style.scss';
* @property {Function} onSearch Function to call when search is submited * @property {Function} onSearch Function to call when search is submited
* @property {CrudModel} model The model used for searching * @property {CrudModel} model The model used for searching
* @property {Function} exprBuilder If defined, is used to build each non-null param expresion * @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 { export default class Searchbar extends Component {
constructor($element, $) { constructor($element, $) {
@ -23,6 +24,8 @@ export default class Searchbar extends Component {
this.searchState = '.'; this.searchState = '.';
this.placeholder = 'Search'; this.placeholder = 'Search';
this.autoState = true; this.autoState = true;
this.separateIndex = true;
this.entityState = 'card.summary';
this.deregisterCallback = this.$transitions.onSuccess( this.deregisterCallback = this.$transitions.onSuccess(
{}, transition => this.onStateChange(transition)); {}, transition => this.onStateChange(transition));
@ -33,11 +36,13 @@ export default class Searchbar extends Component {
if (!this.baseState) { if (!this.baseState) {
let stateParts = this.$state.current.name.split('.'); let stateParts = this.$state.current.name.split('.');
this.baseState = stateParts[0]; this.baseState = stateParts[0];
}
this.searchState = `${this.baseState}.index`; this.searchState = `${this.baseState}.index`;
this.placeholder = this.$t('Search by', { } else
module: this.baseState this.searchState = 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'; subState += '.index';
break; break;
default: default:
subState = 'card.summary'; subState = this.entityState;
} }
if (this.stateParams) if (this.stateParams)
@ -292,8 +297,10 @@ ngModule.vnComponent('vnSearchbar', {
panel: '@', panel: '@',
info: '@?', info: '@?',
onSearch: '&?', onSearch: '&?',
baseState: '@?',
autoState: '<?', autoState: '<?',
baseState: '@?',
entityState: '@?',
separateIndex: '<?',
stateParams: '&?', stateParams: '&?',
model: '<?', model: '<?',
exprBuilder: '&?', exprBuilder: '&?',

View File

@ -1,5 +1,6 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Component from '../../lib/component'; import Component from '../../lib/component';
import {kebabToCamel} from '../../lib/string';
import './style.scss'; import './style.scss';
let positions = ['left', 'right', 'up', 'down']; let positions = ['left', 'right', 'up', 'down'];
@ -206,8 +207,8 @@ ngModule.vnComponent('vnTooltip', {
} }
}); });
directive.$inject = ['$document', '$compile', '$templateRequest']; directive.$inject = ['$document', '$compile'];
export function directive($document, $compile, $templateRequest) { export function directive($document, $compile) {
return { return {
restrict: 'A', restrict: 'A',
link: function($scope, $element, $attrs) { link: function($scope, $element, $attrs) {

View File

@ -1,70 +1,220 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Component from '../../lib/component'; import Component from '../../lib/component';
import getModifiedData from '../../lib/modified'; import getModifiedData from '../../lib/modified';
import copyObject from '../../lib/copy';
import isEqual from '../../lib/equals'; import isEqual from '../../lib/equals';
import isFullEmpty from '../../lib/full-empty'; import isFullEmpty from '../../lib/full-empty';
import UserError from '../../lib/user-error'; import UserError from '../../lib/user-error';
import {mergeFilters} from 'vn-loopback/util/filter';
/** /**
* Component that checks for changes on a specific model property and * Component that checks for changes on a specific model property and asks the
* asks the user to save or discard it when the state changes. * user to save or discard it when the state changes.
* Also it can save the data to the server when the @url and @idField * Also it can save the data to the server when the @url and @idField properties
* properties are provided. * 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 { export default class Watcher extends Component {
constructor($element, $, $state, $stateParams, $transitions, $http, vnApp, $translate, $attrs, $q) { constructor(...args) {
super($element); super(...args);
Object.assign(this, { this.idField = 'id';
$, this.get = true;
$state, this.insertMode = false;
$stateParams,
$http,
_: $translate,
$attrs,
vnApp,
$q
});
this.state = null; this.state = null;
this.deregisterCallback = $transitions.onStart({}, this.deregisterCallback = this.$transitions.onStart({},
transition => this.callback(transition)); transition => this.callback(transition));
this.updateOriginalData(); this.snapshot();
} }
$onInit() { $onInit() {
if (this.get && this.url) let fetch = !this.insertMode
this.fetchData(); && this.get
else if (this.get && !this.url) && this.url
throw new Error('URL parameter ommitted'); && this.idValue;
}
$onChanges() { if (fetch)
if (this.data) this.fetch();
this.updateOriginalData(); else {
this.isNew = !!this.insertMode;
this.snapshot();
}
} }
$onDestroy() { $onDestroy() {
this.deregisterCallback(); 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() { get dirty() {
return this.form && this.form.$dirty || this.dataChanged(); return this.form && this.form.$dirty || this.dataChanged();
} }
dataChanged() { fetch() {
let data = this.copyInNewObject(this.data); let filter = mergeFilters({
return !isEqual(data, this.orgData); 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() { insert(data) {
let id = this.data[this.idField]; this.assign({[this.idField]: this.idValue}, data);
return this.$http.get(`${this.url}/${id}`).then( this.isNew = true;
json => { this.deleted = null;
this.data = copyObject(json.data);
this.updateOriginalData();
} }
);
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() { submit() {
try { try {
if (this.requestMethod() !== 'post') if (this.isNew)
this.isInvalid();
else
this.check(); this.check();
else this.isInvalid();
} catch (err) { } catch (err) {
return this.$q.reject(err); return this.$q.reject(err);
} }
@ -122,65 +273,42 @@ export default class Watcher extends Component {
if (this.form) if (this.form)
this.form.$setSubmitted(); this.form.$setSubmitted();
const isPost = (this.requestMethod() === 'post'); if (!this.dataChanged() && !this.isNew) {
if (!this.dataChanged() && !isPost) { this.snapshot();
this.updateOriginalData();
return this.$q.resolve(); return this.$q.resolve();
} }
let changedData = isPost let changedData = this.isNew
? this.data ? this.data
: getModifiedData(this.data, this.orgData); : getModifiedData(this.data, this.orgData);
let id = this.idField ? this.orgData[this.idField] : null;
// If watcher is associated to mgCrud // If watcher is associated to mgCrud
if (this.save && this.save.accept) { if (this.save && this.save.accept) {
if (id)
changedData[this.idField] = id;
this.save.model = changedData; this.save.model = changedData;
return this.$q((resolve, reject) => { return this.save.accept()
this.save.accept().then( .then(json => this.writeData({data: json}));
json => this.writeData({data: json}, resolve),
reject
);
});
} }
// When mgCrud is not used // When mgCrud is not used
if (id) { let req;
return this.$q((resolve, reject) => {
this.$http.patch(`${this.url}/${id}`, changedData).then(
json => this.writeData(json, resolve),
reject
);
});
}
return this.$q((resolve, reject) => { if (this.deleted)
this.$http.post(this.url, changedData).then( req = this.$http.delete(this.instanceUrl);
json => this.writeData(json, resolve), else if (this.isNew)
reject req = this.$http.post(this.url, changedData);
); else
}); req = this.$http.patch(this.instanceUrl, changedData);
}
/**
* return the request method.
*/
requestMethod() { return req.then(res => this.writeData(res));
return this.$attrs.save && this.$attrs.save.toLowerCase();
} }
/** /**
* Checks if data is ready to send. * Checks if data is ready to send.
*/ */
check() { check() {
if (this.form && this.form.$invalid) this.isInvalid();
throw new UserError('Some fields are invalid');
if (!this.dirty) if (!this.dirty)
throw new UserError('No changes to save'); throw new UserError('No changes to save');
} }
@ -220,33 +348,33 @@ export default class Watcher extends Component {
onConfirmResponse(response) { onConfirmResponse(response) {
if (response === 'accept') { if (response === 'accept') {
if (this.data) this.reset();
Object.assign(this.data, this.orgData);
this.$state.go(this.state); this.$state.go(this.state);
} else } else
this.state = null; this.state = null;
} }
writeData(json, resolve) { /**
Object.assign(this.data, json.data); * @deprecated Use reset()
this.updateOriginalData(); */
resolve(json);
}
updateOriginalData() {
this.orgData = this.copyInNewObject(this.data);
this.setPristine();
}
loadOriginalData() { loadOriginalData() {
const orgData = JSON.parse(JSON.stringify(this.orgData)); this.reset();
this.data = Object.assign(this.data, orgData);
this.setPristine();
} }
copyInNewObject(data) { /**
let newCopy = {}; * @deprecated Use snapshot()
*/
updateOriginalData() {
this.snapshot();
}
}
Watcher.$inject = ['$element', '$scope'];
function copyObject(data) {
let newCopy;
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
newCopy = {};
Object.keys(data).forEach( Object.keys(data).forEach(
key => { key => {
let value = data[key]; let value = data[key];
@ -254,28 +382,48 @@ export default class Watcher extends Component {
newCopy[key] = new Date(value.getTime()); newCopy[key] = new Date(value.getTime());
else if (!isFullEmpty(value)) { else if (!isFullEmpty(value)) {
if (typeof value === 'object') if (typeof value === 'object')
newCopy[key] = this.copyInNewObject(value); newCopy[key] = copyObject(value);
else else
newCopy[key] = value; newCopy[key] = value;
} }
} }
); );
} } else
newCopy = data;
return newCopy; 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);
} }
Watcher.$inject = ['$element', '$scope', '$state', '$stateParams', '$transitions', '$http', 'vnApp', '$translate', '$attrs', '$q'];
ngModule.vnComponent('vnWatcher', { ngModule.vnComponent('vnWatcher', {
template: require('./watcher.html'), template: require('./watcher.html'),
bindings: { bindings: {
url: '@?', url: '@?',
idField: '@?', idField: '@?',
data: '<', idValue: '<?',
data: '=',
form: '<', form: '<',
save: '<', save: '<?',
get: '<?' get: '<?',
insertMode: '<?',
autoFill: '<?',
filter: '<?',
fields: '<?',
where: '<?',
include: '<?'
}, },
controller: Watcher controller: Watcher
}); });

View File

@ -1,5 +1,4 @@
import './watcher.js'; import './watcher.js';
import getModifiedData from '../../lib/modified';
describe('Component vnWatcher', () => { describe('Component vnWatcher', () => {
let $scope; let $scope;
@ -9,10 +8,16 @@ describe('Component vnWatcher', () => {
let controller; let controller;
let $attrs; let $attrs;
let $q; let $q;
let data;
beforeEach(ngModule('vnCore')); beforeEach(ngModule('vnCore'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$state_, _$q_) => { beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$state_, _$q_) => {
data = {
id: 1,
foo: 'bar'
};
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$element = angular.element('<div></div>'); $element = angular.element('<div></div>');
$state = _$state_; $state = _$state_;
@ -25,38 +30,49 @@ describe('Component vnWatcher', () => {
})); }));
describe('$onInit()', () => { describe('$onInit()', () => {
it('should call fetchData() if controllers get and url properties are defined', () => { it('should set data empty by default', () => {
controller.get = () => {};
controller.url = 'test.com';
jest.spyOn(controller, 'fetchData').mockReturnThis();
controller.$onInit(); controller.$onInit();
expect(controller.fetchData).toHaveBeenCalledWith(); expect(controller.data).toBeUndefined();
}); });
it(`should throw an error if $onInit is called without url defined`, () => { it('should set new data when insert mode is enabled', () => {
controller.get = () => {}; controller.insertMode = true;
controller.data = data;
expect(function() {
controller.$onInit(); controller.$onInit();
}).toThrowError(/parameter/);
}); expect(controller.orgData).toEqual(data);
expect(controller.orgData).toEqual(data);
expect(controller.isNew).toBeTruthy();
}); });
describe('fetchData()', () => { it('should call backend and fetch data if url and idValue properties are defined', () => {
it(`should perform a query then store the received data into controller.data and call updateOriginalData()`, () => { controller.url = 'Foos';
jest.spyOn(controller, 'updateOriginalData'); controller.idValue = 1;
let json = {data: 'some data'};
controller.data = [1]; $httpBackend.expectGET('Foos/1').respond(data);
controller.idField = 0; controller.$onInit();
controller.url = 'test.com';
$httpBackend.whenGET('test.com/1').respond(json);
$httpBackend.expectGET('test.com/1');
controller.fetchData();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.data).toEqual({data: 'some data'}); expect(controller.orgData).toEqual(data);
expect(controller.updateOriginalData).toHaveBeenCalledWith(); expect(controller.orgData).toEqual(data);
expect(controller.orgData).not.toBe(controller.data);
});
});
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.orgData).toEqual(data);
expect(controller.orgData).toEqual(data);
expect(controller.orgData).not.toBe(controller.data);
}); });
}); });
@ -95,14 +111,6 @@ describe('Component vnWatcher', () => {
controller.check(); controller.check();
}).toThrowError(); }).toThrowError();
}); });
it(`should throw error if controller.dirty is true`, () => {
controller.form = {$invalid: true};
expect(function() {
controller.check();
}).toThrowError();
});
}); });
describe('realSubmit()', () => { describe('realSubmit()', () => {
@ -130,59 +138,60 @@ describe('Component vnWatcher', () => {
}); });
}); });
describe('when id is defined', () => { describe('should perform a PATCH query and save the data', () => {
it(`should perform a query then call controller.writeData()`, done => { it(`should perform a query then call controller.writeData()`, () => {
controller.dataChanged = () => { controller.url = 'Foos';
return true; controller.data = data;
};
controller.data = {id: 2}; const changedData = {baz: 'value'};
controller.orgData = {id: 1}; Object.assign(controller.data, changedData);
let changedData = getModifiedData(controller.data, controller.orgData);
controller.idField = 'id'; $httpBackend.expectPATCH('Foos/1', changedData).respond({newProp: 'some'});
controller.url = 'test.com'; controller.realSubmit();
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);
$httpBackend.flush(); $httpBackend.flush();
expect(controller.data).toEqual(Object.assign({}, data, changedData));
}); });
}); });
it(`should perform a POST query then call controller.writeData()`, done => { it(`should perform a POST query and save the data`, () => {
controller.dataChanged = () => { controller.insertMode = true;
return true; controller.url = 'Foos';
}; controller.data = data;
controller.data = {id: 2};
controller.orgData = {id: 1}; const changedData = {baz: 'value'};
controller.url = 'test.com'; Object.assign(controller.data, changedData);
let json = {data: 'some data'};
jest.spyOn(controller, 'writeData'); $httpBackend.expectPOST('Foos', controller.data).respond({newProp: 'some'});
$httpBackend.whenPOST(`${controller.url}`, controller.data).respond(json); controller.realSubmit();
$httpBackend.expectPOST(`${controller.url}`, controller.data);
controller.realSubmit()
.then(() => {
expect(controller.writeData).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function));
done();
}).catch(done.fail);
$httpBackend.flush(); $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()', () => { describe('writeData()', () => {
it(`should call Object.asssign() function over controllers.data with json.data, then call updateOriginalData function and finally call resolve() function`, () => { it(`should save data into orgData`, () => {
jest.spyOn(controller, 'updateOriginalData'); controller.data = data;
controller.data = {}; Object.assign(controller.data, {baz: 'value'});
let json = {data: 'some data'};
let resolve = jasmine.createSpy('resolve');
controller.writeData(json, resolve);
expect(controller.updateOriginalData).toHaveBeenCalledWith(); controller.writeData({});
expect(resolve).toHaveBeenCalledWith(json);
expect(controller.data).toEqual(controller.orgData);
}); });
}); });
@ -224,37 +233,38 @@ describe('Component vnWatcher', () => {
describe(`onConfirmResponse()`, () => { describe(`onConfirmResponse()`, () => {
describe(`when response is accept`, () => { describe(`when response is accept`, () => {
it(`should call Object.assing on controlle.data with controller.orgData then call go() on state`, () => { it(`should reset data them go to state`, () => {
let response = 'accept'; let data = {key: 'value'};
controller.data = {}; controller.data = data;
controller.orgData = {name: 'Batman'}; data.foo = 'bar';
controller.$state = {go: jasmine.createSpy('go')}; controller.$state = {go: jasmine.createSpy('go')};
controller.state = 'Batman'; controller.state = 'foo.bar';
controller.onConfirmResponse(response); controller.onConfirmResponse('accept');
expect(controller.data).toEqual(controller.orgData); expect(controller.data).toEqual({key: 'value'});
expect(controller.$state.go).toHaveBeenCalledWith(controller.state); expect(controller.$state.go).toHaveBeenCalledWith(controller.state);
}); });
}); });
describe(`when response is not accept`, () => { describe(`when response is not accept`, () => {
it(`should set controller.state to null`, () => { it(`should set controller.state to null`, () => {
let response = 'anything but accept';
controller.state = 'Batman'; controller.state = 'Batman';
controller.onConfirmResponse(response); controller.onConfirmResponse('cancel');
expect(controller.state).toBeFalsy(); expect(controller.state).toBeFalsy();
}); });
}); });
}); });
describe(`loadOriginalData()`, () => { describe(`reset()`, () => {
it(`should iterate over the current data object, delete all properties then assign the ones from original data`, () => { it(`should reset data as it was before changing it`, () => {
controller.data = {name: 'Bruce'}; let data = {key: 'value'};
controller.orgData = {name: 'Batman'};
controller.loadOriginalData();
expect(controller.data).toEqual(controller.orgData); controller.data = data;
data.foo = 'bar';
controller.reset();
expect(data).toEqual({key: 'value'});
}); });
}); });
}); });

View File

@ -65,7 +65,7 @@ export function directive($translate, $window) {
}; };
if (form) if (form)
$scope.$watch(form.$submitted, refreshError); $scope.$watch(() => form.$submitted, refreshError);
} }
} }
ngModule.directive('rule', directive); ngModule.directive('rule', directive);

View File

@ -25,4 +25,10 @@
%active { %active {
background-color: $color-active; background-color: $color-active;
color: $color-active-font; color: $color-active-font;
&:hover,
&:focus {
background-color: $color-active;
color: $color-active-font;
}
} }

View File

@ -18,5 +18,6 @@ export default function moduleImport(moduleName) {
case 'invoiceOut' : return import('invoiceOut/front'); case 'invoiceOut' : return import('invoiceOut/front');
case 'route' : return import('route/front'); case 'route' : return import('route/front');
case 'entry' : return import('entry/front'); case 'entry' : return import('entry/front');
case 'account' : return import('account/front');
} }
} }

View File

@ -8,13 +8,13 @@
<div class="header"> <div class="header">
<a <a
translate-attr="{title: 'Go to module index'}" translate-attr="{title: 'Go to module index'}"
ui-sref="{{::$ctrl.module}}.index" ui-sref="{{::$ctrl.indexState}}"
name="goToModuleIndex"> name="goToModuleIndex">
<vn-icon icon="{{$ctrl.moduleMap[$ctrl.module].icon}}"></vn-icon> <vn-icon icon="{{$ctrl.moduleMap[$ctrl.module].icon}}"></vn-icon>
</a> </a>
<a <a
translate-attr="{title: 'Preview'}" translate-attr="{title: 'Preview'}"
ui-sref="{{::$ctrl.module}}.card.summary({id: $ctrl.descriptor.id})"> ui-sref="{{::$ctrl.summaryState}}({id: $ctrl.descriptor.id})">
<vn-icon icon="desktop_windows"></vn-icon> <vn-icon icon="desktop_windows"></vn-icon>
</a> </a>
<vn-icon-button <vn-icon-button

View File

@ -15,6 +15,8 @@ export default class Descriptor extends Component {
} }
$postLink() { $postLink() {
super.$postLink();
const content = this.element.querySelector('vn-descriptor-content'); const content = this.element.querySelector('vn-descriptor-content');
if (!content) throw new Error('Directive vnDescriptorContent not found'); if (!content) throw new Error('Directive vnDescriptorContent not found');
@ -104,6 +106,14 @@ export class DescriptorContent {
this.$transclude = $transclude; this.$transclude = $transclude;
this.moduleMap = vnModules.getMap(); this.moduleMap = vnModules.getMap();
} }
get summaryState() {
return `${this.baseState || this.module}.card.summary`;
}
get indexState() {
return this.baseState || `${this.module}.index`;
}
} }
DescriptorContent.$inject = ['$transclude', 'vnModules']; DescriptorContent.$inject = ['$transclude', 'vnModules'];
@ -112,6 +122,7 @@ ngModule.vnComponent('vnDescriptorContent', {
controller: DescriptorContent, controller: DescriptorContent,
bindings: { bindings: {
module: '@', module: '@',
baseState: '@?',
description: '<', description: '<',
descriptor: '<?' descriptor: '<?'
}, },

View File

@ -15,7 +15,7 @@ export class Layout extends Component {
getUserData() { getUserData() {
this.$http.get('Accounts/getCurrentUserData').then(json => { this.$http.get('Accounts/getCurrentUserData').then(json => {
this.$.$root.user = json.data; this.$.$root.user = json.data;
window.localStorage.currentUserWorkerId = json.data.workerId; window.localStorage.currentUserWorkerId = json.data.id;
}); });
} }
} }

View File

@ -1,6 +1,6 @@
<ul class="vn-list" ng-if="::$ctrl.items.length > 0"> <ul class="vn-list" ng-if="::$ctrl.items.length > 0">
<li ng-repeat="item in ::$ctrl.items" name="{{::item.description}}"> <li ng-repeat="item in ::$ctrl.items" name="{{::item.description}}">
<a ng-if="!item.external" <a ng-if="::!item.external"
ui-sref="{{::item.state}}" ui-sref="{{::item.state}}"
class="vn-item" class="vn-item"
ng-class="{active: item.active && !item.childs, expanded: item.active}" ng-class="{active: item.active && !item.childs, expanded: item.active}"
@ -15,7 +15,7 @@
<vn-icon icon="keyboard_arrow_down" ng-if="::item.childs.length > 0"></vn-icon> <vn-icon icon="keyboard_arrow_down" ng-if="::item.childs.length > 0"></vn-icon>
</vn-item-section> </vn-item-section>
</a> </a>
<a ng-if="item.external" <a ng-if="::item.external"
href="{{::item.url}}" href="{{::item.url}}"
class="vn-item"> class="vn-item">
<vn-item-section avatar> <vn-item-section avatar>

View File

@ -32,72 +32,85 @@ export default class LeftMenu {
let moduleIndex = this.$state.current.data.moduleIndex; let moduleIndex = this.$state.current.data.moduleIndex;
let moduleFile = window.routes[moduleIndex] || []; let moduleFile = window.routes[moduleIndex] || [];
let menu = moduleFile.menus && moduleFile.menus[this.source] || []; let menu = moduleFile.menus && moduleFile.menus[this.source] || [];
let items = [];
let addItem = (items, item) => { let cloneItems = (items, parent) => {
let myItems = [];
for (let item of items) {
let state = states[item.state]; let state = states[item.state];
if (state) { if (state) {
state = state.self; state = state.self;
let acl = state.data.acl; let acl = state.data.acl;
if (acl && !this.aclService.hasAny(acl)) if (acl && !this.aclService.hasAny(acl))
return; continue;
} else if (!item.external) {
console.warn('wrong left-menu definition');
return;
} }
items.push({ let myItem = {
icon: item.icon, icon: item.icon,
description: item.description || state.description, description: item.description || state.description,
state: item.state, state: item.state,
external: item.external, external: item.external,
url: item.url url: item.url,
}); parent
}; };
for (let item of menu) { if (item.childs) {
if (item.state || item.external) let myChilds = cloneItems(item.childs, myItem);
addItem(items, item);
else {
let childs = [];
for (let child of item.childs) if (myChilds.length > 0) {
addItem(childs, child); myItem.childs = myChilds;
myItems.push(myItem);
if (childs.length > 0) {
items.push({
icon: item.icon,
description: item.description,
childs: childs
});
}
} }
} else
myItems.push(myItem);
} }
return items; return myItems;
};
return cloneItems(menu);
} }
activateItem() { activateItem() {
let myState = this.$state.current.name if (!this.items) return;
.split('.') let currentState = this.$state.current.name;
.slice(0, this._depth) let maxSpecificity = 0;
.join('.'); let selectedItem;
let re = new RegExp(`^${myState}(\\..*)?$`);
function isParentState(state, currentState) {
if (!state) return 0;
let match = state.match(/^(.*)\.index$/);
if (match) state = match[1];
let isParent =
currentState.startsWith(`${state}.`) ||
currentState === state;
return isParent
? (state.match(/\./g) || []).length + 1
: 0;
}
function selectItem(items) {
if (!items) return;
for (let item of items) {
item.active = false;
let specificity = isParentState(item.state, currentState);
if (specificity > maxSpecificity) {
selectedItem = item;
maxSpecificity = specificity;
}
selectItem(item.childs);
}
}
if (this.items) {
// Check items matching current path // Check items matching current path
for (let item of this.items) { selectItem(this.items);
item.active = re.test(item.state);
if (item.childs) { while (selectedItem) {
for (let child of item.childs) { selectedItem.active = true;
child.active = re.test(child.state); selectedItem = selectedItem.parent;
if (child.active)
item.active = child.active;
}
}
}
} }
} }

View File

@ -36,7 +36,7 @@
</vn-icon-button> </vn-icon-button>
</div> </div>
<a <a
ui-sref="worker.card.summary({id: $root.user.workerId})" ui-sref="worker.card.summary({id: $root.user.id})"
class="vn-button colored" class="vn-button colored"
translate> translate>
My account My account

View File

@ -44,6 +44,7 @@ Routes: Rutas
Locator: Localizador Locator: Localizador
Invoices out: Facturas emitidas Invoices out: Facturas emitidas
Entries: Entradas Entries: Entradas
Users: Usuarios
# Common # Common

View File

@ -134,5 +134,14 @@
"This ticket is deleted": "Este ticket está eliminado", "This ticket is deleted": "Este ticket está eliminado",
"A travel with this data already exists": "Ya existe un travel con estos datos", "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", "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"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
{
"name": "MailConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.mailConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"domain": {
"type": "string",
"required": true
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
module.exports = Self => {
require('../methods/user-account/sync')(Self);
require('../methods/user-account/sync-by-id')(Self);
};

View File

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

View File

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

View File

@ -0,0 +1,15 @@
{
"name": "UserSync",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.userSync"
}
},
"properties": {
"name": {
"type": "string",
"id": true
}
}
}

View File

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

View File

@ -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<String>} methods Array of method names to promisify
*/
function promisifyObject(obj, methods) {
for (let method of methods) {
let orgMethod = obj[method];
obj[method] = promisify(orgMethod);
}
}

View File

@ -0,0 +1,65 @@
<vn-watcher
vn-id="watcher"
url="ACLs"
data="$ctrl.acl"
id-value="$ctrl.$params.id"
insert-mode="!$ctrl.$params.id"
form="form">
</vn-watcher>
<form
name="form"
vn-http-submit="watcher.submitGo('account.acl')"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
label="Role"
ng-model="$ctrl.acl.principalId"
url="Roles"
id-field="name"
value-field="name"
vn-focus>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
label="Model"
ng-model="$ctrl.acl.model"
data="$ctrl.models"
id-field="name"
value-field="name">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Property"
ng-model="$ctrl.acl.property"
info="Use * to match all properties">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
label="Access type"
ng-model="$ctrl.acl.accessType"
data="$ctrl.accessTypes"
id-field="name"
value-field="name">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
label="Permission"
ng-model="$ctrl.acl.permission"
data="$ctrl.permissions"
id-field="name"
value-field="name">
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-submit
icon="check"
vn-tooltip="Create"
class="round"
fixed-bottom-right>
</vn-submit>
</form>

View File

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

View File

@ -0,0 +1,4 @@
import './main';
import './index/';
import './create';
import './search-panel';

View File

@ -0,0 +1,51 @@
<vn-auto-search
model="model">
</vn-auto-search>
<vn-data-viewer
model="model"
class="vn-w-sm">
<vn-card>
<vn-list class="separated">
<a
ng-repeat="row in model.data track by row.id"
ui-sref="account.acl.edit(::{id: row.id})"
translate-attr="{title: 'Edit ACL'}"
class="vn-item search-result">
<vn-item-section>
<h6>{{::row.model}}.{{::row.property}}</h6>
<vn-label-value
label="Role"
value="{{::row.principalId}}">
</vn-label-value>
<vn-label-value
label="Access type"
value="{{::row.accessType}}">
</vn-label-value>
<vn-label-value
label="Permission"
value="{{::row.permission}}">
</vn-label-value>
</vn-item-section>
<vn-item-section side>
<vn-icon-button
vn-click-stop="deleteAcl.show(row)"
vn-tooltip="Delete"
icon="delete">
</vn-icon-button>
</vn-item-section>
</a>
</vn-list>
</vn-card>
</vn-data-viewer>
<a ui-sref="account.acl.create"
vn-tooltip="New ACL"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
vn-id="deleteAcl"
on-accept="$ctrl.onDelete($data)"
question="Are you sure you want to continue?"
message="ACL will be removed">
</vn-confirm>

Some files were not shown because too many files have changed in this diff Show More