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

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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: >

View File

@ -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

View File

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

View File

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

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');
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();

View File

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