diff --git a/CHANGELOG.md b/CHANGELOG.md
index cec86f478..76527ac83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [2328.01] - 2023-07-13
+## [2330.01] - 2023-07-27
### Added
@@ -14,6 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+## [2328.01] - 2023-07-13
+
+### Added
+- (Clientes -> Morosos) Añadida columna "es trabajador"
+- (Trabajadores -> Departamentos) Nueva sección
+- (Trabajadores -> Departamentos) Añadido listado de Trabajadores por departamento
+- (Trabajadores -> Departamentos) Añadido características de departamento e información
+
+### Changed
+
+### Fixed
+- (Trabajadores -> Departamentos) Arreglado búscador
+
+
## [2326.01] - 2023-06-29
### Added
diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js
new file mode 100644
index 000000000..73cc705de
--- /dev/null
+++ b/back/methods/vn-user/sign-in.js
@@ -0,0 +1,102 @@
+const ForbiddenError = require('vn-loopback/util/forbiddenError');
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('signIn', {
+ description: 'Login a user with username/email and password',
+ accepts: [
+ {
+ arg: 'user',
+ type: 'String',
+ description: 'The user name or email',
+ required: true
+ }, {
+ arg: 'password',
+ type: 'String',
+ description: 'The password'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/sign-in`,
+ verb: 'POST'
+ }
+ });
+
+ Self.signIn = async function(ctx, user, password, options) {
+ const myOptions = {};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const where = Self.userUses(user);
+ const vnUser = await Self.findOne({
+ fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
+ where
+ }, myOptions);
+
+ const validCredentials = vnUser
+ && await vnUser.hasPassword(password);
+
+ if (validCredentials) {
+ if (!vnUser.active)
+ throw new UserError('User disabled');
+ await Self.sendTwoFactor(ctx, vnUser, myOptions);
+ await Self.passExpired(vnUser, myOptions);
+
+ if (vnUser.twoFactor)
+ throw new ForbiddenError(null, 'REQUIRES_2FA');
+ }
+
+ return Self.validateLogin(user, password);
+ };
+
+ Self.passExpired = async(vnUser, myOptions) => {
+ const today = Date.vnNew();
+ today.setHours(0, 0, 0, 0);
+
+ if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
+ const $ = Self.app.models;
+ const changePasswordToken = await $.AccessToken.create({
+ scopes: ['changePassword'],
+ userId: vnUser.id
+ }, myOptions);
+ const err = new UserError('Pass expired', 'passExpired');
+ changePasswordToken.twoFactor = vnUser.twoFactor ? true : false;
+ err.details = {token: changePasswordToken};
+ throw err;
+ }
+ };
+
+ Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
+ if (vnUser.twoFactor === 'email') {
+ const $ = Self.app.models;
+
+ const code = String(Math.floor(Math.random() * 999999));
+ const maxTTL = ((60 * 1000) * 5); // 5 min
+ await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
+ userFk: vnUser.id,
+ code: code,
+ expires: Date.vnNow() + maxTTL
+ }, myOptions);
+
+ const headers = ctx.req.headers;
+ const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
+ const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
+ const params = {
+ args: {
+ recipientId: vnUser.id,
+ recipient: vnUser.email,
+ code: code,
+ ip: ctx.req?.connection?.remoteAddress,
+ device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
+ },
+ req: {getLocale: ctx.req.getLocale},
+ };
+
+ await Self.sendTemplate(params, 'auth-code', true);
+ }
+ };
+};
diff --git a/back/methods/vn-user/signIn.js b/back/methods/vn-user/signIn.js
deleted file mode 100644
index e52d68df5..000000000
--- a/back/methods/vn-user/signIn.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const UserError = require('vn-loopback/util/user-error');
-
-module.exports = Self => {
- Self.remoteMethod('signIn', {
- description: 'Login a user with username/email and password',
- accepts: [
- {
- arg: 'user',
- type: 'String',
- description: 'The user name or email',
- http: {source: 'form'},
- required: true
- }, {
- arg: 'password',
- type: 'String',
- description: 'The password'
- }
- ],
- returns: {
- type: 'object',
- root: true
- },
- http: {
- path: `/signIn`,
- verb: 'POST'
- }
- });
-
- Self.signIn = async function(user, password) {
- const models = Self.app.models;
- const usesEmail = user.indexOf('@') !== -1;
- let token;
-
- const userInfo = usesEmail
- ? {email: user}
- : {username: user};
- const instance = await Self.findOne({
- fields: ['username', 'password'],
- where: userInfo
- });
-
- const where = usesEmail
- ? {email: user}
- : {name: user};
- const vnUser = await Self.findOne({
- fields: ['id', 'active', 'passExpired'],
- where
- });
-
- const today = Date.vnNew();
- today.setHours(0, 0, 0, 0);
-
- const validCredentials = instance
- && await instance.hasPassword(password);
-
- if (validCredentials) {
- if (!vnUser.active)
- throw new UserError('User disabled');
-
- if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
- const changePasswordToken = await models.AccessToken.create({
- scopes: ['change-password'],
- userId: vnUser.id
- });
- const err = new UserError('Pass expired', 'passExpired');
- err.details = {token: changePasswordToken};
- throw err;
- }
-
- try {
- await models.Account.sync(instance.username, password);
- } catch (err) {
- console.warn(err);
- }
- }
-
- let loginInfo = Object.assign({password}, userInfo);
- token = await Self.login(loginInfo, 'user');
- return {token: token.id, ttl: token.ttl};
- };
-};
diff --git a/back/methods/vn-user/specs/sign-in.spec.js b/back/methods/vn-user/specs/sign-in.spec.js
new file mode 100644
index 000000000..f4cad88b9
--- /dev/null
+++ b/back/methods/vn-user/specs/sign-in.spec.js
@@ -0,0 +1,101 @@
+const {models} = require('vn-loopback/server/server');
+
+describe('VnUser Sign-in()', () => {
+ const employeeId = 1;
+ const unauthCtx = {
+ req: {
+ headers: {},
+ connection: {
+ remoteAddress: '127.0.0.1'
+ },
+ getLocale: () => 'en'
+ },
+ args: {}
+ };
+ const {VnUser, AccessToken} = models;
+ describe('when credentials are correct', () => {
+ it('should return the token', async() => {
+ let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
+ let accessToken = await AccessToken.findById(login.token);
+ let ctx = {req: {accessToken: accessToken}};
+
+ expect(login.token).toBeDefined();
+
+ await VnUser.logout(ctx.req.accessToken.id);
+ });
+
+ it('should return the token if the user doesnt exist but the client does', async() => {
+ let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
+ let accessToken = await AccessToken.findById(login.token);
+ let ctx = {req: {accessToken: accessToken}};
+
+ expect(login.token).toBeDefined();
+
+ await VnUser.logout(ctx.req.accessToken.id);
+ });
+ });
+
+ describe('when credentials are incorrect', () => {
+ it('should throw a 401 error', async() => {
+ let error;
+
+ try {
+ await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(401);
+ expect(error.code).toBe('LOGIN_FAILED');
+ });
+ });
+
+ describe('when two-factor auth is required', () => {
+ it('should throw a 403 error', async() => {
+ const employee = await VnUser.findById(employeeId);
+ const tx = await VnUser.beginTransaction({});
+
+ let error;
+ try {
+ const options = {transaction: tx};
+ await employee.updateAttribute('twoFactor', 'email', options);
+
+ await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(403);
+ expect(error.code).toBe('REQUIRES_2FA');
+ });
+ });
+
+ describe('when passExpired', () => {
+ it('should throw a passExpired error', async() => {
+ const tx = await VnUser.beginTransaction({});
+ const employee = await VnUser.findById(employeeId);
+ const yesterday = Date.vnNew();
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ let error;
+ try {
+ const options = {transaction: tx};
+ await employee.updateAttribute('passExpired', yesterday, options);
+
+ await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toBe('Pass expired');
+ });
+ });
+});
diff --git a/back/methods/vn-user/specs/signIn.spec.js b/back/methods/vn-user/specs/signIn.spec.js
deleted file mode 100644
index c3f4630c6..000000000
--- a/back/methods/vn-user/specs/signIn.spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const {models} = require('vn-loopback/server/server');
-
-describe('VnUser signIn()', () => {
- describe('when credentials are correct', () => {
- it('should return the token', async() => {
- let login = await models.VnUser.signIn('salesAssistant', 'nightmare');
- let accessToken = await models.AccessToken.findById(login.token);
- let ctx = {req: {accessToken: accessToken}};
-
- expect(login.token).toBeDefined();
-
- await models.VnUser.logout(ctx.req.accessToken.id);
- });
-
- it('should return the token if the user doesnt exist but the client does', async() => {
- let login = await models.VnUser.signIn('PetterParker', 'nightmare');
- let accessToken = await models.AccessToken.findById(login.token);
- let ctx = {req: {accessToken: accessToken}};
-
- expect(login.token).toBeDefined();
-
- await models.VnUser.logout(ctx.req.accessToken.id);
- });
- });
-
- describe('when credentials are incorrect', () => {
- it('should throw a 401 error', async() => {
- let error;
-
- try {
- await models.VnUser.signIn('IDontExist', 'TotallyWrongPassword');
- } catch (e) {
- error = e;
- }
-
- expect(error).toBeDefined();
- expect(error.statusCode).toBe(401);
- expect(error.code).toBe('LOGIN_FAILED');
- });
- });
-});
diff --git a/back/methods/vn-user/specs/validate-auth.spec.js b/back/methods/vn-user/specs/validate-auth.spec.js
new file mode 100644
index 000000000..8018bd3e1
--- /dev/null
+++ b/back/methods/vn-user/specs/validate-auth.spec.js
@@ -0,0 +1,52 @@
+const {models} = require('vn-loopback/server/server');
+
+describe('VnUser validate-auth()', () => {
+ describe('validateAuth', () => {
+ it('should signin if data is correct', async() => {
+ await models.AuthCode.create({
+ userFk: 9,
+ code: '555555',
+ expires: Date.vnNow() + (60 * 1000)
+ });
+ const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
+
+ expect(token.token).toBeDefined();
+ });
+ });
+
+ describe('validateCode', () => {
+ it('should throw an error for a non existent code', async() => {
+ let error;
+ try {
+ await models.VnUser.validateCode('developer', '123456');
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toEqual('Invalid or expired verification code');
+ });
+
+ it('should throw an error when a code doesn`t match the login username', async() => {
+ let error;
+ let authCode;
+ try {
+ authCode = await models.AuthCode.create({
+ userFk: 1,
+ code: '555555',
+ expires: Date.vnNow() + (60 * 1000)
+ });
+
+ await models.VnUser.validateCode('developer', '555555');
+ } catch (e) {
+ authCode && await authCode.destroy();
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.statusCode).toBe(400);
+ expect(error.message).toEqual('Authentication failed');
+ });
+ });
+});
diff --git a/back/methods/vn-user/validate-auth.js b/back/methods/vn-user/validate-auth.js
new file mode 100644
index 000000000..beab43417
--- /dev/null
+++ b/back/methods/vn-user/validate-auth.js
@@ -0,0 +1,66 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethod('validateAuth', {
+ description: 'Login a user with username/email and password',
+ accepts: [
+ {
+ arg: 'user',
+ type: 'String',
+ description: 'The user name or email',
+ required: true
+ },
+ {
+ arg: 'password',
+ type: 'String',
+ description: 'The password'
+ },
+ {
+ arg: 'code',
+ type: 'String',
+ description: 'The auth code'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/validate-auth`,
+ verb: 'POST'
+ }
+ });
+
+ Self.validateAuth = async(username, password, code, options) => {
+ const myOptions = {};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const token = Self.validateLogin(username, password);
+ await Self.validateCode(username, code, myOptions);
+ return token;
+ };
+
+ Self.validateCode = async(username, code, myOptions) => {
+ const {AuthCode} = Self.app.models;
+
+ const authCode = await AuthCode.findOne({
+ where: {
+ code: code
+ }
+ }, myOptions);
+
+ const expired = authCode && Date.vnNow() > authCode.expires;
+ if (!authCode || expired)
+ throw new UserError('Invalid or expired verification code');
+
+ const user = await Self.findById(authCode.userFk, {
+ fields: ['name', 'twoFactor']
+ }, myOptions);
+
+ if (user.name !== username)
+ throw new UserError('Authentication failed');
+
+ await authCode.destroy(myOptions);
+ };
+};
diff --git a/back/model-config.json b/back/model-config.json
index d945f3250..0e37bf527 100644
--- a/back/model-config.json
+++ b/back/model-config.json
@@ -1,7 +1,4 @@
{
- "AccountingType": {
- "dataSource": "vn"
- },
"AccessTokenConfig": {
"dataSource": "vn",
"options": {
@@ -10,6 +7,12 @@
}
}
},
+ "AccountingType": {
+ "dataSource": "vn"
+ },
+ "AuthCode": {
+ "dataSource": "vn"
+ },
"Bank": {
"dataSource": "vn"
},
diff --git a/back/models/auth-code.json b/back/models/auth-code.json
new file mode 100644
index 000000000..b6a89115f
--- /dev/null
+++ b/back/models/auth-code.json
@@ -0,0 +1,31 @@
+{
+ "name": "AuthCode",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "salix.authCode"
+ }
+ },
+ "properties": {
+ "userFk": {
+ "type": "number",
+ "required": true,
+ "id": true
+ },
+ "code": {
+ "type": "string",
+ "required": true
+ },
+ "expires": {
+ "type": "number",
+ "required": true
+ }
+ },
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "Account",
+ "foreignKey": "userFk"
+ }
+ }
+}
diff --git a/back/models/vn-user.js b/back/models/vn-user.js
index b58395acc..a7ce12073 100644
--- a/back/models/vn-user.js
+++ b/back/models/vn-user.js
@@ -5,11 +5,12 @@ const {Email} = require('vn-print');
module.exports = function(Self) {
vnModel(Self);
- require('../methods/vn-user/signIn')(Self);
+ require('../methods/vn-user/sign-in')(Self);
require('../methods/vn-user/acl')(Self);
require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/validate-token')(Self);
require('../methods/vn-user/privileges')(Self);
+ require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@@ -111,6 +112,18 @@ module.exports = function(Self) {
return email.send();
});
+ Self.validateLogin = async function(user, password) {
+ let loginInfo = Object.assign({password}, Self.userUses(user));
+ token = await Self.login(loginInfo, 'user');
+ return {token: token.id, ttl: token.ttl};
+ };
+
+ Self.userUses = function(user) {
+ return user.indexOf('@') !== -1
+ ? {email: user}
+ : {username: user};
+ };
+
const _setPassword = Self.prototype.setPassword;
Self.prototype.setPassword = async function(newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
@@ -143,8 +156,9 @@ module.exports = function(Self) {
}
};
- Self.sharedClass._methods.find(method => method.name == 'changePassword')
- .accessScopes = ['change-password'];
+ Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
+ Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
+ .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
diff --git a/back/models/vn-user.json b/back/models/vn-user.json
index 61e42f77a..9131c9134 100644
--- a/back/models/vn-user.json
+++ b/back/models/vn-user.json
@@ -59,7 +59,10 @@
},
"passExpired": {
"type": "date"
- }
+ },
+ "twoFactor": {
+ "type": "string"
+ }
},
"relations": {
"role": {
@@ -111,6 +114,13 @@
"principalId": "$authenticated",
"permission": "ALLOW"
},
+ {
+ "property": "validateAuth",
+ "accessType": "EXECUTE",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ },
{
"property": "privileges",
"accessType": "*",
diff --git a/db/changes/232801/00-authCode.sql b/db/changes/232801/00-authCode.sql
new file mode 100644
index 000000000..a256db43f
--- /dev/null
+++ b/db/changes/232801/00-authCode.sql
@@ -0,0 +1,13 @@
+create table `salix`.`authCode`
+(
+ userFk int UNSIGNED not null,
+ code int not null,
+ expires bigint not null,
+ constraint authCode_pk
+ primary key (userFk),
+ constraint authCode_unique
+ unique (code),
+ constraint authCode_user_id_fk
+ foreign key (userFk) references `account`.`user` (id)
+ on update cascade on delete cascade
+);
diff --git a/db/changes/232801/00-client_create.sql b/db/changes/232801/00-client_create.sql
index 0728ba05e..d21094dad 100644
--- a/db/changes/232801/00-client_create.sql
+++ b/db/changes/232801/00-client_create.sql
@@ -1,11 +1,11 @@
-DROP PROCEDURE IF EXISTS vn.clientCreate;
+DROP PROCEDURE IF EXISTS `vn`.`clientCreate`;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`client_create`(
vFirstname VARCHAR(50),
- vSurnames VARCHAR(50),
- vFi VARCHAR(9),
- vAddress TEXT,
+ vSurnames VARCHAR(50),
+ vFi VARCHAR(9),
+ vAddress TEXT,
vPostcode CHAR(5),
vCity VARCHAR(25),
vProvinceFk SMALLINT(5),
@@ -61,7 +61,7 @@ BEGIN
provinceFk = vProvinceFk,
city = vCity,
postcode = vPostcode,
- socialName = CONCAT(vSurnames, ' ', vFirstname),
+ socialName = CONCAT(vSurnames, ' ', vFirstname),
payMethodFk = vPayMethodFk,
dueDay = vDueDay,
credit = vDefaultCredit,
@@ -75,14 +75,14 @@ BEGIN
isTaxDataChecked = vIsTaxDataChecked,
hasCoreVnl = vHasCoreVnl,
isActive = TRUE;
-
+
INSERT INTO mandate (clientFk, companyFk, mandateTypeFk)
SELECT vUserFk, vCompanyFk, vMandateTypeFk
WHERE NOT EXISTS (
- SELECT id
- FROM mandate
- WHERE clientFk = vUserFk
- AND companyFk = vCompanyFk
+ SELECT id
+ FROM mandate
+ WHERE clientFk = vUserFk
+ AND companyFk = vCompanyFk
AND mandateTypeFk = vMandateTypeFk
);
END$$
diff --git a/db/changes/232801/00-client_create2.sql b/db/changes/232801/00-client_create2.sql
index 8ba4e78e5..f2e660351 100644
--- a/db/changes/232801/00-client_create2.sql
+++ b/db/changes/232801/00-client_create2.sql
@@ -1,17 +1,17 @@
-ALTER TABLE vn.clientConfig ADD defaultPayMethodFk tinyint(3) unsigned NULL;
-ALTER TABLE vn.clientConfig ADD defaultDueDay int unsigned NULL;
-ALTER TABLE vn.clientConfig ADD defaultCredit decimal(10, 2) NULL;
-ALTER TABLE vn.clientConfig ADD defaultIsTaxDataChecked tinyint(1) NULL;
-ALTER TABLE vn.clientConfig ADD defaultHasCoreVnl boolean NULL;
-ALTER TABLE vn.clientConfig ADD defaultMandateTypeFk smallint(5) NULL;
-ALTER TABLE vn.clientConfig ADD CONSTRAINT clientNewConfigPayMethod_FK FOREIGN KEY (dafaultPayMethodFk) REFERENCES vn.payMethod(id);
-ALTER TABLE vn.clientConfig ADD CONSTRAINT clientNewConfigMandateType_FK FOREIGN KEY (defaultMandateTypeFk) REFERENCES vn.mandateType(id);
+ALTER TABLE `vn`.`clientConfig` ADD defaultPayMethodFk tinyint(3) unsigned NULL;
+ALTER TABLE `vn`.`clientConfig` ADD defaultDueDay int unsigned NULL;
+ALTER TABLE `vn`.`clientConfig` ADD defaultCredit decimal(10, 2) NULL;
+ALTER TABLE `vn`.`clientConfig` ADD defaultIsTaxDataChecked tinyint(1) NULL;
+ALTER TABLE `vn`.`clientConfig` ADD defaultHasCoreVnl boolean NULL;
+ALTER TABLE `vn`.`clientConfig` ADD defaultMandateTypeFk smallint(5) NULL;
+ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigPayMethod_FK FOREIGN KEY (defaultPayMethodFk) REFERENCES vn.payMethod(id);
+ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigMandateType_FK FOREIGN KEY (defaultMandateTypeFk) REFERENCES vn.mandateType(id);
-UPDATE vn.clientConfig
+UPDATE `vn`.`clientConfig`
SET defaultPayMethodFk = 4,
defaultDueDay = 5,
defaultCredit = 300.0,
defaultIsTaxDataChecked = 1,
defaultHasCoreVnl = 1,
defaultMandateTypeFk = 2
- WHERE id = 1;
\ No newline at end of file
+ WHERE id = 1;
diff --git a/db/changes/232801/00-department.sql b/db/changes/232801/00-department.sql
new file mode 100644
index 000000000..3dcb8501d
--- /dev/null
+++ b/db/changes/232801/00-department.sql
@@ -0,0 +1,24 @@
+alter table `vn`.`department`
+ add `twoFactor` ENUM ('email') null comment 'Default user two-factor auth type';
+
+drop trigger `vn`.`department_afterUpdate`;
+
+DELIMITER $$
+$$
+create definer = root@localhost trigger `vn`.`department_afterUpdate`
+ after update
+ on department
+ for each row
+BEGIN
+ IF !(OLD.parentFk <=> NEW.parentFk) THEN
+ UPDATE vn.department_recalc SET isChanged = TRUE;
+ END IF;
+
+ IF !(OLD.twoFactor <=> NEW.twoFactor) THEN
+ UPDATE account.user u
+ JOIN vn.workerDepartment wd ON wd.workerFk = u.id
+ SET u.twoFactor = NEW.twoFactor
+ WHERE wd.departmentFk = NEW.id;
+ END IF;
+END;$$
+DELIMITER ;
diff --git a/db/changes/232801/00-user.sql b/db/changes/232801/00-user.sql
new file mode 100644
index 000000000..376b3dbb1
--- /dev/null
+++ b/db/changes/232801/00-user.sql
@@ -0,0 +1,5 @@
+alter table `account`.`user`
+ add `twoFactor` ENUM ('email') null comment 'Two-factor auth type';
+
+DELETE FROM `salix`.`ACL`
+ WHERE model = 'VnUser' AND property = 'changePassword';
diff --git a/db/changes/233001/.gitkeep b/db/changes/233001/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 80929aebc..5b4b64437 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -77,7 +77,10 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
ORDER BY id;
INSERT INTO `account`.`account`(`id`)
- SELECT id FROM `account`.`user`;
+ SELECT `u`.`id`
+ FROM `account`.`user` `u`
+ JOIN `account`.`role` `r` ON `u`.`role` = `r`.`id`
+ WHERE `r`.`name` <> 'customer';
INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES
@@ -382,6 +385,12 @@ INSERT INTO `vn`.`clientManaCache`(`clientFk`, `mana`, `dated`)
(1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(1104, -30, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH));
+INSERT INTO `vn`.`mandateType`(`id`, `name`)
+ VALUES
+ (1, 'B2B'),
+ (2, 'CORE'),
+ (3, 'LCR');
+
INSERT INTO `vn`.`clientConfig`(`id`, `riskTolerance`, `maxCreditRows`, `maxPriceIncreasingRatio`, `riskScope`, `defaultPayMethodFk`, `defaultDueDay`, `defaultCredit`, `defaultIsTaxDataChecked`, `defaultHasCoreVnl`, `defaultMandateTypeFk`)
VALUES
(1, 200, 10, 0.25, 2, 4, 5, 300.00, 1, 1, 2);
@@ -825,12 +834,6 @@ INSERT INTO `vn`.`greuge`(`id`, `clientFk`, `description`, `amount`, `shipped`,
(11, 1101, 'some heritage charges', -15.99, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 5, 1),
(12, 1101, 'some miscellaneous charges', 58.00, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 6, 1);
-INSERT INTO `vn`.`mandateType`(`id`, `name`)
- VALUES
- (1, 'B2B'),
- (2, 'CORE'),
- (3, 'LCR');
-
INSERT INTO `vn`.`mandate`(`id`, `clientFk`, `companyFk`, `code`, `created`, `mandateTypeFk`)
VALUES
(1, 1102, 442, '1-1', util.VN_CURDATE(), 2);
@@ -2849,8 +2852,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
- ('lilium', 'dev', 'http://localhost:9000/#/'),
- ('salix', 'dev', 'http://localhost:5000/#!/');
+ ('lilium', 'development', 'http://localhost:9000/#/'),
+ ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
VALUES
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index d4a8a316f..b10813488 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -894,6 +894,18 @@ export default {
extension: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(5) > section > span',
},
+ department: {
+ firstDepartment: 'vn-worker-department-index vn-card > vn-treeview vn-treeview-childs vn-treeview-childs vn-treeview-childs a'
+ },
+ departmentSummary: {
+ header: 'vn-worker-department-summary h5',
+ name: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(1) > section > span',
+ code: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(2) > section > span',
+ chat: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(3) > section > span',
+ bossDepartment: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(4) > section > span',
+ email: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(5) > section > span',
+ clientFk: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(6) > section > span',
+ },
workerBasicData: {
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]',
@@ -901,6 +913,13 @@ export default {
locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]',
saveButton: 'vn-worker-basic-data button[type=submit]'
},
+ departmentBasicData: {
+ Name: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.name"]',
+ Code: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.code"]',
+ Chat: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.chat"]',
+ Email: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.notificationEmail"]',
+ saveButton: 'vn-worker-department-basic-data button[type=submit]'
+ },
workerNotes: {
addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]',
note: 'vn-note-worker-create vn-textarea[ng-model="$ctrl.note.text"]',
diff --git a/e2e/paths/01-salix/05_changePassword.spec.js b/e2e/paths/01-salix/05_changePassword.spec.js
index 6e4cfb7f3..950f773dd 100644
--- a/e2e/paths/01-salix/05_changePassword.spec.js
+++ b/e2e/paths/01-salix/05_changePassword.spec.js
@@ -16,6 +16,7 @@ describe('ChangePassword path', async() => {
await browser.close();
});
+ const badPassword = 'badpass';
const oldPassword = 'nightmare';
const newPassword = 'newPass.1234';
describe('Bad login', async() => {
@@ -37,13 +38,22 @@ describe('ChangePassword path', async() => {
expect(message.text).toContain('Invalid current password');
// Bad attempt: password not meet requirements
+ message = await page.sendForm($.form, {
+ oldPassword: oldPassword,
+ newPassword: badPassword,
+ repeatPassword: badPassword
+ });
+
+ expect(message.text).toContain('Password does not meet requirements');
+
+ // Bad attempt: same password
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: oldPassword,
repeatPassword: oldPassword
});
- expect(message.text).toContain('Password does not meet requirements');
+ expect(message.text).toContain('You can not use the same password');
// Correct attempt: change password
message = await page.sendForm($.form, {
diff --git a/e2e/paths/03-worker/01-department/01_summary.spec.js b/e2e/paths/03-worker/01-department/01_summary.spec.js
new file mode 100644
index 000000000..e4bf8fc2d
--- /dev/null
+++ b/e2e/paths/03-worker/01-department/01_summary.spec.js
@@ -0,0 +1,29 @@
+import selectors from '../../../helpers/selectors.js';
+import getBrowser from '../../../helpers/puppeteer';
+
+describe('department summary path', () => {
+ let browser;
+ let page;
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('hr', 'worker');
+ await page.accessToSection('worker.department');
+ await page.doSearch('INFORMATICA');
+ await page.click(selectors.department.firstDepartment);
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it('should reach the employee summary section and check all properties', async() => {
+ expect(await page.waitToGetProperty(selectors.departmentSummary.header, 'innerText')).toEqual('INFORMATICA');
+ expect(await page.getProperty(selectors.departmentSummary.name, 'innerText')).toEqual('INFORMATICA');
+ expect(await page.getProperty(selectors.departmentSummary.code, 'innerText')).toEqual('it');
+ expect(await page.getProperty(selectors.departmentSummary.chat, 'innerText')).toEqual('informatica-cau');
+ expect(await page.getProperty(selectors.departmentSummary.bossDepartment, 'innerText')).toEqual('');
+ expect(await page.getProperty(selectors.departmentSummary.email, 'innerText')).toEqual('-');
+ expect(await page.getProperty(selectors.departmentSummary.clientFk, 'innerText')).toEqual('-');
+ });
+});
diff --git a/e2e/paths/03-worker/01-department/02-basicData.spec.js b/e2e/paths/03-worker/01-department/02-basicData.spec.js
new file mode 100644
index 000000000..219d1426c
--- /dev/null
+++ b/e2e/paths/03-worker/01-department/02-basicData.spec.js
@@ -0,0 +1,43 @@
+import getBrowser from '../../../helpers/puppeteer';
+import selectors from '../../../helpers/selectors.js';
+
+const $ = {
+ form: 'vn-worker-department-basic-data form',
+};
+
+describe('department summary path', () => {
+ let browser;
+ let page;
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('hr', 'worker');
+ await page.accessToSection('worker.department');
+ await page.doSearch('INFORMATICA');
+ await page.click(selectors.department.firstDepartment);
+ });
+
+ beforeEach(async() => {
+ await page.accessToSection('worker.department.card.basicData');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it(`should edit the department basic data and confirm the department data was edited`, async() => {
+ const values = {
+ Name: 'Informatica',
+ Code: 'IT',
+ Chat: 'informatica-cau',
+ Email: 'it@verdnatura.es',
+ };
+
+ await page.fillForm($.form, values);
+ const formValues = await page.fetchForm($.form, Object.keys(values));
+ const message = await page.sendForm($.form, values);
+
+ expect(message.isSuccess).toBeTrue();
+ expect(formValues).toEqual(values);
+ });
+});
diff --git a/front/core/services/auth.js b/front/core/services/auth.js
index 92ff4b061..844a5145d 100644
--- a/front/core/services/auth.js
+++ b/front/core/services/auth.js
@@ -24,7 +24,7 @@ export default class Auth {
initialize() {
let criteria = {
to: state => {
- const outLayout = ['login', 'recover-password', 'reset-password', 'change-password'];
+ const outLayout = ['login', 'recover-password', 'reset-password', 'change-password', 'validate-email'];
return !outLayout.some(ol => ol == state.name);
}
};
@@ -60,7 +60,25 @@ export default class Auth {
};
const now = new Date();
- return this.$http.post('VnUsers/signIn', params)
+ return this.$http.post('VnUsers/sign-in', params).then(
+ json => this.onLoginOk(json, now, remember));
+ }
+
+ validateCode(user, password, code, remember) {
+ if (!user) {
+ let err = new UserError('Please enter your username');
+ err.code = 'EmptyLogin';
+ return this.$q.reject(err);
+ }
+
+ let params = {
+ user: user,
+ password: password || undefined,
+ code: code
+ };
+
+ const now = new Date();
+ return this.$http.post('VnUsers/validate-auth', params)
.then(json => this.onLoginOk(json, now, remember));
}
diff --git a/front/core/services/token.js b/front/core/services/token.js
index 8f9f80e5c..c4b644a89 100644
--- a/front/core/services/token.js
+++ b/front/core/services/token.js
@@ -34,7 +34,6 @@ export default class Token {
remember
});
this.vnInterceptor.setToken(token);
-
try {
if (remember)
this.setStorage(localStorage, token, created, ttl);
diff --git a/front/salix/components/change-password/index.html b/front/salix/components/change-password/index.html
index 8d338d411..04f66976e 100644
--- a/front/salix/components/change-password/index.html
+++ b/front/salix/components/change-password/index.html
@@ -21,6 +21,14 @@
type="password"
autocomplete="false">
+