refs #5475 feat(signin): use twoFactor and passExpired
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
aeb04efe82
commit
3a5e591cf0
|
@ -1,4 +1,5 @@
|
||||||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('signin', {
|
Self.remoteMethodCtx('signin', {
|
||||||
|
@ -26,7 +27,6 @@ module.exports = Self => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.signin = async function(ctx, user, password) {
|
Self.signin = async function(ctx, user, password) {
|
||||||
const $ = Self.app.models;
|
|
||||||
const usesEmail = user.indexOf('@') !== -1;
|
const usesEmail = user.indexOf('@') !== -1;
|
||||||
|
|
||||||
const where = usesEmail
|
const where = usesEmail
|
||||||
|
@ -34,30 +34,70 @@ module.exports = Self => {
|
||||||
: {name: user};
|
: {name: user};
|
||||||
|
|
||||||
const vnUser = await Self.findOne({
|
const vnUser = await Self.findOne({
|
||||||
fields: ['id', 'active', 'email', 'password', 'twoFactor'],
|
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
|
||||||
if (vnUser && vnUser.twoFactor === 'email') {
|
const validCredentials = vnUser
|
||||||
const code = String(Math.floor(Math.random() * 999999));
|
&& await vnUser.hasPassword(password);
|
||||||
const maxTTL = ((60 * 1000) * 5); // 5 min
|
|
||||||
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
|
|
||||||
userFk: vnUser.id,
|
|
||||||
code: code,
|
|
||||||
expires: Date.now() + maxTTL
|
|
||||||
});
|
|
||||||
|
|
||||||
const params = {
|
if (validCredentials) {
|
||||||
recipientId: vnUser.id,
|
if (!vnUser.active)
|
||||||
recipient: vnUser.email,
|
throw new UserError('User disabled');
|
||||||
code: code
|
await Self.sendTwoFactor(ctx, vnUser);
|
||||||
};
|
await Self.passExpired(vnUser);
|
||||||
ctx.args = {...ctx.args, ...params};
|
|
||||||
await Self.sendTemplate(ctx, 'auth-code');
|
|
||||||
|
|
||||||
|
if (vnUser.twoFactor)
|
||||||
throw new ForbiddenError('REQUIRES_2FA');
|
throw new ForbiddenError('REQUIRES_2FA');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Self.validateLogin(user, password);
|
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.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 = {
|
||||||
|
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}
|
||||||
|
};
|
||||||
|
await Self.sendTemplate(params, 'auth-code');
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const UserError = require('vn-loopback/util/user-error');
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('validateAuth', {
|
Self.remoteMethod('validateAuth', {
|
||||||
description: 'Login a user with username/email and password',
|
description: 'Login a user with username/email and password',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
|
@ -31,8 +31,13 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.validateAuth = async function(ctx, username, password, code) {
|
Self.validateAuth = async function(username, password, code) {
|
||||||
const {AuthCode, UserAccess} = Self.app.models;
|
await Self.validateCode(code);
|
||||||
|
return Self.validateLogin(username, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.validateCode = async(username, code) => {
|
||||||
|
const {AuthCode} = Self.app.models;
|
||||||
|
|
||||||
const authCode = await AuthCode.findOne({
|
const authCode = await AuthCode.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -47,27 +52,10 @@ module.exports = Self => {
|
||||||
const user = await Self.findById(authCode.userFk, {
|
const user = await Self.findById(authCode.userFk, {
|
||||||
fields: ['name', 'twoFactor']
|
fields: ['name', 'twoFactor']
|
||||||
});
|
});
|
||||||
|
console.log(username, code);
|
||||||
if (user.name !== username)
|
if (user.name !== username)
|
||||||
throw new UserError('Authentication failed');
|
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();
|
await authCode.destroy();
|
||||||
|
|
||||||
return Self.validateLogin(username, password);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -116,9 +116,6 @@
|
||||||
"Town": {
|
"Town": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
"UserAccess": {
|
|
||||||
"dataSource": "vn"
|
|
||||||
},
|
|
||||||
"Url": {
|
"Url": {
|
||||||
"dataSource": "vn"
|
"dataSource": "vn"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
const vnModel = require('vn-loopback/common/models/vn-model');
|
const vnModel = require('vn-loopback/common/models/vn-model');
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
const {Email} = require('vn-print');
|
const {Email} = require('vn-print');
|
||||||
|
const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = function(Self) {
|
module.exports = function(Self) {
|
||||||
vnModel(Self);
|
vnModel(Self);
|
||||||
|
@ -110,53 +111,11 @@ module.exports = function(Self) {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.validateLogin = async function(user, password) {
|
Self.validateLogin = async function(user, password) {
|
||||||
const models = Self.app.models;
|
|
||||||
const usesEmail = user.indexOf('@') !== -1;
|
const usesEmail = user.indexOf('@') !== -1;
|
||||||
let token;
|
|
||||||
|
|
||||||
const userInfo = usesEmail
|
const userInfo = usesEmail
|
||||||
? {email: user}
|
? {email: user}
|
||||||
: {username: 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);
|
let loginInfo = Object.assign({password}, userInfo);
|
||||||
token = await Self.login(loginInfo, 'user');
|
token = await Self.login(loginInfo, 'user');
|
||||||
|
@ -197,6 +156,50 @@ module.exports = function(Self) {
|
||||||
|
|
||||||
Self.sharedClass._methods.find(method => method.name == 'changePassword')
|
Self.sharedClass._methods.find(method => method.name == 'changePassword')
|
||||||
.accessScopes = ['change-password'];
|
.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
|
// FIXME: https://redmine.verdnatura.es/issues/5761
|
||||||
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
|
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
|
||||||
|
|
|
@ -11,17 +11,3 @@ create table `salix`.`authCode`
|
||||||
foreign key (userFk) references `account`.`user` (id)
|
foreign key (userFk) references `account`.`user` (id)
|
||||||
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;
|
|
|
@ -2821,8 +2821,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
|
||||||
|
|
||||||
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
|
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
|
||||||
VALUES
|
VALUES
|
||||||
('lilium', 'dev', 'http://localhost:9000/#/'),
|
('lilium', 'development', 'http://localhost:9000/#/'),
|
||||||
('salix', 'dev', 'http://localhost:5000/#!/');
|
('salix', 'development', 'http://localhost:5000/#!/');
|
||||||
|
|
||||||
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
|
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
|
@ -21,6 +21,14 @@
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="false">
|
autocomplete="false">
|
||||||
</vn-textfield>
|
</vn-textfield>
|
||||||
|
<vn-textfield
|
||||||
|
ng-if="$ctrl.$state.params.twoFactor"
|
||||||
|
label="Verification code"
|
||||||
|
ng-model="$ctrl.verificationCode"
|
||||||
|
vn-name="verificationCode"
|
||||||
|
autocomplete="false"
|
||||||
|
class="vn-mt-md">
|
||||||
|
</vn-textfield>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
|
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
|
||||||
<div class="spinner-wrapper">
|
<div class="spinner-wrapper">
|
||||||
|
|
|
@ -26,23 +26,33 @@ export default class Controller {
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
const id = this.$state.params.id;
|
const id = this.$state.params.id;
|
||||||
const newPassword = this.newPassword;
|
|
||||||
const oldPassword = this.oldPassword;
|
const oldPassword = this.oldPassword;
|
||||||
|
const newPassword = this.newPassword;
|
||||||
|
const repeatPassword = this.repeatPassword;
|
||||||
|
const verificationCode = this.verificationCode;
|
||||||
|
|
||||||
if (!newPassword)
|
if (!oldPassword || !newPassword || !repeatPassword)
|
||||||
throw new UserError(`You must enter a new password`);
|
throw new UserError(`You must fill all the fields`);
|
||||||
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)
|
||||||
|
throw new UserError(`You can't use the same password`);
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: this.$state.params.token
|
Authorization: this.$state.params.token
|
||||||
};
|
};
|
||||||
|
console.log({
|
||||||
|
id,
|
||||||
|
oldPassword,
|
||||||
|
newPassword,
|
||||||
|
verificationCode
|
||||||
|
});
|
||||||
this.$http.post('VnUsers/change-password',
|
this.$http.post('VnUsers/change-password',
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
oldPassword,
|
oldPassword,
|
||||||
newPassword
|
newPassword,
|
||||||
|
verificationCode
|
||||||
},
|
},
|
||||||
{headers}
|
{headers}
|
||||||
).then(() => {
|
).then(() => {
|
||||||
|
|
|
@ -2,6 +2,9 @@ Change password: Cambiar contraseña
|
||||||
Old password: Antigua contraseña
|
Old password: Antigua contraseña
|
||||||
New password: Nueva contraseña
|
New password: Nueva contraseña
|
||||||
Repeat password: Repetir 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
|
||||||
Password updated!: ¡Contraseña actualizada!
|
Password updated!: ¡Contraseña actualizada!
|
||||||
Password requirements: >
|
Password requirements: >
|
||||||
La contraseña debe tener al menos {{ length }} caracteres de longitud,
|
La contraseña debe tener al menos {{ length }} caracteres de longitud,
|
||||||
|
|
|
@ -44,7 +44,7 @@ function config($stateProvider, $urlRouterProvider) {
|
||||||
})
|
})
|
||||||
.state('change-password', {
|
.state('change-password', {
|
||||||
parent: 'outLayout',
|
parent: 'outLayout',
|
||||||
url: '/change-password?id&token',
|
url: '/change-password?id&token&twoFactor',
|
||||||
description: 'Change password',
|
description: 'Change password',
|
||||||
template: '<vn-change-password></vn-change-password>'
|
template: '<vn-change-password></vn-change-password>'
|
||||||
})
|
})
|
||||||
|
|
|
@ -295,5 +295,7 @@
|
||||||
"Pass expired": "La contraseña ha caducado, cambiela desde Salix",
|
"Pass expired": "La contraseña ha caducado, cambiela desde Salix",
|
||||||
"Invalid NIF for VIES": "Invalid NIF for VIES",
|
"Invalid NIF for VIES": "Invalid NIF for VIES",
|
||||||
"Ticket does not exist": "Este ticket no existe",
|
"Ticket does not exist": "Este ticket no existe",
|
||||||
"Ticket is already signed": "Este ticket ya ha sido firmado"
|
"Ticket is already signed": "Este ticket ya ha sido firmado",
|
||||||
|
"Authentication failed": "Autenticación fallida",
|
||||||
|
"You can't use the same password": "No puedes usar la misma contraseña"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
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: [
|
||||||
{
|
{
|
||||||
|
@ -23,5 +23,5 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.login = async(user, password) => Self.app.models.VnUser.validateLogin(user, password);
|
Self.login = async(ctx, user, password) => Self.app.models.VnUser.signin(ctx, user, password);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
<div class="grid-block vn-pa-ml">
|
<div class="grid-block vn-pa-ml">
|
||||||
<h1>{{ $t('title') }}</h1>
|
<h1>{{ $t('title') }}</h1>
|
||||||
<p v-html="$t('description')"></p>
|
<p v-html="$t('description')"></p>
|
||||||
|
<p v-html="$t('device', [device])"></p>
|
||||||
|
<p v-html="$t('ip', [ip])"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
|
|
|
@ -10,6 +10,12 @@ module.exports = {
|
||||||
code: {
|
code: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
ip: {
|
||||||
|
type: Number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
subject: Verification code
|
subject: Verification code
|
||||||
title: Verification code
|
title: Verification code
|
||||||
description: Somebody did request a verification code for login. If you didn't request it, please ignore this email.
|
description: Somebody did request a verification code for login. If you didn't request it, please ignore this email.
|
||||||
|
device: 'Device: <strong>{0}</strong>'
|
||||||
|
ip: 'IP: <strong>{0}</strong>'
|
||||||
Enter the following code to continue to your account: Enter the following code to continue to your account
|
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
|
It expires in 5 minutes: It expires in 5 minutes
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
subject: Código de verificación
|
subject: Código de verificación
|
||||||
title: 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.
|
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: <strong>{0}</strong>'
|
||||||
|
ip: 'IP: <strong>{0}</strong>'
|
||||||
Enter the following code to continue to your account: Introduce el siguiente código para poder continuar con tu cuenta
|
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
|
It expires in 5 minutes: Expira en 5 minutos
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
subject: Code de vérification
|
subject: Code de vérification
|
||||||
title: 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.
|
description: Quelqu'un a demandé un code de vérification pour se connecter. Si ce n'était pas toi, ignore cet email.
|
||||||
|
device: 'Appareil: <strong>{0}</strong>'
|
||||||
|
ip: 'IP: <strong>{0}</strong>'
|
||||||
Enter the following code to continue to your account: Entrez le code suivant pour continuer avec votre compte
|
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.
|
It expires in 5 minutes: Il expire dans 5 minutes.
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
subject: Código de verificação
|
subject: Código de verificação
|
||||||
title: 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.
|
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: <strong>{0}</strong>'
|
||||||
|
ip: 'IP: <strong>{0}</strong>'
|
||||||
Enter the following code to continue to your account: Insira o seguinte código para continuar com sua conta.
|
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.
|
It expires in 5 minutes: Expira em 5 minutos.
|
||||||
|
|
Loading…
Reference in New Issue