diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js index 0a0133b826..e4cb00623b 100644 --- a/back/methods/vn-user/sign-in.js +++ b/back/methods/vn-user/sign-in.js @@ -1,4 +1,5 @@ const ForbiddenError = require('vn-loopback/util/forbiddenError'); +const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethodCtx('signin', { @@ -26,7 +27,6 @@ module.exports = Self => { }); Self.signin = async function(ctx, user, password) { - const $ = Self.app.models; const usesEmail = user.indexOf('@') !== -1; const where = usesEmail @@ -34,30 +34,70 @@ module.exports = Self => { : {name: user}; const vnUser = await Self.findOne({ - fields: ['id', 'active', 'email', 'password', 'twoFactor'], + fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'], where }); - if (vnUser && vnUser.twoFactor === 'email') { + const validCredentials = vnUser + && await vnUser.hasPassword(password); + + if (validCredentials) { + if (!vnUser.active) + throw new UserError('User disabled'); + await Self.sendTwoFactor(ctx, vnUser); + await Self.passExpired(vnUser); + + if (vnUser.twoFactor) + throw new ForbiddenError('REQUIRES_2FA'); + } + + return Self.validateLogin(user, password); + }; + + Self.passExpired = async vnUser => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) { + const $ = Self.app.models; + const changePasswordToken = await $.AccessToken.create({ + scopes: ['change-password'], + userId: vnUser.id + }); + throw new UserError('Pass expired', 'passExpired', { + id: vnUser.id, + token: changePasswordToken.id, + twoFactor: vnUser.twoFactor ? true : false + }); + } + }; + + Self.sendTwoFactor = async(ctx, vnUser) => { + if (vnUser.twoFactor === 'email') { + const $ = Self.app.models; + const code = String(Math.floor(Math.random() * 999999)); const maxTTL = ((60 * 1000) * 5); // 5 min await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, { userFk: vnUser.id, code: code, - expires: Date.now() + maxTTL + expires: Date.vnNow() + maxTTL }); + const headers = ctx.req.headers; + let platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, ''); + let browser = headers['sec-ch-ua']?.replace(/['"=]+/g, ''); const params = { - recipientId: vnUser.id, - recipient: vnUser.email, - code: code + args: { + recipientId: vnUser.id, + recipient: vnUser.email, + code: code, + ip: ctx.req?.connection?.remoteAddress, + device: platform && browser ? platform + ', ' + browser : headers['user-agent'], + }, + req: {getLocale: ctx.req.getLocale} }; - ctx.args = {...ctx.args, ...params}; - await Self.sendTemplate(ctx, 'auth-code'); - - throw new ForbiddenError('REQUIRES_2FA'); + await Self.sendTemplate(params, 'auth-code'); } - - return Self.validateLogin(user, password); }; }; diff --git a/back/methods/vn-user/validate-auth.js b/back/methods/vn-user/validate-auth.js index 312f1347af..cc78e983e4 100644 --- a/back/methods/vn-user/validate-auth.js +++ b/back/methods/vn-user/validate-auth.js @@ -1,7 +1,7 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { - Self.remoteMethodCtx('validateAuth', { + Self.remoteMethod('validateAuth', { description: 'Login a user with username/email and password', accepts: [ { @@ -31,8 +31,13 @@ module.exports = Self => { } }); - Self.validateAuth = async function(ctx, username, password, code) { - const {AuthCode, UserAccess} = Self.app.models; + Self.validateAuth = async function(username, password, code) { + await Self.validateCode(code); + return Self.validateLogin(username, password); + }; + + Self.validateCode = async(username, code) => { + const {AuthCode} = Self.app.models; const authCode = await AuthCode.findOne({ where: { @@ -47,27 +52,10 @@ module.exports = Self => { const user = await Self.findById(authCode.userFk, { fields: ['name', 'twoFactor'] }); - + console.log(username, code); if (user.name !== username) throw new UserError('Authentication failed'); - const headers = ctx.req.headers; - let platform = headers['sec-ch-ua-platform']; - let browser = headers['sec-ch-ua']; - - if (platform) platform = platform.replace(/['"]+/g, ''); - if (browser) browser = browser.split(';')[0].replace(/['"]+/g, ''); - - await UserAccess.upsertWithWhere({userFk: authCode.userFk}, { - userFk: authCode.userFk, - ip: ctx.req.connection.remoteAddress, - agent: headers['user-agent'], - platform: platform, - browser: browser - }); - await authCode.destroy(); - - return Self.validateLogin(username, password); }; }; diff --git a/back/model-config.json b/back/model-config.json index 504107f895..fd8ff2f209 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -116,9 +116,6 @@ "Town": { "dataSource": "vn" }, - "UserAccess": { - "dataSource": "vn" - }, "Url": { "dataSource": "vn" }, diff --git a/back/models/user-access.json b/back/models/user-access.json deleted file mode 100644 index f60d702518..0000000000 --- a/back/models/user-access.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "UserAccess", - "base": "VnModel", - "options": { - "mysql": { - "table": "salix.userAccess" - } - }, - "properties": { - "userFk": { - "type": "number", - "required": true, - "id": true - }, - "ip": { - "type": "string", - "required": true - }, - "agent": { - "type": "string" - }, - "platform": { - "type": "string" - }, - "browser": { - "type": "string" - } - }, - "relations": { - "user": { - "type": "belongsTo", - "model": "Account", - "foreignKey": "userFk" - } - } -} diff --git a/back/models/vn-user.js b/back/models/vn-user.js index 139ac3320c..75490ccf84 100644 --- a/back/models/vn-user.js +++ b/back/models/vn-user.js @@ -1,6 +1,7 @@ const vnModel = require('vn-loopback/common/models/vn-model'); const LoopBackContext = require('loopback-context'); const {Email} = require('vn-print'); +const UserError = require('vn-loopback/util/user-error'); module.exports = function(Self) { vnModel(Self); @@ -110,53 +111,11 @@ module.exports = function(Self) { }); Self.validateLogin = async function(user, password) { - const models = Self.app.models; const usesEmail = user.indexOf('@') !== -1; - let token; const userInfo = usesEmail ? {email: user} : {username: user}; - const instance = await Self.findOne({ - fields: ['username', 'password'], - where: userInfo - }); - - const where = usesEmail - ? {email: user} - : {name: user}; - const vnUser = await Self.findOne({ - fields: ['id', 'active', 'passExpired'], - where - }); - - const today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - - const validCredentials = instance - && await instance.hasPassword(password); - - if (validCredentials) { - if (!vnUser.active) - throw new UserError('User disabled'); - - if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) { - const changePasswordToken = await models.AccessToken.create({ - scopes: ['change-password'], - userId: vnUser.id - }); - throw new UserError('Pass expired', 'passExpired', { - id: vnUser.id, - token: changePasswordToken.id - }); - } - - try { - await models.Account.sync(instance.username, password); - } catch (err) { - console.warn(err); - } - } let loginInfo = Object.assign({password}, userInfo); token = await Self.login(loginInfo, 'user'); @@ -197,6 +156,50 @@ module.exports = function(Self) { Self.sharedClass._methods.find(method => method.name == 'changePassword') .accessScopes = ['change-password']; + Self.sharedClass._methods.find(method => method.name == 'changePassword').accepts.splice(3, 0, { + arg: 'verificationCode', + type: 'string' + }); + const _changePassword = Self.changePassword; + Self.changePassword = async(userId, oldPassword, newPassword, verificationCode, options, cb) => { + if (oldPassword == newPassword) + throw new UserError(`You can't use the same password`); + + const user = await this.findById(userId, {fields: ['name', 'twoFactor']}); + if (user.twoFactor) + await Self.validateCode(user.name, verificationCode); + + await _changePassword.call(this, userId, oldPassword, newPassword, options, cb); + }; + + const _prototypeChangePassword = Self.prototype.ChangePassword; + Self.prototype.changePassword = async function(oldPassword, newPassword, options, cb) { + if (cb === undefined && typeof options === 'function') { + cb = options; + options = undefined; + } + + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + options = myOptions; + + try { + await _prototypeChangePassword.call(this, oldPassword, newPassword, options); + tx && await tx.commit(); + cb && cb(); + } catch (err) { + tx && await tx.rollback(); + if (cb) cb(err); else throw err; + } + }; // FIXME: https://redmine.verdnatura.es/issues/5761 // Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { diff --git a/db/changes/232001/00-authCode.sql b/db/changes/232601/00-authCode.sql similarity index 50% rename from db/changes/232001/00-authCode.sql rename to db/changes/232601/00-authCode.sql index 0415c90f05..3a85ce58ff 100644 --- a/db/changes/232001/00-authCode.sql +++ b/db/changes/232601/00-authCode.sql @@ -11,17 +11,3 @@ create table `salix`.`authCode` foreign key (userFk) references `account`.`user` (id) on update cascade on delete cascade ); - -create table `salix`.`userAccess` -( - userFk int UNSIGNED not null, - ip VARCHAR(25) not null, - agent text null, - platform VARCHAR(25) null, - browser VARCHAR(25) null, - constraint userAccess_pk - primary key (userFk), - constraint userAccess_user_null_fk - foreign key (userFk) references `account`.`user` (id) -) - auto_increment = 0; \ No newline at end of file diff --git a/db/changes/232001/00-department.sql b/db/changes/232601/00-department.sql similarity index 100% rename from db/changes/232001/00-department.sql rename to db/changes/232601/00-department.sql diff --git a/db/changes/232001/00-user.sql b/db/changes/232601/00-user.sql similarity index 100% rename from db/changes/232001/00-user.sql rename to db/changes/232601/00-user.sql diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index a6557ff895..4c41c5a063 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2821,8 +2821,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) VALUES - ('lilium', 'dev', 'http://localhost:9000/#/'), - ('salix', 'dev', 'http://localhost:5000/#!/'); + ('lilium', 'development', 'http://localhost:9000/#/'), + ('salix', 'development', 'http://localhost:5000/#!/'); INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) VALUES diff --git a/front/salix/components/change-password/index.html b/front/salix/components/change-password/index.html index 8d338d4118..026374bca0 100644 --- a/front/salix/components/change-password/index.html +++ b/front/salix/components/change-password/index.html @@ -21,6 +21,14 @@ type="password" autocomplete="false"> + + - \ No newline at end of file + diff --git a/print/templates/email/auth-code/auth-code.js b/print/templates/email/auth-code/auth-code.js index 7ddd1c2dbc..1606241be0 100755 --- a/print/templates/email/auth-code/auth-code.js +++ b/print/templates/email/auth-code/auth-code.js @@ -10,6 +10,12 @@ module.exports = { code: { type: String, required: true + }, + device: { + type: String + }, + ip: { + type: Number } } }; diff --git a/print/templates/email/auth-code/locale/en.yml b/print/templates/email/auth-code/locale/en.yml index 5f63d280f5..b52a73e937 100644 --- a/print/templates/email/auth-code/locale/en.yml +++ b/print/templates/email/auth-code/locale/en.yml @@ -1,5 +1,7 @@ subject: Verification code title: Verification code description: Somebody did request a verification code for login. If you didn't request it, please ignore this email. +device: 'Device: {0}' +ip: 'IP: {0}' Enter the following code to continue to your account: Enter the following code to continue to your account It expires in 5 minutes: It expires in 5 minutes diff --git a/print/templates/email/auth-code/locale/es.yml b/print/templates/email/auth-code/locale/es.yml index 31952891bc..f49b2511f8 100644 --- a/print/templates/email/auth-code/locale/es.yml +++ b/print/templates/email/auth-code/locale/es.yml @@ -1,5 +1,7 @@ subject: Código de verificación title: Código de verificación description: Alguien ha solicitado un código de verificación para poder iniciar sesión. Si no lo has solicitado tu, ignora este email. +device: 'Dispositivo: {0}' +ip: 'IP: {0}' Enter the following code to continue to your account: Introduce el siguiente código para poder continuar con tu cuenta It expires in 5 minutes: Expira en 5 minutos diff --git a/print/templates/email/auth-code/locale/fr.yml b/print/templates/email/auth-code/locale/fr.yml index e435a2487e..12606fcda7 100644 --- a/print/templates/email/auth-code/locale/fr.yml +++ b/print/templates/email/auth-code/locale/fr.yml @@ -1,5 +1,7 @@ subject: Code de vérification title: Code de vérification description: Quelqu'un a demandé un code de vérification pour se connecter. Si ce n'était pas toi, ignore cet email. +device: 'Appareil: {0}' +ip: 'IP: {0}' Enter the following code to continue to your account: Entrez le code suivant pour continuer avec votre compte It expires in 5 minutes: Il expire dans 5 minutes. diff --git a/print/templates/email/auth-code/locale/pt.yml b/print/templates/email/auth-code/locale/pt.yml index 940b03c4dd..e73f5e39ad 100644 --- a/print/templates/email/auth-code/locale/pt.yml +++ b/print/templates/email/auth-code/locale/pt.yml @@ -1,5 +1,7 @@ subject: Código de verificação title: Código de verificação description: Alguém solicitou um código de verificação para entrar. Se você não fez essa solicitação, ignore este e-mail. +device: 'Dispositivo: {0}' +ip: 'IP: {0}' Enter the following code to continue to your account: Insira o seguinte código para continuar com sua conta. It expires in 5 minutes: Expira em 5 minutos.