This commit is contained in:
parent
d6c504d3bb
commit
6ab431f8ef
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,14 +1,24 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('account validateAuth()', () => {
|
||||
const developerId = 9;
|
||||
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)
|
||||
});
|
||||
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() => {
|
||||
const ctx = {req: {accessToken: {userId: developerId}}};
|
||||
|
||||
let error;
|
||||
try {
|
||||
await models.VnUser.validateAuth(ctx, 'developer', 'nightmare', '123456');
|
||||
await models.VnUser.validateCode('developer', '123456');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
@ -19,18 +29,18 @@ describe('account validateAuth()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error when a code doesn`t match the login username', async() => {
|
||||
const ctx = {req: {accessToken: {userId: developerId}}};
|
||||
|
||||
let error;
|
||||
let authCode;
|
||||
try {
|
||||
const authCode = await models.AuthCode.create({
|
||||
authCode = await models.AuthCode.create({
|
||||
userFk: 1,
|
||||
code: '555555',
|
||||
expires: Date.vnNow() + (60 * 1000)
|
||||
});
|
||||
await models.VnUser.validateAuth(ctx, 'developer', 'nightmare', '555555');
|
||||
await authCode.destroy();
|
||||
|
||||
await models.VnUser.validateCode('developer', '555555');
|
||||
} catch (e) {
|
||||
authCode && await authCode.destroy();
|
||||
error = e;
|
||||
}
|
||||
|
||||
|
@ -39,3 +49,4 @@ describe('account validateAuth()', () => {
|
|||
expect(error.message).toEqual('Authentication failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: >
|
||||
|
|
|
@ -2,3 +2,4 @@ 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
|
||||
Validate: Validar
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
try {
|
||||
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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
let error;
|
||||
return this.transporter.sendMail(options).catch(err => {
|
||||
error = err;
|
||||
|
|
Loading…
Reference in New Issue