Validate code
gitea/salix/pipeline/head There was a failure building this commit
Details
gitea/salix/pipeline/head There was a failure building this commit
Details
This commit is contained in:
parent
641fdf39d8
commit
2e0325e567
|
@ -1,8 +1,9 @@
|
|||
const md5 = require('md5');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('login', {
|
||||
Self.remoteMethodCtx('login', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
|
@ -26,7 +27,7 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.login = async function(user, password) {
|
||||
Self.login = async function(ctx, user, password) {
|
||||
let $ = Self.app.models;
|
||||
let token;
|
||||
let usesEmail = user.indexOf('@') !== -1;
|
||||
|
@ -43,7 +44,7 @@ module.exports = Self => {
|
|||
? {email: user}
|
||||
: {name: user};
|
||||
let account = await Self.findOne({
|
||||
fields: ['active', 'password'],
|
||||
fields: ['id', 'active', 'password', 'twoFactor'],
|
||||
where
|
||||
});
|
||||
|
||||
|
@ -63,6 +64,29 @@ module.exports = Self => {
|
|||
}
|
||||
}
|
||||
|
||||
if (account.twoFactor === 'email') {
|
||||
const authAccess = await $.UserAccess.findOne({
|
||||
where: {
|
||||
userFk: account.id,
|
||||
ip: ctx.req.connection.remoteAddress
|
||||
}
|
||||
});
|
||||
if (!authAccess) {
|
||||
const code = String(Math.floor(Math.random() * 999999));
|
||||
const maxTTL = ((60 * 1000) * 5); // 5 min
|
||||
await $.AuthCode.upsertWithWhere({userFk: account.id}, {
|
||||
userFk: account.id,
|
||||
code: code,
|
||||
expires: Date.now() + maxTTL
|
||||
});
|
||||
|
||||
ctx.args.code = code;
|
||||
await Self.sendTemplate(ctx, 'auth-code');
|
||||
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
}
|
||||
|
||||
let loginInfo = Object.assign({password}, userInfo);
|
||||
token = await $.User.login(loginInfo, 'user');
|
||||
return {token: token.id};
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('validateAuth', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
},
|
||||
{
|
||||
arg: 'code',
|
||||
type: 'String',
|
||||
description: 'The auth code'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/validate-auth`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.validateAuth = async function(ctx, username, password, code) {
|
||||
const {AuthCode, UserAccess} = Self.app.models;
|
||||
|
||||
const authCode = await AuthCode.findOne({
|
||||
where: {
|
||||
code: code
|
||||
}
|
||||
});
|
||||
|
||||
const expired = Date.now() > authCode.expires;
|
||||
if (!authCode || expired)
|
||||
throw new UserError('Invalid or expired verification code');
|
||||
|
||||
const user = await Self.findById(authCode.userFk, {
|
||||
fields: ['name', 'twoFactor']
|
||||
});
|
||||
|
||||
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.login(ctx, username, password);
|
||||
};
|
||||
};
|
|
@ -5,6 +5,9 @@
|
|||
"AccountingType": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"AuthCode": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Bank": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
@ -116,6 +119,9 @@
|
|||
"Town": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"UserAccess": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Url": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = Self => {
|
|||
require('../methods/account/set-password')(Self);
|
||||
require('../methods/account/recover-password')(Self);
|
||||
require('../methods/account/validate-token')(Self);
|
||||
require('../methods/account/validate-auth')(Self);
|
||||
require('../methods/account/privileges')(Self);
|
||||
|
||||
// Validations
|
||||
|
|
|
@ -54,6 +54,9 @@
|
|||
},
|
||||
"hasGrant": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"twoFactor": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
@ -113,6 +116,13 @@
|
|||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "validateAuth",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "privileges",
|
||||
"accessType": "*",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "AuthCode",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "salix.authCode"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"userFk": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"expires": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "Account",
|
||||
"foreignKey": "userFk"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ create table `salix`.`authCode`
|
|||
(
|
||||
userFk int UNSIGNED not null,
|
||||
code int not null,
|
||||
expires TIMESTAMP not null,
|
||||
constraint authCode_pk
|
||||
primary key (userFk),
|
||||
constraint authCode_unique
|
||||
|
@ -11,3 +12,16 @@ create table `salix`.`authCode`
|
|||
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;
|
|
@ -1,3 +1,3 @@
|
|||
alter table `account`.`user`
|
||||
add `2FA` ENUM ('email') null comment 'Two factor auth type';
|
||||
add `twoFactor` ENUM ('email') null comment 'Two factor auth type';
|
||||
|
||||
|
|
|
@ -63,6 +63,23 @@ export default class Auth {
|
|||
json => this.onLoginOk(json, remember));
|
||||
}
|
||||
|
||||
validateCode(user, password, code, remember) {
|
||||
if (!user) {
|
||||
let err = new UserError('Please enter your username');
|
||||
err.code = 'EmptyLogin';
|
||||
return this.$q.reject(err);
|
||||
}
|
||||
|
||||
let params = {
|
||||
user: user,
|
||||
password: password || undefined,
|
||||
code: code
|
||||
};
|
||||
|
||||
return this.$http.post('Accounts/validate-auth', params).then(
|
||||
json => this.onLoginOk(json, remember));
|
||||
}
|
||||
|
||||
onLoginOk(json, remember) {
|
||||
this.vnToken.set(json.data.token, remember);
|
||||
|
||||
|
|
|
@ -5,13 +5,14 @@ import './style.scss';
|
|||
* A simple login form.
|
||||
*/
|
||||
export default class Controller {
|
||||
constructor($, $element, vnAuth) {
|
||||
constructor($, $element, vnAuth, $state) {
|
||||
Object.assign(this, {
|
||||
$,
|
||||
$element,
|
||||
vnAuth,
|
||||
user: localStorage.getItem('lastUser'),
|
||||
remember: true
|
||||
remember: true,
|
||||
$state
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -22,11 +23,21 @@ export default class Controller {
|
|||
localStorage.setItem('lastUser', this.user);
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
if (error.message === 'Forbidden') {
|
||||
this.outLayout.login = {
|
||||
user: this.user,
|
||||
password: this.password,
|
||||
remember: this.remember
|
||||
};
|
||||
this.$state.go('validate-email');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.password = '';
|
||||
this.focusUser();
|
||||
throw err;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -35,9 +46,12 @@ export default class Controller {
|
|||
this.$.userField.focus();
|
||||
}
|
||||
}
|
||||
Controller.$inject = ['$scope', '$element', 'vnAuth'];
|
||||
Controller.$inject = ['$scope', '$element', 'vnAuth', '$state'];
|
||||
|
||||
ngModule.vnComponent('vnLogin', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
controller: Controller,
|
||||
require: {
|
||||
outLayout: '^vnOutLayout'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
<h5 class="vn-mb-md vn-mt-lg" translate>Reset password</h5>
|
||||
<vn-textfield
|
||||
label="New password"
|
||||
ng-model="$ctrl.newPassword"
|
||||
type="password"
|
||||
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Repeat password"
|
||||
ng-model="$ctrl.repeatPassword"
|
||||
type="password">
|
||||
<h5 class="vn-mb-md vn-mt-lg" translate>Enter verification code</h5>
|
||||
<span>Please enter the verification code that we have sent to your email address within 5 minutes.</span>
|
||||
<vn-textfield label="Code" ng-model="$ctrl.code" type="text" vn-focus>
|
||||
</vn-textfield>
|
||||
<div class="footer">
|
||||
<vn-submit label="Reset password" ng-click="$ctrl.submit()"></vn-submit>
|
||||
<vn-submit label="Validate" ng-click="$ctrl.submit()"></vn-submit>
|
||||
<div class="spinner-wrapper">
|
||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -2,47 +2,42 @@ import ngModule from '../../module';
|
|||
import './style.scss';
|
||||
|
||||
export default class Controller {
|
||||
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
|
||||
constructor($scope, $element, vnAuth, $state) {
|
||||
Object.assign(this, {
|
||||
$scope,
|
||||
$element,
|
||||
$http,
|
||||
vnApp,
|
||||
$translate,
|
||||
$state,
|
||||
$location
|
||||
vnAuth,
|
||||
user: localStorage.getItem('lastUser'),
|
||||
remember: true,
|
||||
$state
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.$http.get('UserPasswords/findOne')
|
||||
.then(res => {
|
||||
this.passRequirements = res.data;
|
||||
});
|
||||
this.loginData = this.outLayout.login;
|
||||
if (!this.loginData)
|
||||
this.$state.go('login');
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.newPassword)
|
||||
throw new UserError(`You must enter a new password`);
|
||||
if (this.newPassword != this.repeatPassword)
|
||||
throw new UserError(`Passwords don't match`);
|
||||
|
||||
const headers = {
|
||||
Authorization: this.$location.$$search.access_token
|
||||
};
|
||||
|
||||
const newPassword = this.newPassword;
|
||||
|
||||
this.$http.post('users/reset-password', {newPassword}, {headers})
|
||||
this.loading = true;
|
||||
this.vnAuth.validateCode(this.loginData.user, this.loginData.password, this.code, this.loginData.remember)
|
||||
.then(() => {
|
||||
this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
|
||||
this.$state.go('login');
|
||||
localStorage.setItem('lastUser', this.user);
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
|
||||
Controller.$inject = ['$scope', '$element', 'vnAuth', '$state'];
|
||||
|
||||
ngModule.vnComponent('vnValidateEmail', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
controller: Controller,
|
||||
require: {
|
||||
outLayout: '^vnOutLayout'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -273,6 +273,8 @@
|
|||
"Not exist this branch": "La rama no existe",
|
||||
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
|
||||
"Insert a date range": "Inserte un rango de fechas",
|
||||
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
||||
"Comment added to client": "Observación añadida al cliente {{clientFk}}"
|
||||
}
|
||||
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
||||
"Comment added to client": "Observación añadida al cliente {{clientFk}}",
|
||||
"Invalid auth code": "Invalid auth code",
|
||||
"Invalid or expired verification code": "Invalid or expired verification code"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = class ForbiddenError extends Error {
|
||||
constructor(message, code, ...translateArgs) {
|
||||
super(message);
|
||||
this.name = 'ForbiddenError';
|
||||
this.statusCode = 403;
|
||||
this.code = code;
|
||||
this.translateArgs = translateArgs;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
const Stylesheet = require(`vn-print/core/stylesheet`);
|
||||
|
||||
const path = require('path');
|
||||
const vnPrintPath = path.resolve('print');
|
||||
|
||||
module.exports = new Stylesheet([
|
||||
`${vnPrintPath}/common/css/spacing.css`,
|
||||
`${vnPrintPath}/common/css/misc.css`,
|
||||
`${vnPrintPath}/common/css/layout.css`,
|
||||
`${vnPrintPath}/common/css/email.css`,
|
||||
`${__dirname}/style.css`])
|
||||
.mergeStyles();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.code {
|
||||
border: 2px dashed #8dba25;
|
||||
border-radius: 3px;
|
||||
text-align: center
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<email-body v-bind="$props">
|
||||
<div class="grid-row">
|
||||
<div class="grid-block vn-pa-ml">
|
||||
<h1>{{ $t('title') }}</h1>
|
||||
<p v-html="$t('description')"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-block vn-pa-ml">
|
||||
<p>{{$t('Enter the following code to continue to your account')}}</p>
|
||||
<div class="code vn-pa-sm vn-m-md">
|
||||
{{ code }}
|
||||
</div>
|
||||
<p>{{$t('It expires in 5 minutes.')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</email-body>
|
|
@ -0,0 +1,15 @@
|
|||
const Component = require(`vn-print/core/component`);
|
||||
const emailBody = new Component('email-body');
|
||||
|
||||
module.exports = {
|
||||
name: 'auth-code',
|
||||
components: {
|
||||
'email-body': emailBody.build(),
|
||||
},
|
||||
props: {
|
||||
code: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
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.
|
||||
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
|
Loading…
Reference in New Issue