refs #5475 tests
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Alex Moreno 2023-06-21 14:17:25 +02:00
parent d6c504d3bb
commit 6ab431f8ef
17 changed files with 233 additions and 166 deletions

View File

@ -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 usesEmail = user.indexOf('@') !== -1;
const myOptions = {...(options || {})};
const where = usesEmail const where = usesEmail
? {email: user} ? {email: user}
: {name: user}; : {name: user};
@ -36,7 +38,7 @@ module.exports = Self => {
const vnUser = await Self.findOne({ const vnUser = await Self.findOne({
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'], fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
where where
}); }, myOptions);
const validCredentials = vnUser const validCredentials = vnUser
&& await vnUser.hasPassword(password); && await vnUser.hasPassword(password);
@ -44,8 +46,8 @@ module.exports = Self => {
if (validCredentials) { if (validCredentials) {
if (!vnUser.active) if (!vnUser.active)
throw new UserError('User disabled'); throw new UserError('User disabled');
await Self.sendTwoFactor(ctx, vnUser); await Self.sendTwoFactor(ctx, vnUser, myOptions);
await Self.passExpired(vnUser); await Self.passExpired(vnUser, myOptions);
if (vnUser.twoFactor) if (vnUser.twoFactor)
throw new ForbiddenError('REQUIRES_2FA'); throw new ForbiddenError('REQUIRES_2FA');
@ -54,7 +56,7 @@ module.exports = Self => {
return Self.validateLogin(user, password); return Self.validateLogin(user, password);
}; };
Self.passExpired = async vnUser => { Self.passExpired = async(vnUser, myOptions) => {
const today = Date.vnNew(); const today = Date.vnNew();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
@ -63,7 +65,7 @@ module.exports = Self => {
const changePasswordToken = await $.AccessToken.create({ const changePasswordToken = await $.AccessToken.create({
scopes: ['changePassword'], scopes: ['changePassword'],
userId: vnUser.id userId: vnUser.id
}); }, myOptions);
const err = new UserError('Pass expired', 'passExpired'); const err = new UserError('Pass expired', 'passExpired');
changePasswordToken.twoFactor = vnUser.twoFactor ? true : false; changePasswordToken.twoFactor = vnUser.twoFactor ? true : false;
err.details = {token: changePasswordToken}; 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') { if (vnUser.twoFactor === 'email') {
const $ = Self.app.models; const $ = Self.app.models;
@ -81,7 +83,7 @@ module.exports = Self => {
userFk: vnUser.id, userFk: vnUser.id,
code: code, code: code,
expires: Date.vnNow() + maxTTL expires: Date.vnNow() + maxTTL
}); }, myOptions);
const headers = ctx.req.headers; const headers = ctx.req.headers;
let platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, ''); let platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
@ -94,9 +96,10 @@ module.exports = Self => {
ip: ctx.req?.connection?.remoteAddress, ip: ctx.req?.connection?.remoteAddress,
device: platform && browser ? platform + ', ' + browser : headers['user-agent'], 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);
} }
}; };
}; };

View File

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

View File

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

View File

@ -1,41 +1,52 @@
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
describe('account validateAuth()', () => { fdescribe('VnUser validate-auth()', () => {
const developerId = 9; describe('validateAuth', () => {
it('should signin if data is correct', async() => {
it('should throw an error for a non existent code', async() => { await models.AuthCode.create({
const ctx = {req: {accessToken: {userId: developerId}}}; userFk: 9,
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,
code: '555555', code: '555555',
expires: Date.vnNow() + (60 * 1000) expires: Date.vnNow() + (60 * 1000)
}); });
await models.VnUser.validateAuth(ctx, 'developer', 'nightmare', '555555'); const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
await authCode.destroy();
} catch (e) {
error = e;
}
expect(error).toBeDefined(); expect(token.token).toBeDefined();
expect(error.statusCode).toBe(400); });
expect(error.message).toEqual('Authentication failed'); });
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');
});
}); });
}); });

View File

@ -31,19 +31,21 @@ module.exports = Self => {
} }
}); });
Self.validateAuth = async function(username, password, code) { Self.validateAuth = async(username, password, code, options) => {
await Self.validateCode(username, code); const myOptions = {...(options || {})};
await Self.validateCode(username, code, myOptions);
return Self.validateLogin(username, password); return Self.validateLogin(username, password);
}; };
Self.validateCode = async(username, code) => { Self.validateCode = async(username, code, myOptions) => {
const {AuthCode} = Self.app.models; const {AuthCode} = Self.app.models;
const authCode = await AuthCode.findOne({ const authCode = await AuthCode.findOne({
where: { where: {
code: code code: code
} }
}); }, myOptions);
const expired = authCode && Date.vnNow() > authCode.expires; const expired = authCode && Date.vnNow() > authCode.expires;
if (!authCode || expired) if (!authCode || expired)
@ -51,11 +53,11 @@ module.exports = Self => {
const user = await Self.findById(authCode.userFk, { const user = await Self.findById(authCode.userFk, {
fields: ['name', 'twoFactor'] fields: ['name', 'twoFactor']
}); }, myOptions);
if (user.name !== username) if (user.name !== username)
throw new UserError('Authentication failed'); throw new UserError('Authentication failed');
await authCode.destroy(); await authCode.destroy(myOptions);
}; };
}; };

View File

@ -2,7 +2,8 @@ INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalTyp
VALUES VALUES
('VnUser','acl','READ','ALLOW','ROLE','account'), ('VnUser','acl','READ','ALLOW','ROLE','account'),
('VnUser','getCurrentUserData','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) INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES VALUES

View File

@ -2,7 +2,7 @@ create table `salix`.`authCode`
( (
userFk int UNSIGNED not null, userFk int UNSIGNED not null,
code int not null, code int not null,
expires TIMESTAMP not null, expires bigint not null,
constraint authCode_pk constraint authCode_pk
primary key (userFk), primary key (userFk),
constraint authCode_unique constraint authCode_unique

View File

@ -77,7 +77,9 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
ORDER BY id; ORDER BY id;
INSERT INTO `account`.`account`(`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`) INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES VALUES

View File

@ -59,7 +59,7 @@ export default class Auth {
password: password || undefined password: password || undefined
}; };
return this.$http.post('Accounts/login', params).then( return this.$http.post('VnUsers/signin', params).then(
json => this.onLoginOk(json, remember)); json => this.onLoginOk(json, remember));
} }

View File

@ -36,7 +36,7 @@ export default class Controller {
if (newPassword != this.repeatPassword) if (newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`); throw new UserError(`Passwords don't match`);
if (newPassword == oldPassword) 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 = { const headers = {
Authorization: this.$state.params.id Authorization: this.$state.params.id

View File

@ -4,7 +4,7 @@ New password: Nueva contraseña
Repeat password: Repetir contraseña Repeat password: Repetir contraseña
Passwords don't match: Las contraseñas no coinciden Passwords don't match: Las contraseñas no coinciden
You must fill all the fields: Debes rellenar todos los campos 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 Verification code: Código de verificación
Password updated!: ¡Contraseña actualizada! Password updated!: ¡Contraseña actualizada!
Password requirements: > Password requirements: >

View File

@ -1,4 +1,5 @@
Validate email auth: Autenticar email Validate email auth: Autenticar email
Enter verification code: Introduce código de verificación Enter verification code: Introduce código de verificación
Code: Código 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 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

View File

@ -39,7 +39,7 @@ module.exports = Self => {
return [html, 'text/html', `filename=${fileName}.pdf"`]; 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 args = Object.assign({}, ctx.args);
const params = { const params = {
recipient: args.recipient, recipient: args.recipient,
@ -52,6 +52,6 @@ module.exports = Self => {
const email = new Email(templateName, params); const email = new Email(templateName, params);
return email.send({force: true}); return email.send({force: force});
}; };
}; };

View File

@ -1,3 +1,4 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('changePassword', { Self.remoteMethodCtx('changePassword', {
@ -34,7 +35,7 @@ module.exports = Self => {
const {VnUser} = Self.app.models; const {VnUser} = Self.app.models;
if (oldPassword == newPassword) 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); const user = await VnUser.findById(userId, {fields: ['name', 'twoFactor']}, myOptions);
if (user.twoFactor) if (user.twoFactor)

View File

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

View File

@ -1,8 +1,17 @@
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
fdescribe('account changePassword()', () => { 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', () => { describe('Without 2FA', () => {
it('should throw an error when old password is wrong', async() => { it('should throw an error when old password is wrong', async() => {
const tx = await models.Account.beginTransaction({}); const tx = await models.Account.beginTransaction({});
@ -21,6 +30,24 @@ fdescribe('account changePassword()', () => {
expect(error).toContain('Invalid current password'); 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() => { it('should change password', async() => {
const tx = await models.Account.beginTransaction({}); const tx = await models.Account.beginTransaction({});
@ -38,36 +65,27 @@ fdescribe('account changePassword()', () => {
}); });
describe('With 2FA', () => { 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() => { it('should change password when code is correct', async() => {
const tx = await models.Account.beginTransaction({}); const tx = await models.Account.beginTransaction({});
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
const options = {transaction: tx};
try { try {
const options = {transaction: tx};
await models.VnUser.updateAll( await models.VnUser.updateAll(
{id: 70}, {id: 70},
{twoFactor: 'email'} {
twoFactor: 'email',
passExpired: yesterday
}
, options); , 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); const authCode = await models.AuthCode.findOne({where: {userFk: 70}}, options);
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', authCode.code, options); await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', authCode.code, options);
await tx.rollback(); await tx.rollback();

View File

@ -10,16 +10,17 @@ module.exports = {
async send(options) { async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`; options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
console.log(process.env.NODE_ENV !== 'production' && !options.force); if (!process.env.NODE_ENV)
console.log(process.env.NODE_ENV !== 'production', !options.force); options.to = config.app.senderEmail;
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && !options.force) {
const notProductionError = {message: 'This not production, this email not sended'}; const notProductionError = {message: 'This not production, this email not sended'};
await this.mailLog(options, notProductionError); 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; let error;
return this.transporter.sendMail(options).catch(err => { return this.transporter.sendMail(options).catch(err => {
error = err; error = err;