diff --git a/app/api/server/api.js b/app/api/server/api.js index 954ffd7e6216..daf430960299 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -25,11 +25,31 @@ let prometheusAPIUserAgent = false; export let API = {}; -const getRequestIP = (req) => - req.headers['x-forwarded-for'] - || (req.connection && req.connection.remoteAddress) - || (req.socket && req.socket.remoteAddress) - || (req.connection && req.connection.socket && req.connection.socket.remoteAddress); +const getRequestIP = (req) => { + const socket = req.socket || req.connection?.socket; + const remoteAddress = req.headers['x-real-ip'] || socket?.remoteAddress || req.connection?.remoteAddress || null; + let forwardedFor = req.headers['x-forwarded-for']; + + if (!socket) { + return remoteAddress || forwardedFor || null; + } + + const httpForwardedCount = parseInt(process.env.HTTP_FORWARDED_COUNT) || 0; + if (httpForwardedCount <= 0) { + return remoteAddress; + } + + if (!_.isString(forwardedFor)) { + return remoteAddress; + } + + forwardedFor = forwardedFor.trim().split(/\s*,\s*/); + if (httpForwardedCount > forwardedFor.length) { + return remoteAddress; + } + + return forwardedFor[forwardedFor.length - httpForwardedCount]; +}; export class APIClass extends Restivus { constructor(properties) { @@ -462,6 +482,8 @@ export class APIClass extends Restivus { const invocation = new DDPCommon.MethodInvocation({ connection: { close() {}, + httpHeaders: this.request.headers, + clientAddress: getRequestIP(this.request), }, }); diff --git a/app/authentication/server/lib/logLoginAttempts.ts b/app/authentication/server/lib/logLoginAttempts.ts index 18fcb3906baa..699c7c00ddd0 100644 --- a/app/authentication/server/lib/logLoginAttempts.ts +++ b/app/authentication/server/lib/logLoginAttempts.ts @@ -15,13 +15,15 @@ export const logFailedLoginAttempts = (login: ILoginAttempt): void => { if (!settings.get('Login_Logs_ClientIp')) { clientAddress = '-'; } - let forwardedFor = connection.httpHeaders['x-forwarded-for']; + let forwardedFor = connection.httpHeaders && connection.httpHeaders['x-forwarded-for']; + let realIp = connection.httpHeaders && connection.httpHeaders['x-real-ip']; if (!settings.get('Login_Logs_ForwardedForIp')) { forwardedFor = '-'; + realIp = '-'; } - let userAgent = connection.httpHeaders['user-agent']; + let userAgent = connection.httpHeaders && 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); + console.log('Failed login detected - Username[%s] ClientAddress[%s] ForwardedFor[%s] XRealIp[%s] UserAgent[%s]', user, clientAddress, forwardedFor, realIp, userAgent); }; diff --git a/app/authentication/server/lib/restrictLoginAttempts.ts b/app/authentication/server/lib/restrictLoginAttempts.ts index d537363d1add..0e1fa7ac237a 100644 --- a/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/app/authentication/server/lib/restrictLoginAttempts.ts @@ -7,6 +7,7 @@ import { IUser } from '../../../../definition/IUser'; import { settings } from '../../../settings/server'; import { addMinutesToADate } from '../../../utils/lib/date.helper'; import Sessions from '../../../models/server/raw/Sessions'; +import { getClientAddress } from '../../../../server/lib/getClientAddress'; export const isValidLoginAttemptByIp = async (ip: string): Promise => { const whitelist = String(settings.get('Block_Multiple_Failed_Logins_Ip_Whitelist')).split(','); @@ -89,7 +90,7 @@ export const saveFailedLoginAttempts = async (login: ILoginAttempt): Promise => { await ServerEvents.insertOne({ - ip: login.connection.clientAddress, + ip: getClientAddress(login.connection), t: IServerEventType.LOGIN, ts: new Date(), u: login.user, diff --git a/app/authentication/server/startup/index.js b/app/authentication/server/startup/index.js index 667fcb240523..338d1ec5055d 100644 --- a/app/authentication/server/startup/index.js +++ b/app/authentication/server/startup/index.js @@ -17,6 +17,7 @@ import { isValidLoginAttemptByIp, } from '../lib/restrictLoginAttempts'; import './settings'; +import { getClientAddress } from '../../../../server/lib/getClientAddress'; Accounts.config({ forbidClientAccountCreation: true, @@ -296,7 +297,7 @@ Accounts.insertUserDoc = _.wrap(Accounts.insertUserDoc, function(insertUserDoc, Accounts.validateLoginAttempt(function(login) { login = callbacks.run('beforeValidateLogin', login); - if (!Promise.await(isValidLoginAttemptByIp(login.connection?.clientAddress))) { + if (!Promise.await(isValidLoginAttemptByIp(getClientAddress(login.connection)))) { throw new Meteor.Error('error-login-blocked-for-ip', 'Login has been temporarily blocked For IP', { function: 'Accounts.validateLoginAttempt', }); diff --git a/app/discussion/client/public/stylesheets/discussion.css b/app/discussion/client/public/stylesheets/discussion.css index b4dc9737f02a..14083f8d0686 100644 --- a/app/discussion/client/public/stylesheets/discussion.css +++ b/app/discussion/client/public/stylesheets/discussion.css @@ -7,17 +7,26 @@ flex-wrap: wrap; } -.discussion-reply-lm { - padding: 4px 8px; +.discussion-reply-lm, +.reply-counter { + padding: 4px; color: var(--color-gray); font-size: 12px; + font-style: italic; flex-grow: 0; flex-shrink: 0; } +.reply-counter { + color: var(--color-dark-light); + + font-weight: 600; + margin-inline-start: 4px; +} + .discussions-list .load-more { text-align: center; text-transform: lowercase; diff --git a/app/livechat/client/views/app/business-hours/BusinessHours.ts b/app/livechat/client/views/app/business-hours/BusinessHours.ts index 8d5fe0401e5f..8252d59ec00d 100644 --- a/app/livechat/client/views/app/business-hours/BusinessHours.ts +++ b/app/livechat/client/views/app/business-hours/BusinessHours.ts @@ -1,33 +1,37 @@ -import { IBusinessHour } from './IBusinessHour'; -import { SingleBusinessHour } from './Single'; +import { IBusinessHourBehavior } from './IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from './Single'; import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour'; class BusinessHoursManager { - private businessHour: IBusinessHour; + private behavior: IBusinessHourBehavior; - constructor(businessHour: IBusinessHour) { - this.setBusinessHourManager(businessHour); + constructor(businessHour: IBusinessHourBehavior) { + this.setBusinessHourBehavior(businessHour); } - setBusinessHourManager(businessHour: IBusinessHour): void { - this.registerBusinessHourMethod(businessHour); + setBusinessHourBehavior(businessHour: IBusinessHourBehavior): void { + this.registerBusinessHourBehavior(businessHour); } - registerBusinessHourMethod(businessHour: IBusinessHour): void { - this.businessHour = businessHour; + registerBusinessHourBehavior(behavior: IBusinessHourBehavior): void { + this.behavior = behavior; } getTemplate(): string { - return this.businessHour.getView(); + return this.behavior.getView(); } - shouldShowCustomTemplate(businessHourData: ILivechatBusinessHour): boolean { - return this.businessHour.shouldShowCustomTemplate(businessHourData); + showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean { + return this.behavior.showCustomTemplate(businessHourData); } - shouldShowBackButton(): boolean { - return this.businessHour.shouldShowBackButton(); + showBackButton(): boolean { + return this.behavior.showBackButton(); + } + + showTimezoneTemplate(): boolean { + return this.behavior.showTimezoneTemplate(); } } -export const businessHourManager = new BusinessHoursManager(new SingleBusinessHour() as IBusinessHour); +export const businessHourManager = new BusinessHoursManager(new SingleBusinessHourBehavior()); diff --git a/app/livechat/client/views/app/business-hours/IBusinessHour.ts b/app/livechat/client/views/app/business-hours/IBusinessHour.ts deleted file mode 100644 index f5a1c97dab10..000000000000 --- a/app/livechat/client/views/app/business-hours/IBusinessHour.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour'; - -export interface IBusinessHour { - getView(): string; - shouldShowCustomTemplate(businessHourData: ILivechatBusinessHour): boolean; - shouldShowBackButton(): boolean; -} diff --git a/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts b/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts new file mode 100644 index 000000000000..1909f9510d86 --- /dev/null +++ b/app/livechat/client/views/app/business-hours/IBusinessHourBehavior.ts @@ -0,0 +1,8 @@ +import { ILivechatBusinessHour } from '../../../../../../definition/ILivechatBusinessHour'; + +export interface IBusinessHourBehavior { + getView(): string; + showCustomTemplate(businessHourData: ILivechatBusinessHour): boolean; + showBackButton(): boolean; + showTimezoneTemplate(): boolean; +} diff --git a/app/livechat/client/views/app/business-hours/Single.ts b/app/livechat/client/views/app/business-hours/Single.ts index 4dc41b05cb95..2841a27d7b79 100644 --- a/app/livechat/client/views/app/business-hours/Single.ts +++ b/app/livechat/client/views/app/business-hours/Single.ts @@ -1,15 +1,19 @@ -import { IBusinessHour } from './IBusinessHour'; +import { IBusinessHourBehavior } from './IBusinessHourBehavior'; -export class SingleBusinessHour implements IBusinessHour { +export class SingleBusinessHourBehavior implements IBusinessHourBehavior { getView(): string { return 'livechatBusinessHoursForm'; } - shouldShowCustomTemplate(): boolean { + showCustomTemplate(): boolean { return false; } - shouldShowBackButton(): boolean { + showBackButton(): boolean { + return false; + } + + showTimezoneTemplate(): boolean { return false; } } diff --git a/app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html b/app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html index a212fed851e5..0fc164fee9ff 100644 --- a/app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html +++ b/app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html @@ -1,6 +1,9 @@