Added feature to restrict chat by ip or/and country.

This commit is contained in:
Martin Edenhofer 2018-01-29 00:52:02 +01:00
parent b7d949bf38
commit 5fb6bfed7d
9 changed files with 412 additions and 28 deletions

View file

@ -6,24 +6,24 @@ class App.UiElement.column_select extends App.UiElement.ApplicationUiElement
attribute.multiple = 'multiple'
# build options list based on config
@getConfigOptionList( attribute, params )
@getConfigOptionList(attribute, params)
# build options list based on relation
@getRelationOptionList( attribute, params )
@getRelationOptionList(attribute, params)
# add null selection if needed
@addNullOption( attribute, params )
@addNullOption(attribute, params)
# sort attribute.options
@sortOptions( attribute, params )
@sortOptions(attribute, params)
# find selected/checked item of list
@selectedOptions( attribute, params )
@selectedOptions(attribute, params)
# disable item of list
@disabledOptions( attribute, params )
@disabledOptions(attribute, params)
# filter attributes
@filterOption( attribute, params )
@filterOption(attribute, params)
new App.ColumnSelect( attribute: attribute ).element()
new App.ColumnSelect(attribute: attribute).element()

View file

@ -39,14 +39,27 @@ class App.ColumnSelect extends Spine.Controller
)
render: ->
if !_.isEmpty(@attribute.seperator)
values = []
if @attribute.value
values = @attribute.value.split(';')
else if @attribute.default
values = @attribute.default.split(';')
for value in values
for option in @options.attribute.options
if option.value is value
option.selected = true
@values = []
_.each @options.attribute.options, (option) =>
if option.selected
@values.push option.value.toString()
@html App.view('generic/column_select')
@html App.view('generic/column_select')(
attribute: @options.attribute
values: @values
)
# keep inital height
# disabled for now since controls in modals get rendered hidden
@ -60,13 +73,17 @@ class App.ColumnSelect extends Spine.Controller
@throttledSelect()
select: (value) ->
@selected.find("[data-value='#{value}']").removeClass 'is-hidden'
@pool.find("[data-value='#{value}']").addClass 'is-hidden'
@selected.find("[data-value='#{value}']").removeClass('is-hidden')
@pool.find("[data-value='#{value}']").addClass('is-hidden')
@values.push(value)
@shadow.val(@values)
@shadow.trigger('change')
@placeholder.addClass 'is-hidden'
if !_.isEmpty(@attribute.seperator)
@shadow.val(@values.join(';'))
else
@shadow.val(@values)
@shadow.trigger('change')
@placeholder.addClass('is-hidden')
if @search.val() and @poolOptions.not('.is-filtered').not('.is-hidden').size() is 0
@clear()
@ -76,14 +93,17 @@ class App.ColumnSelect extends Spine.Controller
@throttledRemove()
remove: (value) ->
@pool.find("[data-value='#{value}']").removeClass 'is-hidden'
@selected.find("[data-value='#{value}']").addClass 'is-hidden'
@pool.find("[data-value='#{value}']").removeClass('is-hidden')
@selected.find("[data-value='#{value}']").addClass('is-hidden')
@values.splice(@values.indexOf(value), 1)
@shadow.val(@values)
@shadow.trigger('change')
if !_.isEmpty(@attribute.seperator)
@shadow.val(@values.join(';'))
else
@shadow.val(@values)
@shadow.trigger('change')
if !@values.length
@placeholder.removeClass 'is-hidden'
@placeholder.removeClass('is-hidden')
filter: (event) ->
filter = $(event.currentTarget).val()
@ -92,16 +112,16 @@ class App.ColumnSelect extends Spine.Controller
return if $(el).hasClass('is-hidden')
if $(el).text().toLowerCase().indexOf(filter.toLowerCase()) > -1
$(el).removeClass 'is-filtered'
$(el).removeClass('is-filtered')
else
$(el).addClass 'is-filtered'
$(el).addClass('is-filtered')
@clearButton.toggleClass 'is-hidden', filter.length is 0
clear: ->
@search.val('')
@poolOptions.removeClass 'is-filtered'
@clearButton.addClass 'is-hidden'
@poolOptions.removeClass('is-filtered')
@clearButton.addClass('is-hidden')
onFilterKeydown: (event) ->
return if event.keyCode != 13
@ -111,4 +131,4 @@ class App.ColumnSelect extends Spine.Controller
firstVisibleOption = @poolOptions.not('.is-filtered').not('.is-hidden').first()
if firstVisibleOption
@select firstVisibleOption.attr('data-value')
@select firstVisibleOption.attr('data-value')

View file

@ -1,16 +1,260 @@
class App.Chat extends App.Model
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'note'
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chats'
@countries:
AF: 'Afghanistan'
AL: 'Albania'
DZ: 'Algeria'
AS: 'American Samoa'
AD: 'Andorra'
AO: 'Angola'
AI: 'Anguilla'
AQ: 'Antarctica'
AG: 'Antigua And Barbuda'
AR: 'Argentina'
AM: 'Armenia'
AW: 'Aruba'
AU: 'Australia'
AT: 'Austria'
AZ: 'Azerbaijan'
BS: 'Bahamas'
BH: 'Bahrain'
BD: 'Bangladesh'
BB: 'Barbados'
BY: 'Belarus'
BE: 'Belgium'
BZ: 'Belize'
BJ: 'Benin'
BM: 'Bermuda'
BT: 'Bhutan'
BO: 'Bolivia'
BA: 'Bosnia And Herzegovina'
BW: 'Botswana'
BV: 'Bouvet Island'
BR: 'Brazil'
IO: 'British Indian Ocean Territory'
BN: 'Brunei Darussalam'
BG: 'Bulgaria'
BF: 'Burkina Faso'
BI: 'Burundi'
KH: 'Cambodia'
CM: 'Cameroon'
CA: 'Canada'
CV: 'Cape Verde'
KY: 'Cayman Islands'
CF: 'Central African Republic'
TD: 'Chad'
CL: 'Chile'
CN: 'China'
CX: 'Christmas Island'
CC: 'Cocos (keeling) Islands'
CO: 'Colombia'
KM: 'Comoros'
CG: 'Congo'
CD: 'Congo, The Democratic Republic Of The'
CK: 'Cook Islands'
CR: 'Costa Rica'
CI: 'Cote D\'ivoire'
HR: 'Croatia'
CU: 'Cuba'
CY: 'Cyprus'
CZ: 'Czech Republic'
DK: 'Denmark'
DJ: 'Djibouti'
DM: 'Dominica'
DO: 'Dominican Republic'
TP: 'East Timor'
EC: 'Ecuador'
EG: 'Egypt'
SV: 'El Salvador'
GQ: 'Equatorial Guinea'
ER: 'Eritrea'
EE: 'Estonia'
ET: 'Ethiopia'
FK: 'Falkland Islands (malvinas)'
FO: 'Faroe Islands'
FJ: 'Fiji'
FI: 'Finland'
FR: 'France'
GF: 'French Guiana'
PF: 'French Polynesia'
TF: 'French Southern Territories'
GA: 'Gabon'
GM: 'Gambia'
GE: 'Georgia'
DE: 'Germany'
GH: 'Ghana'
GI: 'Gibraltar'
GR: 'Greece'
GL: 'Greenland'
GD: 'Grenada'
GP: 'Guadeloupe'
GU: 'Guam'
GT: 'Guatemala'
GN: 'Guinea'
GW: 'Guinea-bissau'
GY: 'Guyana'
HT: 'Haiti'
HM: 'Heard Island And Mcdonald Islands'
VA: 'Holy See (vatican City State)'
HN: 'Honduras'
HK: 'Hong Kong'
HU: 'Hungary'
IS: 'Iceland'
IN: 'India'
ID: 'Indonesia'
IR: 'Iran, Islamic Republic Of'
IQ: 'Iraq'
IE: 'Ireland'
IL: 'Israel'
IT: 'Italy'
JM: 'Jamaica'
JP: 'Japan'
JO: 'Jordan'
KZ: 'Kazakstan'
KE: 'Kenya'
KI: 'Kiribati'
KP: 'Korea, Democratic People\'s Republic Of'
KR: 'Korea, Republic Of'
KV: 'Kosovo'
KW: 'Kuwait'
KG: 'Kyrgyzstan'
LA: 'Lao People\'s Democratic Republic'
LV: 'Latvia'
LB: 'Lebanon'
LS: 'Lesotho'
LR: 'Liberia'
LY: 'Libyan Arab Jamahiriya'
LI: 'Liechtenstein'
LT: 'Lithuania'
LU: 'Luxembourg'
MO: 'Macau'
MK: 'Macedonia, The Former Yugoslav Republic Of'
MG: 'Madagascar'
MW: 'Malawi'
MY: 'Malaysia'
MV: 'Maldives'
ML: 'Mali'
MT: 'Malta'
MH: 'Marshall Islands'
MQ: 'Martinique'
MR: 'Mauritania'
MU: 'Mauritius'
YT: 'Mayotte'
MX: 'Mexico'
FM: 'Micronesia, Federated States Of'
MD: 'Moldova, Republic Of'
MC: 'Monaco'
MN: 'Mongolia'
MS: 'Montserrat'
ME: 'Montenegro'
MA: 'Morocco'
MZ: 'Mozambique'
MM: 'Myanmar'
NA: 'Namibia'
NR: 'Nauru'
NP: 'Nepal'
NL: 'Netherlands'
AN: 'Netherlands Antilles'
NC: 'New Caledonia'
NZ: 'New Zealand'
NI: 'Nicaragua'
NE: 'Niger'
NG: 'Nigeria'
NU: 'Niue'
NF: 'Norfolk Island'
MP: 'Northern Mariana Islands'
NO: 'Norway'
OM: 'Oman'
PK: 'Pakistan'
PW: 'Palau'
PS: 'Palestinian Territory, Occupied'
PA: 'Panama'
PG: 'Papua New Guinea'
PY: 'Paraguay'
PE: 'Peru'
PH: 'Philippines'
PN: 'Pitcairn'
PL: 'Poland'
PT: 'Portugal'
PR: 'Puerto Rico'
QA: 'Qatar'
RE: 'Reunion'
RO: 'Romania'
RU: 'Russian Federation'
RW: 'Rwanda'
SH: 'Saint Helena'
KN: 'Saint Kitts And Nevis'
LC: 'Saint Lucia'
PM: 'Saint Pierre And Miquelon'
VC: 'Saint Vincent And The Grenadines'
WS: 'Samoa'
SM: 'San Marino'
ST: 'Sao Tome And Principe'
SA: 'Saudi Arabia'
SN: 'Senegal'
RS: 'Serbia'
SC: 'Seychelles'
SL: 'Sierra Leone'
SG: 'Singapore'
SK: 'Slovakia'
SI: 'Slovenia'
SB: 'Solomon Islands'
SO: 'Somalia'
ZA: 'South Africa'
GS: 'South Georgia And The South Sandwich Islands'
ES: 'Spain'
LK: 'Sri Lanka'
SD: 'Sudan'
SR: 'Suriname'
SJ: 'Svalbard And Jan Mayen'
SZ: 'Swaziland'
SE: 'Sweden'
CH: 'Switzerland'
SY: 'Syrian Arab Republic'
TW: 'Taiwan, Province Of China'
TJ: 'Tajikistan'
TZ: 'Tanzania, United Republic Of'
TH: 'Thailand'
TG: 'Togo'
TK: 'Tokelau'
TO: 'Tonga'
TT: 'Trinidad And Tobago'
TN: 'Tunisia'
TR: 'Turkey'
TM: 'Turkmenistan'
TC: 'Turks And Caicos Islands'
TV: 'Tuvalu'
UG: 'Uganda'
UA: 'Ukraine'
AE: 'United Arab Emirates'
GB: 'United Kingdom'
US: 'United States'
UM: 'United States Minor Outlying Islands'
UY: 'Uruguay'
UZ: 'Uzbekistan'
VU: 'Vanuatu'
VE: 'Venezuela'
VN: 'Viet Nam'
VG: 'Virgin Islands, British'
VI: 'Virgin Islands, U.s.'
WF: 'Wallis And Futuna'
EH: 'Western Sahara'
YE: 'Yemen'
ZM: 'Zambia'
ZW: 'Zimbabwe'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
#{ name: 'public', display: 'Public', tag: 'boolean', default: true },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'block_country', display: 'Blocked contries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]

View file

@ -1,3 +1,6 @@
<% if @attribute.seperator: %>
<input class="js-shadow hide" id="<%= @attribute.id %>" name="<%= @attribute.name %>" value="<%= @attribute.value %>">
<% else: %>
<select
class="columnSelect-shadow js-shadow"
id="<%= @attribute.id %>"
@ -11,6 +14,7 @@
<option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
<% end %>
</select>
<% end %>
<div class="columnSelect-column columnSelect-column--selected js-selected" data-name="<%= @attribute.name %>">
<div class="u-placeholder u-unselectable js-placeholder<%= ' is-hidden' if @values.length %>"><%- @T('Nothing selected') %></div>
<% for option in @attribute.options: %>

View file

@ -264,4 +264,46 @@ optional you can put the max oldest chat sessions as argument
true
end
=begin
check if ip address is blocked for chat
chat = Chat.find(123)
chat.blocked_ip?(ip)
=end
def blocked_ip?(ip)
return false if ip.blank?
return false if block_ip.blank?
ips = block_ip.split(';')
ips.each do |local_ip|
return true if ip == local_ip.strip
return true if ip.match?(/#{local_ip.strip.gsub(/\*/, '.+?')}/)
end
false
end
=begin
check if country is blocked for chat
chat = Chat.find(123)
chat.blocked_country?(ip)
=end
def blocked_country?(ip)
return false if ip.blank?
retunn false if block_country.blank?
geo_ip = Service::GeoIp.location(ip)
return false if geo_ip.blank?
return false if geo_ip['country_code'].blank?
countries = block_country.split(';')
countries.each do |local_country|
return true if geo_ip['country_code'] == local_country
end
false
end
end

View file

@ -467,6 +467,8 @@ class CreateTicket < ActiveRecord::Migration[4.2]
t.string :note, limit: 250, null: true
t.boolean :active, null: false, default: true
t.boolean :public, null: false, default: false
t.string :block_ip, limit: 5000, null: true
t.string :block_country, limit: 5000, null: true
t.string :preferences, limit: 5000, null: true
t.integer :updated_by_id, null: false
t.integer :created_by_id, null: false

View file

@ -0,0 +1,10 @@
class ChatAddIpCountry < ActiveRecord::Migration[5.1]
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :chats, :block_ip, :string, limit: 5000, null: true
add_column :chats, :block_country, :string, limit: 5000, null: true
end
end

View file

@ -3,6 +3,8 @@ class Sessions::Event::ChatStatusCustomer < Sessions::Event::ChatBase
def run
return super if super
return if !check_chat_exists
return if !check_chat_block_by_ip
return if !check_chat_block_by_country
# check if it's a chat sessin reconnect
session_id = nil
@ -32,4 +34,30 @@ class Sessions::Event::ChatStatusCustomer < Sessions::Event::ChatBase
}
end
def check_chat_block_by_ip
chat = current_chat
return true if !chat.blocked_ip?(@remote_ip)
error = {
event: 'chat_error',
data: {
state: 'chat_unavailable',
},
}
Sessions.send(@client_id, error)
false
end
def check_chat_block_by_country
chat = current_chat
return true if !chat.blocked_country?(@remote_ip)
error = {
event: 'chat_error',
data: {
state: 'chat_unavailable',
},
}
Sessions.send(@client_id, error)
false
end
end

View file

@ -5,7 +5,7 @@ class ChatTest < ActiveSupport::TestCase
setup do
groups = Group.all
roles = Role.where( name: %w[Agent] )
roles = Role.where(name: %w[Agent])
@agent1 = User.create_or_update(
login: 'ticket-chat-agent1@example.com',
firstname: 'Notification',
@ -352,4 +352,38 @@ class ChatTest < ActiveSupport::TestCase
travel_back
end
test 'blocked ip test' do
chat = Chat.create!(
name: 'ip test',
max_queue: 5,
note: '',
block_ip: '127.0.0.1;127.0.0.2;127.1.0.*',
active: true,
updated_by_id: 1,
created_by_id: 1,
)
assert_not(chat.blocked_ip?('128.0.0.1'))
assert_not(chat.blocked_ip?('127.0.0.30'))
assert(chat.blocked_ip?('127.0.0.1'))
assert(chat.blocked_ip?('127.0.0.2'))
assert(chat.blocked_ip?('127.1.0.1'))
assert(chat.blocked_ip?('127.1.0.100'))
end
test 'blocked country test' do
chat = Chat.create!(
name: 'country test',
max_queue: 5,
note: '',
block_country: 'AU;CH',
active: true,
updated_by_id: 1,
created_by_id: 1,
)
assert_not(chat.blocked_country?('127.0.0.1'))
assert(chat.blocked_country?('1.1.1.8'))
end
end