diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js index f27d40e0a..eaf17a900 100644 --- a/back/methods/vn-user/sign-in.js +++ b/back/methods/vn-user/sign-in.js @@ -26,9 +26,11 @@ module.exports = Self => { } }); - Self.signin = async function(ctx, user, password) { + Self.signin = async function(ctx, user, password, options) { const usesEmail = user.indexOf('@') !== -1; + const myOptions = {...(options || {})}; + const where = usesEmail ? {email: user} : {name: user}; @@ -36,7 +38,7 @@ module.exports = Self => { const vnUser = await Self.findOne({ fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'], where - }); + }, myOptions); const validCredentials = vnUser && await vnUser.hasPassword(password); @@ -44,8 +46,8 @@ module.exports = Self => { if (validCredentials) { if (!vnUser.active) throw new UserError('User disabled'); - await Self.sendTwoFactor(ctx, vnUser); - await Self.passExpired(vnUser); + await Self.sendTwoFactor(ctx, vnUser, myOptions); + await Self.passExpired(vnUser, myOptions); if (vnUser.twoFactor) throw new ForbiddenError('REQUIRES_2FA'); @@ -54,7 +56,7 @@ module.exports = Self => { return Self.validateLogin(user, password); }; - Self.passExpired = async vnUser => { + Self.passExpired = async(vnUser, myOptions) => { const today = Date.vnNew(); today.setHours(0, 0, 0, 0); @@ -63,7 +65,7 @@ module.exports = Self => { 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}; @@ -71,7 +73,7 @@ module.exports = Self => { } }; - Self.sendTwoFactor = async(ctx, vnUser) => { + Self.sendTwoFactor = async(ctx, vnUser, myOptions) => { if (vnUser.twoFactor === 'email') { const $ = Self.app.models; @@ -81,7 +83,7 @@ module.exports = Self => { userFk: vnUser.id, code: code, expires: Date.vnNow() + maxTTL - }); + }, myOptions); const headers = ctx.req.headers; let platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, ''); @@ -94,9 +96,10 @@ module.exports = Self => { ip: ctx.req?.connection?.remoteAddress, device: platform && browser ? platform + ', ' + browser : headers['user-agent'], }, - req: {getLocale: ctx.req.getLocale} + req: {getLocale: ctx.req.getLocale}, }; - await Self.sendTemplate(params, 'auth-code'); + + await Self.sendTemplate(params, 'auth-code', true); } }; }; 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..a6e89d528 --- /dev/null +++ b/back/methods/vn-user/specs/sign-in.spec.js @@ -0,0 +1,101 @@ +const {models} = require('vn-loopback/server/server'); + +fdescribe('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.message).toBe('REQUIRES_2FA'); + + await employee.updateAttribute('twoFactor', null); + }); + }); + + describe('when passExpired', () => { + it('should throw a passExpired error', async() => { + let error; + const employee = await VnUser.findById(employeeId); + const yesterday = Date.vnNew(); + yesterday.setDate(yesterday.getDate() - 1); + + try { + await employee.updateAttribute('passExpired', yesterday); + + await VnUser.signin(unauthCtx, 'employee', 'nightmare'); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error.statusCode).toBe(400); + expect(error.message).toBe('Pass expired'); + + await employee.updateAttribute('passExpired', null); + }); + }); +}); 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 5a28a4d12..000000000 --- a/back/methods/vn-user/specs/signIn.spec.js +++ /dev/null @@ -1,74 +0,0 @@ -const {models} = require('vn-loopback/server/server'); - -describe('account login()', () => { - const employeeId = 1; - const unauthCtx = { - req: { - connection: { - remoteAddress: '127.0.0.1' - }, - getLocale: () => 'en' - }, - args: {} - }; - describe('when credentials are correct', () => { - it('should return the token', async() => { - let login = await models.VnUser.signin(unauthCtx, '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(unauthCtx, '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.Account.login(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() => { - let error; - const Account = models.Account; - - const employee = await Account.findById(employeeId); - - try { - await employee.updateAttribute('twoFactor', 'email'); - - await Account.login(unauthCtx, 'employee', 'nightmare'); - } catch (e) { - error = e; - } - - expect(error).toBeDefined(); - expect(error.statusCode).toBe(403); - expect(error.message).toBe('REQUIRES_2FA'); - - await employee.updateAttribute('twoFactor', null); - }); - }); -}); diff --git a/back/methods/vn-user/specs/validate-auth.spec.js b/back/methods/vn-user/specs/validate-auth.spec.js index 958770b4b..a58837e7b 100644 --- a/back/methods/vn-user/specs/validate-auth.spec.js +++ b/back/methods/vn-user/specs/validate-auth.spec.js @@ -1,41 +1,52 @@ const {models} = require('vn-loopback/server/server'); -describe('account validateAuth()', () => { - const developerId = 9; - - it('should throw an error for a non existent code', async() => { - const ctx = {req: {accessToken: {userId: developerId}}}; - - let error; - try { - await models.VnUser.validateAuth(ctx, 'developer', 'nightmare', '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() => { - const ctx = {req: {accessToken: {userId: developerId}}}; - - let error; - try { - const authCode = await models.AuthCode.create({ - userFk: 1, +fdescribe('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) }); - await models.VnUser.validateAuth(ctx, 'developer', 'nightmare', '555555'); - await authCode.destroy(); - } catch (e) { - error = e; - } + const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555'); - expect(error).toBeDefined(); - expect(error.statusCode).toBe(400); - expect(error.message).toEqual('Authentication failed'); + 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 index 8065932f9..0fb1cf884 100644 --- a/back/methods/vn-user/validate-auth.js +++ b/back/methods/vn-user/validate-auth.js @@ -31,19 +31,21 @@ module.exports = Self => { } }); - Self.validateAuth = async function(username, password, code) { - await Self.validateCode(username, code); + Self.validateAuth = async(username, password, code, options) => { + const myOptions = {...(options || {})}; + + await Self.validateCode(username, code, myOptions); return Self.validateLogin(username, password); }; - Self.validateCode = async(username, code) => { + 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) @@ -51,11 +53,11 @@ module.exports = Self => { const user = await Self.findById(authCode.userFk, { fields: ['name', 'twoFactor'] - }); + }, myOptions); if (user.name !== username) throw new UserError('Authentication failed'); - await authCode.destroy(); + await authCode.destroy(myOptions); }; }; diff --git a/db/changes/231801/00-userAcl.sql b/db/changes/231801/00-userAcl.sql index 3935792f9..f84a4d7c3 100644 --- a/db/changes/231801/00-userAcl.sql +++ b/db/changes/231801/00-userAcl.sql @@ -2,7 +2,8 @@ INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalTyp VALUES ('VnUser','acl','READ','ALLOW','ROLE','account'), ('VnUser','getCurrentUserData','READ','ALLOW','ROLE','account'), - ('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'); + ('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'), ('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'); + ('Account','exists','READ','ALLOW','ROLE','account'); INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) VALUES diff --git a/db/changes/232601/00-authCode.sql b/db/changes/232601/00-authCode.sql index 3a85ce58f..a256db43f 100644 --- a/db/changes/232601/00-authCode.sql +++ b/db/changes/232601/00-authCode.sql @@ -2,7 +2,7 @@ create table `salix`.`authCode` ( userFk int UNSIGNED not null, code int not null, - expires TIMESTAMP not null, + expires bigint not null, constraint authCode_pk primary key (userFk), constraint authCode_unique diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 150f12a0e..82edf896f 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -77,7 +77,9 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, ` ORDER BY id; INSERT INTO `account`.`account`(`id`) - SELECT id FROM `account`.`user`; + SELECT id + FROM `account`.`user` + WHERE role <> 2; INSERT INTO `vn`.`educationLevel` (`id`, `name`) VALUES diff --git a/front/core/services/auth.js b/front/core/services/auth.js index 40bee44ba..d3e590a74 100644 --- a/front/core/services/auth.js +++ b/front/core/services/auth.js @@ -59,7 +59,7 @@ export default class Auth { password: password || undefined }; - return this.$http.post('Accounts/login', params).then( + return this.$http.post('VnUsers/signin', params).then( json => this.onLoginOk(json, remember)); } diff --git a/front/salix/components/change-password/index.js b/front/salix/components/change-password/index.js index 0067cbe8f..80784e5d0 100644 --- a/front/salix/components/change-password/index.js +++ b/front/salix/components/change-password/index.js @@ -36,7 +36,7 @@ export default class Controller { if (newPassword != this.repeatPassword) throw new UserError(`Passwords don't match`); if (newPassword == oldPassword) - throw new UserError(`You can't use the same password`); + throw new UserError(`You can not use the same password`); const headers = { Authorization: this.$state.params.id diff --git a/front/salix/components/change-password/locale/es.yml b/front/salix/components/change-password/locale/es.yml index 147966272..5bae9f696 100644 --- a/front/salix/components/change-password/locale/es.yml +++ b/front/salix/components/change-password/locale/es.yml @@ -4,7 +4,7 @@ New password: Nueva contraseña Repeat password: Repetir contraseña Passwords don't match: Las contraseñas no coinciden You must fill all the fields: Debes rellenar todos los campos -You can't use the same password: No puedes usar la misma contraseña +You can not use the same password: No puedes usar la misma contraseña Verification code: Código de verificación Password updated!: ¡Contraseña actualizada! Password requirements: > diff --git a/front/salix/components/validate-email/locale/es.yml b/front/salix/components/validate-email/locale/es.yml index aae564330..9c7635a40 100644 --- a/front/salix/components/validate-email/locale/es.yml +++ b/front/salix/components/validate-email/locale/es.yml @@ -1,4 +1,5 @@ Validate email auth: Autenticar email Enter verification code: Introduce código de verificación Code: Código -Please enter the verification code that we have sent to your email address within 5 minutes: Por favor, introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos \ No newline at end of file +Please enter the verification code that we have sent to your email address within 5 minutes: Por favor, introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos +Validate: Validar diff --git a/loopback/common/methods/vn-model/printService.js b/loopback/common/methods/vn-model/printService.js index 807613653..12a6a67cc 100644 --- a/loopback/common/methods/vn-model/printService.js +++ b/loopback/common/methods/vn-model/printService.js @@ -39,7 +39,7 @@ module.exports = Self => { return [html, 'text/html', `filename=${fileName}.pdf"`]; }; - Self.sendTemplate = async function(ctx, templateName) { + Self.sendTemplate = async function(ctx, templateName, force) { const args = Object.assign({}, ctx.args); const params = { recipient: args.recipient, @@ -52,6 +52,6 @@ module.exports = Self => { const email = new Email(templateName, params); - return email.send({force: true}); + return email.send({force: force}); }; }; diff --git a/modules/account/back/methods/account/change-password.js b/modules/account/back/methods/account/change-password.js index 7ea38a221..be795b704 100644 --- a/modules/account/back/methods/account/change-password.js +++ b/modules/account/back/methods/account/change-password.js @@ -1,3 +1,4 @@ +const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethodCtx('changePassword', { @@ -34,7 +35,7 @@ module.exports = Self => { const {VnUser} = Self.app.models; if (oldPassword == newPassword) - throw new UserError(`You can't use the same password`); + throw new UserError(`You can not use the same password`); const user = await VnUser.findById(userId, {fields: ['name', 'twoFactor']}, myOptions); if (user.twoFactor) diff --git a/modules/account/back/methods/account/login.js b/modules/account/back/methods/account/login.js index 9ff55d8ad..5178f9caf 100644 --- a/modules/account/back/methods/account/login.js +++ b/modules/account/back/methods/account/login.js @@ -23,5 +23,5 @@ module.exports = Self => { } }); - Self.login = async(ctx, user, password) => Self.app.models.VnUser.signin(ctx, user, password); + Self.login = async(ctx, user, password, options) => Self.app.models.VnUser.signin(ctx, user, password, options); }; diff --git a/modules/account/back/methods/account/specs/change-password.spec.js b/modules/account/back/methods/account/specs/change-password.spec.js index 3c2166672..84685fac2 100644 --- a/modules/account/back/methods/account/specs/change-password.spec.js +++ b/modules/account/back/methods/account/specs/change-password.spec.js @@ -1,8 +1,17 @@ const {models} = require('vn-loopback/server/server'); fdescribe('account changePassword()', () => { - let ctx = {req: {accessToken: {userId: 70}}}; - + const ctx = {req: {accessToken: {userId: 70}}}; + const unauthCtx = { + req: { + headers: {}, + connection: { + remoteAddress: '127.0.0.1' + }, + getLocale: () => 'en' + }, + args: {} + }; describe('Without 2FA', () => { it('should throw an error when old password is wrong', async() => { const tx = await models.Account.beginTransaction({}); @@ -21,6 +30,24 @@ fdescribe('account changePassword()', () => { expect(error).toContain('Invalid current password'); }); + it('should throw an error when old and new password are the same', async() => { + const tx = await models.Account.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', null, options); + await models.Account.changePassword(ctx, 'nightmare.9999', 'nightmare.9999', null, options); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e.message; + } + + expect(error).toContain('You can not use the same password'); + }); + it('should change password', async() => { const tx = await models.Account.beginTransaction({}); @@ -38,36 +65,27 @@ fdescribe('account changePassword()', () => { }); describe('With 2FA', () => { - it('should throw an error when code is incorrect', async() => { - const tx = await models.Account.beginTransaction({}); - - let error; - try { - const options = {transaction: tx}; - await models.VnUser.updateAll( - {id: 70}, - {twoFactor: 'email'} - , options); - await models.Account.changePassword(ctx, 'wrongPassword', 'nightmare.9999', null, options); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - error = e.message; - } - - expect(error).toContain('Invalid current password'); - }); - it('should change password when code is correct', async() => { const tx = await models.Account.beginTransaction({}); + const yesterday = Date.vnNew(); + yesterday.setDate(yesterday.getDate() - 1); + const options = {transaction: tx}; try { - const options = {transaction: tx}; await models.VnUser.updateAll( {id: 70}, - {twoFactor: 'email'} + { + twoFactor: 'email', + passExpired: yesterday + } , options); - await models.VnUser.signin('trainee', 'nightmare', options); + await models.VnUser.signin(unauthCtx, 'trainee', 'nightmare', options); + } catch (e) { + if (e.message != 'Pass expired') + throw e; + } + + try { const authCode = await models.AuthCode.findOne({where: {userFk: 70}}, options); await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', authCode.code, options); await tx.rollback(); diff --git a/print/core/smtp.js b/print/core/smtp.js index 50fe55986..e22108e16 100644 --- a/print/core/smtp.js +++ b/print/core/smtp.js @@ -10,16 +10,17 @@ module.exports = { async send(options) { options.from = `${config.app.senderName} <${config.app.senderEmail}>`; - console.log(process.env.NODE_ENV !== 'production' && !options.force); - console.log(process.env.NODE_ENV !== 'production', !options.force); - if (process.env.NODE_ENV !== 'production') { + if (!process.env.NODE_ENV) + options.to = config.app.senderEmail; + + if (process.env.NODE_ENV !== 'production' && !options.force) { const notProductionError = {message: 'This not production, this email not sended'}; await this.mailLog(options, notProductionError); - if (!config.smtp.auth.user) - return Promise.resolve(true); - - options.to = config.app.senderEmail; } + + if (!config.smtp.auth.user) + return Promise.resolve(true); + let error; return this.transporter.sendMail(options).catch(err => { error = err;