[NEW] Add ability to block failed login attempts by user and IP (#17783)

pull/17979/head
Marcos Spessatto Defendi 5 years ago committed by GitHub
parent 143ff3ab58
commit 0aada1571e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      app/authentication/server/ILoginAttempt.ts
  2. 23
      app/authentication/server/hooks/login.ts
  3. 3
      app/authentication/server/index.ts
  4. 27
      app/authentication/server/lib/logLoginAttempts.ts
  5. 106
      app/authentication/server/lib/restrictLoginAttempts.ts
  6. 31
      app/authentication/server/startup/index.js
  7. 69
      app/authentication/server/startup/settings.ts
  8. 11
      app/models/server/models/ServerEvents.ts
  9. 1
      app/models/server/models/Sessions.js
  10. 67
      app/models/server/raw/ServerEvents.ts
  11. 9
      app/models/server/raw/Sessions.js
  12. 6
      app/models/server/raw/Users.js
  13. 3
      app/models/server/raw/index.ts
  14. 4
      app/ui-login/client/login/form.js
  15. 5
      app/utils/lib/date.helper.ts
  16. 14
      definition/IServerEvent.ts
  17. 19
      packages/rocketchat-i18n/i18n/en.i18n.json
  18. 19
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  19. 1
      server/importPackages.js
  20. 1
      server/main.js
  21. 2
      server/startup/migrations/v152.js

@ -0,0 +1,20 @@
import { IUser } from '../../../definition/IUser';
import { IMethodConnection } from '../../../definition/IMethodThisType';
interface IMethodArgument {
user?: { username: string };
password?: {
digest: string;
algorithm: string;
};
resume?: string;
}
export interface ILoginAttempt {
type: string;
allowed: boolean;
methodName: string;
methodArguments: IMethodArgument[];
connection: IMethodConnection;
user?: IUser;
}

@ -0,0 +1,23 @@
import { Accounts } from 'meteor/accounts-base';
import { ILoginAttempt } from '../ILoginAttempt';
import { saveFailedLoginAttempts, saveSuccessfulLogin } from '../lib/restrictLoginAttempts';
import { logFailedLoginAttempts } from '../lib/logLoginAttempts';
import { callbacks } from '../../../callbacks/server';
import { settings } from '../../../settings/server';
Accounts.onLoginFailure((login: ILoginAttempt) => {
if (settings.get('Block_Multiple_Failed_Logins_Enabled')) {
saveFailedLoginAttempts(login);
}
logFailedLoginAttempts(login);
});
callbacks.add('afterValidateLogin', (login: ILoginAttempt) => {
if (!settings.get('Block_Multiple_Failed_Logins_Enabled')) {
return;
}
saveSuccessfulLogin(login);
});

@ -0,0 +1,3 @@
import './hooks/login';
export * from './startup';

@ -0,0 +1,27 @@
import { ILoginAttempt } from '../ILoginAttempt';
import { settings } from '../../../settings/server';
export const logFailedLoginAttempts = (login: ILoginAttempt): void => {
if (!settings.get('Login_Logs_Enabled')) {
return;
}
let user = 'unknown';
if (login.methodArguments[0]?.user?.username && settings.get('Login_Logs_Username')) {
user = login.methodArguments[0]?.user?.username;
}
const { connection } = login;
let { clientAddress } = connection;
if (!settings.get('Login_Logs_ClientIp')) {
clientAddress = '-';
}
let forwardedFor = connection.httpHeaders['x-forwarded-for'];
if (!settings.get('Login_Logs_ForwardedForIp')) {
forwardedFor = '-';
}
let userAgent = connection.httpHeaders['user-agent'];
if (!settings.get('Login_Logs_UserAgent')) {
userAgent = '-';
}
console.log('Failed login detected - Username[%s] ClientAddress[%s] ForwardedFor[%s] UserAgent[%s]', user, clientAddress, forwardedFor, userAgent);
};

@ -0,0 +1,106 @@
import moment from 'moment';
import { ILoginAttempt } from '../ILoginAttempt';
import { ServerEvents, Users } from '../../../models/server/raw';
import { IServerEventType } from '../../../../definition/IServerEvent';
import { IUser } from '../../../../definition/IUser';
import { settings } from '../../../settings/server';
import { addMinutesToADate } from '../../../utils/lib/date.helper';
import Sessions from '../../../models/server/raw/Sessions';
export const isValidLoginAttemptByIp = async (ip: string): Promise<boolean> => {
const whitelist = String(settings.get('Block_Multiple_Failed_Logins_Ip_Whitelist')).split(',');
if (!settings.get('Block_Multiple_Failed_Logins_Enabled')
|| !settings.get('Block_Multiple_Failed_Logins_By_Ip')
|| whitelist.includes(ip)) {
return true;
}
const lastLogin = await Sessions.findLastLoginByIp(ip);
let failedAttemptsSinceLastLogin;
if (!lastLogin) {
failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByIp(ip);
} else {
failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByIpSince(ip, new Date(lastLogin.loginAt));
}
const attemptsUntilBlock = settings.get('Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip');
if (attemptsUntilBlock && failedAttemptsSinceLastLogin < attemptsUntilBlock) {
return true;
}
const lastAttemptAt = (await ServerEvents.findLastFailedAttemptByIp(ip))?.ts;
if (!lastAttemptAt) {
return true;
}
const minutesUntilUnblock = settings.get('Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes') as number;
const willBeBlockedUntil = addMinutesToADate(new Date(lastAttemptAt), minutesUntilUnblock);
return moment(new Date()).isSameOrAfter(willBeBlockedUntil);
};
export const isValidAttemptByUser = async (login: ILoginAttempt): Promise<boolean> => {
if (!settings.get('Block_Multiple_Failed_Logins_Enabled')
|| !settings.get('Block_Multiple_Failed_Logins_By_User')) {
return true;
}
const user = login.user || await Users.findOneByUsername(login.methodArguments[0].user?.username);
if (!user) {
return true;
}
let failedAttemptsSinceLastLogin;
if (!user?.lastLogin) {
failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByUsername(user.username);
} else {
failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByUsernameSince(user.username, new Date(user.lastLogin));
}
const attemptsUntilBlock = settings.get('Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User');
if (attemptsUntilBlock && failedAttemptsSinceLastLogin < attemptsUntilBlock) {
return true;
}
const lastAttemptAt = (await ServerEvents.findLastFailedAttemptByUsername(user.username as string))?.ts;
if (!lastAttemptAt) {
return true;
}
const minutesUntilUnblock = settings.get('Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes') as number;
const willBeBlockedUntil = addMinutesToADate(new Date(lastAttemptAt), minutesUntilUnblock);
return moment(new Date()).isSameOrAfter(willBeBlockedUntil);
};
export const saveFailedLoginAttempts = async (login: ILoginAttempt): Promise<void> => {
const user: Partial<IUser> = {
_id: login.user?._id,
username: login.user?.username || login.methodArguments[0].user?.username,
};
await ServerEvents.insertOne({
ip: login.connection.clientAddress,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
ts: new Date(),
u: user,
});
};
export const saveSuccessfulLogin = async (login: ILoginAttempt): Promise<void> => {
await ServerEvents.insertOne({
ip: login.connection.clientAddress,
t: IServerEventType.LOGIN,
ts: new Date(),
u: login.user,
});
};

@ -5,13 +5,18 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import s from 'underscore.string';
import * as Mailer from '../../app/mailer';
import { settings } from '../../app/settings';
import { callbacks } from '../../app/callbacks';
import { Roles, Users, Settings } from '../../app/models';
import { Users as UsersRaw } from '../../app/models/server/raw';
import { addUserRoles } from '../../app/authorization';
import { getAvatarSuggestionForUser } from '../../app/lib/server/functions';
import * as Mailer from '../../../mailer/server/api';
import { settings } from '../../../settings/server';
import { callbacks } from '../../../callbacks/server';
import { Roles, Users, Settings } from '../../../models/server';
import { Users as UsersRaw } from '../../../models/server/raw';
import { addUserRoles } from '../../../authorization/server';
import { getAvatarSuggestionForUser } from '../../../lib/server/functions';
import {
isValidAttemptByUser,
isValidLoginAttemptByIp,
} from '../lib/restrictLoginAttempts';
import './settings';
Accounts.config({
forbidClientAccountCreation: true,
@ -291,6 +296,18 @@ Accounts.insertUserDoc = _.wrap(Accounts.insertUserDoc, function(insertUserDoc,
Accounts.validateLoginAttempt(function(login) {
login = callbacks.run('beforeValidateLogin', login);
if (!Promise.await(isValidLoginAttemptByIp(login.connection?.clientAddress))) {
throw new Meteor.Error('error-login-blocked-for-ip', 'Login has been temporarily blocked For IP', {
function: 'Accounts.validateLoginAttempt',
});
}
if (!Promise.await(isValidAttemptByUser(login))) {
throw new Meteor.Error('error-login-blocked-for-user', 'Login has been temporarily blocked For User', {
function: 'Accounts.validateLoginAttempt',
});
}
if (login.allowed !== true) {
return login.allowed;
}

@ -0,0 +1,69 @@
import { Meteor } from 'meteor/meteor';
import { settings } from '../../../settings/server';
Meteor.startup(function() {
settings.addGroup('Accounts', function() {
const enableQueryCollectData = { _id: 'Block_Multiple_Failed_Logins_Enabled', value: true };
this.section('Login_Attempts', function() {
this.add('Block_Multiple_Failed_Logins_Enabled', false, {
type: 'boolean',
});
this.add('Block_Multiple_Failed_Logins_By_User', true, {
type: 'boolean',
enableQuery: enableQueryCollectData,
});
const enableQueryByUser = [enableQueryCollectData, { _id: 'Block_Multiple_Failed_Logins_By_User', value: true }];
this.add('Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User', 10, {
type: 'int',
enableQuery: enableQueryByUser,
});
this.add('Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes', 5, {
type: 'int',
enableQuery: enableQueryByUser,
});
this.add('Block_Multiple_Failed_Logins_By_Ip', true, {
type: 'boolean',
enableQuery: enableQueryCollectData,
});
const enableQueryByIp = [enableQueryCollectData, { _id: 'Block_Multiple_Failed_Logins_By_Ip', value: true }];
this.add('Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip', 50, {
type: 'int',
enableQuery: enableQueryByIp,
});
this.add('Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes', 5, {
type: 'int',
enableQuery: enableQueryByIp,
});
this.add('Block_Multiple_Failed_Logins_Ip_Whitelist', '', {
type: 'string',
enableQuery: enableQueryByIp,
});
});
this.section('Login_Logs', function() {
const enableQueryAudit = { _id: 'Login_Logs_Enabled', value: true };
this.add('Login_Logs_Enabled', false, { type: 'boolean' });
this.add('Login_Logs_Username', false, { type: 'boolean', enableQuery: enableQueryAudit });
this.add('Login_Logs_UserAgent', false, { type: 'boolean', enableQuery: enableQueryAudit });
this.add('Login_Logs_ClientIp', false, { type: 'boolean', enableQuery: enableQueryAudit });
this.add('Login_Logs_ForwardedForIp', false, { type: 'boolean', enableQuery: enableQueryAudit });
});
});
});

@ -0,0 +1,11 @@
import { Base } from './_Base';
export class ServerEvents extends Base {
constructor() {
super('server_events');
this.tryEnsureIndex({ t: 1, ip: 1, ts: -1 });
this.tryEnsureIndex({ t: 1, 'u.username': 1, ts: -1 });
}
}
export default new ServerEvents();

@ -379,6 +379,7 @@ export class Sessions extends Base {
this.tryEnsureIndex({ sessionId: 1 });
this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 });
this.tryEnsureIndex({ type: 1 });
this.tryEnsureIndex({ ip: 1, loginAt: 1 });
this.tryEnsureIndex({ _computedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 45 });
}

@ -0,0 +1,67 @@
import { Collection, ObjectId } from 'mongodb';
import { BaseRaw } from './BaseRaw';
import { IServerEvent, IServerEventType } from '../../../../definition/IServerEvent';
import { IUser } from '../../../../definition/IUser';
export class ServerEventsRaw extends BaseRaw {
public readonly col!: Collection<IServerEvent>;
async insertOne(data: Omit<IServerEvent, '_id'>): Promise<any> {
if (data.u) {
data.u = { _id: data.u._id, username: data.u.username } as IUser;
}
return this.col.insertOne({
_id: new ObjectId().toHexString(),
...data,
});
}
async findLastFailedAttemptByIp(ip: string): Promise<IServerEvent | null> {
return this.col.findOne({
ip,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
}, { sort: { ts: -1 } });
}
async findLastFailedAttemptByUsername(username: string): Promise<IServerEvent | null> {
return this.col.findOne({
'u.username': username,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
}, { sort: { ts: -1 } });
}
async countFailedAttemptsByUsernameSince(username: string, since: Date): Promise<number> {
return this.col.find({
'u.username': username,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
ts: {
$gte: since,
},
}).count();
}
countFailedAttemptsByIpSince(ip: string, since: Date): Promise<number> {
return this.col.find({
ip,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
ts: {
$gte: since,
},
}).count();
}
countFailedAttemptsByIp(ip: string): Promise<number> {
return this.col.find({
ip,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
}).count();
}
countFailedAttemptsByUsername(username: string): Promise<number> {
return this.col.find({
'u.username': username,
t: IServerEventType.FAILED_LOGIN_ATTEMPT,
}).count();
}
}

@ -112,6 +112,15 @@ export class SessionsRaw extends BaseRaw {
]).toArray();
}
async findLastLoginByIp(ip) {
return (await this.col.find({
ip,
}, {
sort: { loginAt: -1 },
limit: 1,
}).toArray())[0];
}
getActiveUsersOfPeriodByDayBetweenDates({ start, end }) {
return this.col.aggregate([
{

@ -11,6 +11,12 @@ export class UsersRaw extends BaseRaw {
return this.find(query, options);
}
findOneByUsername(username, options) {
const query = { username };
return this.findOne(query, options);
}
findUsersInRolesWithQuery(roles, query, options) {
roles = [].concat(roles);

@ -48,6 +48,8 @@ import StatisticsModel from '../models/Statistics';
import { StatisticsRaw } from './Statistics';
import NotificationQueueModel from '../models/NotificationQueue';
import { NotificationQueueRaw } from './NotificationQueue';
import ServerEventModel from '../models/ServerEvents';
import { ServerEventsRaw } from './ServerEvents';
export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection());
export const Roles = new RolesRaw(RolesModel.model.rawCollection());
@ -74,3 +76,4 @@ export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.mo
export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection());
export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection());
export const NotificationQueue = new NotificationQueueRaw(NotificationQueueModel.model.rawCollection());
export const ServerEvents = new ServerEventsRaw(ServerEventModel.model.rawCollection());

@ -149,6 +149,10 @@ Template.loginForm.events({
toastr.error(t('Wait_activation_warning'));
} else if (error.error === 'error-app-user-is-not-allowed-to-login') {
toastr.error(t('App_user_not_allowed_to_login'));
} else if (error.error === 'error-login-blocked-for-ip') {
toastr.error(t('Error_login_blocked_for_ip'));
} else if (error.error === 'error-login-blocked-for-user') {
toastr.error(t('Error_login_blocked_for_user'));
} else {
return toastr.error(t('User_not_found_or_incorrect_password'));
}

@ -0,0 +1,5 @@
export const addMinutesToADate = (date: Date, minutes: number): Date => {
const copy = new Date(date);
copy.setMinutes(copy.getMinutes() + minutes);
return copy;
};

@ -0,0 +1,14 @@
import { IUser } from './IUser';
export enum IServerEventType {
FAILED_LOGIN_ATTEMPT = 'failed-login-attempt',
LOGIN = 'login',
}
export interface IServerEvent {
_id: string;
t: IServerEventType;
ts: Date;
ip: string;
u?: Partial<IUser>;
}

@ -56,6 +56,16 @@
"Accounts_AvatarExternalProviderUrl_Description": "Example: `https://acme.com/api/v1/{username}`",
"Accounts_BlockedDomainsList": "Blocked Domains List",
"Accounts_BlockedDomainsList_Description": "Comma-separated list of blocked domains",
"Block_Multiple_Failed_Logins_Enabled": "Enable collect log in data",
"Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Stores IP and username from log in attempts to a collection on database",
"Block_Multiple_Failed_Logins_Ip_Whitelist": "IP Whitelist",
"Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Comma-separated list of whitelisted IPs",
"Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Time to unblock IP (In Minutes)",
"Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Time to unblock User (In Minutes)",
"Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "How many failed attempts until block by IP",
"Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "How many failed attempts until block by User",
"Block_Multiple_Failed_Logins_By_Ip": "Block failed login attempts by IP",
"Block_Multiple_Failed_Logins_By_User": "Block failed login attempts by Username",
"Accounts_BlockedUsernameList": "Blocked Username List",
"Accounts_BlockedUsernameList_Description": "Comma-separated list of blocked usernames (case-insensitive)",
"Accounts_CustomFields_Description": "Should be a valid JSON where keys are the field names containing a dictionary of field settings. Example:<br/><code>{\n\"role\": {\n\"type\": \"select\",\n\"defaultValue\": \"student\",\n\"options\": [\"teacher\", \"student\"],\n\"required\": true,\n\"modifyRecordField\": {\n\"array\": true,\n\"field\": \"roles\"\n}\n},\n\"twitter\": {\n\"type\": \"text\",\n\"required\": true,\n\"minLength\": 2,\n\"maxLength\": 10\n}\n}</code> ",
@ -79,6 +89,11 @@
"Accounts_Enrollment_Email_Subject_Default": "Welcome to [Site_Name]",
"Accounts_Enrollment_Email": "Enrollment Email",
"Accounts_Enrollment_Email_Description": "You may use the following placeholders: <br/><ul><li>[name], [fname], [lname] for the user's full name, first name or last name, respectively.</li><li>[email] for the user's email.</li><li>[Site_Name] and [Site_URL] for the Application Name and URL respectively.</li></ul>",
"Login_Logs_Enabled": "Log (on console) failed login attempts",
"Login_Logs_ClientIp": "Show Client IP on failed login attempts logs",
"Login_Logs_ForwardedForIp": "Show Forwarded IP on failed login attempts logs",
"Login_Logs_Username": "Show Username on failed login attempts logs",
"Login_Logs_UserAgent": "Show UserAgent on failed login attempts logs",
"Accounts_ForgetUserSessionOnWindowClose": "Forget User Session on Window Close",
"Accounts_Iframe_api_method": "Api Method",
"Accounts_Iframe_api_url": "API URL",
@ -1495,6 +1510,8 @@
"Error_404": "Error:404",
"Error_changing_password": "Error changing password",
"Error_loading_pages": "Error loading pages",
"Error_login_blocked_for_ip": "Login has been temporarily blocked for this IP",
"Error_login_blocked_for_user": "Login has been temporarily blocked for this User",
"Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "Error: Rocket.Chat requires oplog tailing when running in multiple instances",
"Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "Please make sure your MongoDB is on ReplicaSet mode and MONGO_OPLOG_URL environment variable is defined correctly on the application server",
"Error_sending_livechat_transcript": "Error sending Omnichannel transcript",
@ -1533,6 +1550,7 @@
"External_Service": "External Service",
"Facebook_Page": "Facebook Page",
"Failed": "Failed",
"Login_Attempts": "Failed Login Attempts",
"Failed_to_activate_invite_token": "Failed to activate invite token",
"Failed_To_Download_Files": "Failed to download files",
"Failed_to_generate_invite_link": "Failed to generate invite link",
@ -2275,6 +2293,7 @@
"Log_View_Limit": "Log View Limit",
"Logged_out_of_other_clients_successfully": "Logged out of other clients successfully",
"Login": "Login",
"Login_Logs": "Login Logs",
"Login_with": "Login with %s",
"Logistics": "Logistics",
"Logout": "Logout",

@ -51,6 +51,16 @@
"Accounts_AvatarExternalProviderUrl_Description": "Exemplo: `https://acme.com/api/v1/ {username}`",
"Accounts_BlockedDomainsList": "Lista de Domínios Bloqueados",
"Accounts_BlockedDomainsList_Description": "Lista de domínios bloqueados, separados por vírgulas ",
"Block_Multiple_Failed_Logins_Enabled": "Habilitar a coleta de dados do login",
"Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Salva IP e username das tentativas de login em uma coleção no banco de dados",
"Block_Multiple_Failed_Logins_Ip_Whitelist": "Lista de IPs para não verificar",
"Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Lista de IPs separados por vírgula",
"Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Tempo para desbloquear o Usuário (Em Minutos)",
"Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Tempo para desbloquear o IP (Em Minutos)",
"Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Quantas tentativas falhas até bloquear por IP",
"Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Quantas tentativas falhas até bloquear por Usuário",
"Block_Multiple_Failed_Logins_By_User": "Bloquear tentativas falhas de login por Usuário",
"Block_Multiple_Failed_Logins_By_Ip": "Bloquear tentativas falhas de login por IP",
"Accounts_BlockedUsernameList": "Lista de Nomes de Usuário Bloqueados",
"Accounts_BlockedUsernameList_Description": "Lista de nomes de usuários bloqueados, separada por vírgulas (não diferencia maiúsculas)",
"Accounts_CustomFields_Description": "Deve ser um JSON válido onde as chaves são os nomes de campos contendo um dicionário de configuração de campos. Exemplo:<br/><code>{\n\"role\": {\n\"type\": \"select\",\n\"defaultValue\": \"student\",\n\"options\": [\"teacher\", \"student\"],\n\"required\": true,\n\"modifyRecordField\": {\n\"array\": true,\n\"field\": \"roles\"\n}\n},\n\"twitter\": {\n\"type\": \"text\",\n\"required\": true,\n\"minLength\": 2,\n\"maxLength\": 10\n}\n}</code> ",
@ -74,6 +84,12 @@
"Accounts_Enrollment_Email_Subject_Default": "Bem-vindo ao [Site_Name]",
"Accounts_Enrollment_Email": "E-mail de Inscrição",
"Accounts_Enrollment_Email_Description": "Você pode usar os seguintes placeholders: <br/><ul><li> [name], [fname], [lname] para o nome completo do usuário, primeiro nome ou sobrenome, respectivamente. </li><li>[email] para o e-mail do usuário. </li><li>[Site_Name] e [Site_URL] para o Nome da Aplicação e URL, respectivamente.</li></ul>",
"Login_Logs": "Registrar tentativas login",
"Login_Logs_Enabled": "Registrar(no console) tentativas falhas de login",
"Login_Logs_ClientIp": "Mostrar o IP do cliente nos registros de tentativas falhas de login",
"Login_Logs_ForwardedForIp": "Mostrar o 'ForwardedFor' IP nos logs de tentativas falhas de login",
"Login_Logs_Username": "Mostrar o Nome do usuário nos logs de tentativas falhas de login",
"Login_Logs_UserAgent": "Mostrar o 'UserAgent' nos logs de tentativas falhas de login",
"Accounts_ForgetUserSessionOnWindowClose": "Esqueça a sessão do usuário ao fechar a janela",
"Accounts_Iframe_api_method": "Método HTTP da API",
"Accounts_Iframe_api_url": "URL da API",
@ -1363,6 +1379,8 @@
"Error_404": "Erro 404",
"Error_changing_password": "Erro ao alterar senha",
"Error_loading_pages": "Erro ao carregar páginas",
"Error_login_blocked_for_ip": "O login foi temporariamente bloqueado para este IP",
"Error_login_blocked_for_user": "O login foi temporariamente bloqueado para este Usuário",
"Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "Erro: Rocket.Chat requer oplog tailing quando executado em várias instâncias",
"Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "Certifique-se de que seu MongoDB esteja no modo ReplicaSet e a variável de ambiente MONGO_OPLOG_URL esteja definida corretamente no servidor de aplicativos",
"Error_sending_livechat_transcript": "Erro ao enviar transcript do Omnichannel",
@ -1397,6 +1415,7 @@
"External_Queue_Service_URL": "URL do Serviço de Fila Externa",
"External_Service": "Serviço Externo",
"Facebook_Page": "Página do Facebook",
"Login_Attempts": "Tentativas falhas de Login",
"Failed_to_activate_invite_token": "Falha na ativação do token de convite",
"Failed_to_generate_invite_link": "Falha na geração do link de convite",
"Failed_to_validate_invite_token": "Falha na validação do token de convite",

@ -112,3 +112,4 @@ import '../app/action-links/server';
import '../app/reactions/server';
import '../app/livechat/server';
import '../app/custom/server';
import '../app/authentication/server';

@ -4,7 +4,6 @@ import '../imports/startup/server';
import '../lib/RegExp';
import '../ee/server';
import './lib/accounts';
import './lib/pushConfig';
import './lib/roomFiles';
import './startup/migrations';

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Migrations } from '../../../app/migrations/server';
import { Users } from '../../../app/models/server';
import { MAX_RESUME_LOGIN_TOKENS } from '../../lib/accounts';
import { MAX_RESUME_LOGIN_TOKENS } from '../../../app/authentication/server';
Migrations.add({
version: 152,

Loading…
Cancel
Save