diff --git a/back/methods/vn-user/renew-token.js b/back/methods/vn-user/renew-token.js new file mode 100644 index 0000000000..1f3532bd6a --- /dev/null +++ b/back/methods/vn-user/renew-token.js @@ -0,0 +1,38 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('renewToken', { + description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it', + accepts: [], + returns: { + type: 'Object', + root: true + }, + http: { + path: `/renewToken`, + verb: 'POST' + } + }); + + Self.renewToken = async function(ctx) { + const models = Self.app.models; + const userId = ctx.req.accessToken.userId; + const created = ctx.req.accessToken.created; + const tokenId = ctx.req.accessToken.id; + + const now = new Date(); + const differenceMilliseconds = now - created; + const differenceSeconds = Math.floor(differenceMilliseconds / 1000); + + const accessTokenConfig = await models.AccessTokenConfig.findOne({fields: ['renewPeriod']}); + + if (differenceSeconds <= accessTokenConfig.renewPeriod) + throw new UserError(`The renew period has not been exceeded`); + + await Self.logout(tokenId); + const user = await Self.findById(userId); + const accessToken = await user.createAccessToken(); + + return {token: accessToken.id, created: accessToken.created}; + }; +}; diff --git a/back/methods/vn-user/signIn.js b/back/methods/vn-user/signIn.js index 6615cb5f13..c98f1da54b 100644 --- a/back/methods/vn-user/signIn.js +++ b/back/methods/vn-user/signIn.js @@ -76,6 +76,6 @@ module.exports = Self => { let loginInfo = Object.assign({password}, userInfo); token = await Self.login(loginInfo, 'user'); - return {token: token.id}; + return {token: token.id, created: token.created}; }; }; diff --git a/back/methods/vn-user/specs/signIn.js b/back/methods/vn-user/specs/signIn.spec.js similarity index 91% rename from back/methods/vn-user/specs/signIn.js rename to back/methods/vn-user/specs/signIn.spec.js index b46c645d67..c3f4630c63 100644 --- a/back/methods/vn-user/specs/signIn.js +++ b/back/methods/vn-user/specs/signIn.spec.js @@ -9,7 +9,7 @@ describe('VnUser signIn()', () => { expect(login.token).toBeDefined(); - await models.VnUser.signOut(ctx); + await models.VnUser.logout(ctx.req.accessToken.id); }); it('should return the token if the user doesnt exist but the client does', async() => { @@ -19,7 +19,7 @@ describe('VnUser signIn()', () => { expect(login.token).toBeDefined(); - await models.VnUser.signOut(ctx); + await models.VnUser.logout(ctx.req.accessToken.id); }); }); diff --git a/back/methods/vn-user/specs/signOut.js b/back/methods/vn-user/specs/signOut.js deleted file mode 100644 index c84e86f05c..0000000000 --- a/back/methods/vn-user/specs/signOut.js +++ /dev/null @@ -1,42 +0,0 @@ -const {models} = require('vn-loopback/server/server'); - -describe('VnUser signOut()', () => { - it('should logout and remove token after valid login', async() => { - let loginResponse = await models.VnUser.signOut('buyer', 'nightmare'); - let accessToken = await models.AccessToken.findById(loginResponse.token); - let ctx = {req: {accessToken: accessToken}}; - - let logoutResponse = await models.VnUser.signOut(ctx); - let tokenAfterLogout = await models.AccessToken.findById(loginResponse.token); - - expect(logoutResponse).toBeTrue(); - expect(tokenAfterLogout).toBeNull(); - }); - - it('should throw a 401 error when token is invalid', async() => { - let error; - let ctx = {req: {accessToken: {id: 'invalidToken'}}}; - - try { - response = await models.VnUser.signOut(ctx); - } catch (e) { - error = e; - } - - expect(error).toBeDefined(); - expect(error.statusCode).toBe(401); - }); - - it('should throw an error when no token is passed', async() => { - let error; - let ctx = {req: {accessToken: null}}; - - try { - response = await models.VnUser.signOut(ctx); - } catch (e) { - error = e; - } - - expect(error).toBeDefined(); - }); -}); diff --git a/back/model-config.json b/back/model-config.json index ff2bf5850b..d945f32508 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -2,6 +2,14 @@ "AccountingType": { "dataSource": "vn" }, + "AccessTokenConfig": { + "dataSource": "vn", + "options": { + "mysql": { + "table": "salix.accessTokenConfig" + } + } + }, "Bank": { "dataSource": "vn" }, diff --git a/back/models/access-token-config.json b/back/models/access-token-config.json new file mode 100644 index 0000000000..6d90a0f4d4 --- /dev/null +++ b/back/models/access-token-config.json @@ -0,0 +1,30 @@ +{ + "name": "AccessTokenConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "accessTokenConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "renewPeriod": { + "type": "number", + "required": true + }, + "renewInterval": { + "type": "number", + "required": true + } + }, + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} diff --git a/back/models/vn-user.js b/back/models/vn-user.js index fb32793539..39a0abd9ed 100644 --- a/back/models/vn-user.js +++ b/back/models/vn-user.js @@ -10,6 +10,7 @@ module.exports = function(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/renew-token')(Self); // Validations diff --git a/db/changes/232601/00-salix.sql b/db/changes/232601/00-salix.sql new file mode 100644 index 0000000000..dc1ed69be3 --- /dev/null +++ b/db/changes/232601/00-salix.sql @@ -0,0 +1,10 @@ +CREATE TABLE `salix`.`accessTokenConfig` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `renewPeriod` int(10) unsigned DEFAULT NULL, + `renewInterval` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +INSERT IGNORE INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `renewInterval`) + VALUES + (1, 21600, 300); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 3f4433fa1b..9c930222f3 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2895,6 +2895,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`) (2, 1, 50, 2), (3, 1, 0, 3); +INSERT INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `renewInterval`) + VALUES + (1, 21600, 300); + INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agencyFk`, `companyFk`) VALUES (1, 1, 1, 1, 442); diff --git a/front/core/services/auth.js b/front/core/services/auth.js index 41cd27f030..ef6c076372 100644 --- a/front/core/services/auth.js +++ b/front/core/services/auth.js @@ -64,7 +64,7 @@ export default class Auth { } onLoginOk(json, remember) { - this.vnToken.set(json.data.token, remember); + this.vnToken.set(json.data.token, json.data.created, remember); return this.loadAcls().then(() => { let continueHash = this.$state.params.continue; diff --git a/front/core/services/index.js b/front/core/services/index.js index 867a13df03..855f2fab14 100644 --- a/front/core/services/index.js +++ b/front/core/services/index.js @@ -11,3 +11,4 @@ import './report'; import './email'; import './file'; import './date'; + diff --git a/front/core/services/token.js b/front/core/services/token.js index 126fbb6048..c1bb5a1732 100644 --- a/front/core/services/token.js +++ b/front/core/services/token.js @@ -9,25 +9,33 @@ export default class Token { constructor() { try { this.token = sessionStorage.getItem('vnToken'); - if (!this.token) + this.created = sessionStorage.getItem('vnTokenCreated'); + if (!this.token) { this.token = localStorage.getItem('vnToken'); + this.created = localStorage.getItem('vnTokenCreated'); + } } catch (e) {} } - set(value, remember) { + set(token, created, remember) { this.unset(); try { - if (remember) - localStorage.setItem('vnToken', value); - else - sessionStorage.setItem('vnToken', value); + if (remember) { + localStorage.setItem('vnToken', token); + localStorage.setItem('vnTokenCreated', created); + } else { + sessionStorage.setItem('vnToken', token); + sessionStorage.setItem('vnTokenCreated', created); + } } catch (e) {} - this.token = value; + this.token = token; + this.created = created; } unset() { localStorage.removeItem('vnToken'); sessionStorage.removeItem('vnToken'); this.token = null; + this.created = null; } } diff --git a/front/salix/components/layout/index.js b/front/salix/components/layout/index.js index 48f50f4043..dc2313f4f2 100644 --- a/front/salix/components/layout/index.js +++ b/front/salix/components/layout/index.js @@ -3,13 +3,14 @@ import Component from 'core/lib/component'; import './style.scss'; export class Layout extends Component { - constructor($element, $, vnModules) { + constructor($element, $, vnModules, vnToken) { super($element, $); this.modules = vnModules.get(); } $onInit() { this.getUserData(); + this.getAccessTokenConfig(); } getUserData() { @@ -30,8 +31,42 @@ export class Layout extends Component { refresh() { window.location.reload(); } + + getAccessTokenConfig() { + this.$http.get('AccessTokenConfigs').then(json => { + const firtsResult = json.data[0]; + if (!firtsResult) return; + this.renewPeriod = firtsResult.renewPeriod; + this.renewInterval = firtsResult.renewInterval; + + const intervalMilliseconds = firtsResult.renewInterval * 1000; + this.inservalId = setInterval(this.checkTokenValidity.bind(this), intervalMilliseconds); + }); + } + + checkTokenValidity() { + const now = new Date(); + const differenceMilliseconds = now - new Date(this.vnToken.created); + const differenceSeconds = Math.floor(differenceMilliseconds / 1000); + + if (differenceSeconds > this.renewPeriod) { + this.$http.post('VnUsers/renewToken') + .then(json => { + if (json.data.token) { + let remember = true; + if (window.sessionStorage.vnToken) remember = false; + + this.vnToken.set(json.data.token, json.data.created, remember); + } + }); + } + } + + $onDestroy() { + clearInterval(this.inservalId); + } } -Layout.$inject = ['$element', '$scope', 'vnModules']; +Layout.$inject = ['$element', '$scope', 'vnModules', 'vnToken']; ngModule.vnComponent('vnLayout', { template: require('./index.html'), diff --git a/front/salix/components/layout/index.spec.js b/front/salix/components/layout/index.spec.js index 0d70c48068..8f65f32cef 100644 --- a/front/salix/components/layout/index.spec.js +++ b/front/salix/components/layout/index.spec.js @@ -37,4 +37,49 @@ describe('Component vnLayout', () => { expect(url).not.toBeDefined(); }); }); + + describe('getAccessTokenConfig()', () => { + it(`should set the renewPeriod and renewInterval properties in localStorage`, () => { + const response = [{ + renewPeriod: 100, + renewInterval: 5 + }]; + + $httpBackend.expect('GET', `AccessTokenConfigs`).respond(response); + controller.getAccessTokenConfig(); + $httpBackend.flush(); + + expect(controller.renewPeriod).toBe(100); + expect(controller.renewInterval).toBe(5); + expect(controller.inservalId).toBeDefined(); + }); + }); + + describe('checkTokenValidity()', () => { + it(`should not call renewToken and not set vnToken in the controller`, () => { + controller.renewPeriod = 100; + controller.vnToken.created = new Date(); + + controller.checkTokenValidity(); + + expect(controller.vnToken.token).toBeNull(); + }); + + it(`should call renewToken and set vnToken properties in the controller`, () => { + const response = { + token: 999, + created: new Date() + }; + controller.renewPeriod = 100; + const oneHourBefore = new Date(Date.now() - (60 * 60 * 1000)); + controller.vnToken.created = oneHourBefore; + + $httpBackend.expect('POST', `VnUsers/renewToken`).respond(response); + controller.checkTokenValidity(); + $httpBackend.flush(); + + expect(controller.vnToken.token).toBe(999); + expect(controller.vnToken.created).toEqual(response.created); + }); + }); }); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 7e2701f953..895d087dc9 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -293,5 +293,6 @@ "Pass expired": "La contraseƱa ha caducado, cambiela desde Salix", "Invalid NIF for VIES": "Invalid NIF for VIES", "Ticket does not exist": "Este ticket no existe", - "Ticket is already signed": "Este ticket ya ha sido firmado" + "Ticket is already signed": "Este ticket ya ha sido firmado", + "The renew period has not been exceeded": "El periodo de renovaciĆ³n no ha sido superado" }