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 md5 = require('md5');
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethod('login', {
|
Self.remoteMethodCtx('login', {
|
||||||
description: 'Login a user with username/email and password',
|
description: 'Login a user with username/email and password',
|
||||||
accepts: [
|
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 $ = Self.app.models;
|
||||||
let token;
|
let token;
|
||||||
let usesEmail = user.indexOf('@') !== -1;
|
let usesEmail = user.indexOf('@') !== -1;
|
||||||
|
@ -43,7 +44,7 @@ module.exports = Self => {
|
||||||
? {email: user}
|
? {email: user}
|
||||||
: {name: user};
|
: {name: user};
|
||||||
let account = await Self.findOne({
|
let account = await Self.findOne({
|
||||||
fields: ['active', 'password'],
|
fields: ['id', 'active', 'password', 'twoFactor'],
|
||||||
where
|
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);
|
let loginInfo = Object.assign({password}, userInfo);
|
||||||
token = await $.User.login(loginInfo, 'user');
|
token = await $.User.login(loginInfo, 'user');
|
||||||
return {token: token.id};
|
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": {
|
"AccountingType": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
"AuthCode": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
"Bank": {
|
"Bank": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
@ -116,6 +119,9 @@
|
||||||
"Town": {
|
"Town": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
"UserAccess": {
|
||||||
|
"dataSource": "vn"
|
||||||
|
},
|
||||||
"Url": {
|
"Url": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ module.exports = Self => {
|
||||||
require('../methods/account/set-password')(Self);
|
require('../methods/account/set-password')(Self);
|
||||||
require('../methods/account/recover-password')(Self);
|
require('../methods/account/recover-password')(Self);
|
||||||
require('../methods/account/validate-token')(Self);
|
require('../methods/account/validate-token')(Self);
|
||||||
|
require('../methods/account/validate-auth')(Self);
|
||||||
require('../methods/account/privileges')(Self);
|
require('../methods/account/privileges')(Self);
|
||||||
|
|
||||||
// Validations
|
// Validations
|
||||||
|
|
|
@ -54,6 +54,9 @@
|
||||||
},
|
},
|
||||||
"hasGrant": {
|
"hasGrant": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"twoFactor": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relations": {
|
"relations": {
|
||||||
|
@ -113,6 +116,13 @@
|
||||||
"principalId": "$authenticated",
|
"principalId": "$authenticated",
|
||||||
"permission": "ALLOW"
|
"permission": "ALLOW"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"property": "validateAuth",
|
||||||
|
"accessType": "EXECUTE",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"property": "privileges",
|
"property": "privileges",
|
||||||
"accessType": "*",
|
"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,
|
userFk int UNSIGNED not null,
|
||||||
code int not null,
|
code int not null,
|
||||||
|
expires TIMESTAMP not null,
|
||||||
constraint authCode_pk
|
constraint authCode_pk
|
||||||
primary key (userFk),
|
primary key (userFk),
|
||||||
constraint authCode_unique
|
constraint authCode_unique
|
||||||
|
@ -11,3 +12,16 @@ create table `salix`.`authCode`
|
||||||
on update cascade on delete cascade
|
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`
|
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));
|
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) {
|
onLoginOk(json, remember) {
|
||||||
this.vnToken.set(json.data.token, remember);
|
this.vnToken.set(json.data.token, remember);
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,14 @@ import './style.scss';
|
||||||
* A simple login form.
|
* A simple login form.
|
||||||
*/
|
*/
|
||||||
export default class Controller {
|
export default class Controller {
|
||||||
constructor($, $element, vnAuth) {
|
constructor($, $element, vnAuth, $state) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
$,
|
$,
|
||||||
$element,
|
$element,
|
||||||
vnAuth,
|
vnAuth,
|
||||||
user: localStorage.getItem('lastUser'),
|
user: localStorage.getItem('lastUser'),
|
||||||
remember: true
|
remember: true,
|
||||||
|
$state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,11 +23,21 @@ export default class Controller {
|
||||||
localStorage.setItem('lastUser', this.user);
|
localStorage.setItem('lastUser', this.user);
|
||||||
this.loading = false;
|
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.loading = false;
|
||||||
this.password = '';
|
this.password = '';
|
||||||
this.focusUser();
|
this.focusUser();
|
||||||
throw err;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,9 +46,12 @@ export default class Controller {
|
||||||
this.$.userField.focus();
|
this.$.userField.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Controller.$inject = ['$scope', '$element', 'vnAuth'];
|
Controller.$inject = ['$scope', '$element', 'vnAuth', '$state'];
|
||||||
|
|
||||||
ngModule.vnComponent('vnLogin', {
|
ngModule.vnComponent('vnLogin', {
|
||||||
template: require('./index.html'),
|
template: require('./index.html'),
|
||||||
controller: Controller
|
controller: Controller,
|
||||||
|
require: {
|
||||||
|
outLayout: '^vnOutLayout'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
<h5 class="vn-mb-md vn-mt-lg" translate>Reset password</h5>
|
<h5 class="vn-mb-md vn-mt-lg" translate>Enter verification code</h5>
|
||||||
<vn-textfield
|
<span>Please enter the verification code that we have sent to your email address within 5 minutes.</span>
|
||||||
label="New password"
|
<vn-textfield label="Code" ng-model="$ctrl.code" type="text" vn-focus>
|
||||||
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">
|
|
||||||
</vn-textfield>
|
</vn-textfield>
|
||||||
<div class="footer">
|
<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">
|
<div class="spinner-wrapper">
|
||||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,47 +2,42 @@ import ngModule from '../../module';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
export default class Controller {
|
export default class Controller {
|
||||||
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
|
constructor($scope, $element, vnAuth, $state) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
$scope,
|
$scope,
|
||||||
$element,
|
$element,
|
||||||
$http,
|
vnAuth,
|
||||||
vnApp,
|
user: localStorage.getItem('lastUser'),
|
||||||
$translate,
|
remember: true,
|
||||||
$state,
|
$state
|
||||||
$location
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
this.$http.get('UserPasswords/findOne')
|
this.loginData = this.outLayout.login;
|
||||||
.then(res => {
|
if (!this.loginData)
|
||||||
this.passRequirements = res.data;
|
this.$state.go('login');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.newPassword)
|
this.loading = true;
|
||||||
throw new UserError(`You must enter a new password`);
|
this.vnAuth.validateCode(this.loginData.user, this.loginData.password, this.code, this.loginData.remember)
|
||||||
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})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
|
localStorage.setItem('lastUser', this.user);
|
||||||
this.$state.go('login');
|
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', {
|
ngModule.vnComponent('vnValidateEmail', {
|
||||||
template: require('./index.html'),
|
template: require('./index.html'),
|
||||||
controller: Controller
|
controller: Controller,
|
||||||
|
require: {
|
||||||
|
outLayout: '^vnOutLayout'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -274,5 +274,7 @@
|
||||||
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
|
"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",
|
"Insert a date range": "Inserte un rango de fechas",
|
||||||
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
||||||
"Comment added to client": "Observación añadida al cliente {{clientFk}}"
|
"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