diff --git a/client/.eslintrc b/client/.eslintrc index aa104244..d5d5955b 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -48,6 +48,7 @@ "camelcase": "off", "no-console": ["warn", { "allow": ["warn", "error"] }], "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], - "import/prefer-default-export": "off" + "import/prefer-default-export": "off", + "no-alert": "off" } } diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 9e11009a..961b47c7 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -458,5 +458,9 @@ "check_reason": "Reason: {{reason}}", "check_rule": "Rule: {{rule}}", "check_service": "Service name: {{service}}", - "check_not_found": "Doesn't exist in any filter" + "check_not_found": "Doesn't exist in any filter", + "client_confirm_block": "Are you sure you want to block the client \"{{ip}}\"?", + "client_confirm_unblock": "Are you sure you want to unblock the client \"{{ip}}\"?", + "client_blocked": "Client \"{{ip}}\" successfully blocked", + "client_unblocked": "Client \"{{ip}}\" successfully unblocked" } diff --git a/client/src/actions/access.js b/client/src/actions/access.js index 5b5272d7..1f51cea7 100644 --- a/client/src/actions/access.js +++ b/client/src/actions/access.js @@ -1,7 +1,11 @@ import { createAction } from 'redux-actions'; +import { t } from 'i18next'; + import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './index'; +import { getStats, getStatsConfig } from './stats'; import { normalizeTextarea } from '../helpers/helpers'; +import { ACTION } from '../helpers/constants'; export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST'); export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE'); @@ -28,9 +32,9 @@ export const setAccessList = config => async (dispatch) => { const { allowed_clients, disallowed_clients, blocked_hosts } = config; const values = { - allowed_clients: (allowed_clients && normalizeTextarea(allowed_clients)) || [], - disallowed_clients: (disallowed_clients && normalizeTextarea(disallowed_clients)) || [], - blocked_hosts: (blocked_hosts && normalizeTextarea(blocked_hosts)) || [], + allowed_clients: normalizeTextarea(allowed_clients), + disallowed_clients: normalizeTextarea(disallowed_clients), + blocked_hosts: normalizeTextarea(blocked_hosts), }; await apiClient.setAccessList(values); @@ -41,3 +45,43 @@ export const setAccessList = config => async (dispatch) => { dispatch(setAccessListFailure()); } }; + +export const toggleClientBlockRequest = createAction('TOGGLE_CLIENT_BLOCK_REQUEST'); +export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE'); +export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS'); + +export const toggleClientBlock = (type, ip) => async (dispatch, getState) => { + dispatch(toggleClientBlockRequest()); + try { + const { allowed_clients, disallowed_clients, blocked_hosts } = getState().access; + let updatedDisallowedClients = normalizeTextarea(disallowed_clients); + + if (type === ACTION.unblock && updatedDisallowedClients.includes(ip)) { + updatedDisallowedClients = updatedDisallowedClients.filter(client => client !== ip); + } else if (type === ACTION.block && !updatedDisallowedClients.includes(ip)) { + updatedDisallowedClients.push(ip); + } + + const values = { + allowed_clients: normalizeTextarea(allowed_clients), + blocked_hosts: normalizeTextarea(blocked_hosts), + disallowed_clients: updatedDisallowedClients, + }; + + await apiClient.setAccessList(values); + dispatch(toggleClientBlockSuccess()); + + if (type === ACTION.unblock) { + dispatch(addSuccessToast(t('client_unblocked', { ip }))); + } else if (type === ACTION.block) { + dispatch(addSuccessToast(t('client_blocked', { ip }))); + } + + dispatch(getStats()); + dispatch(getStatsConfig()); + dispatch(getAccessList()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleClientBlockFailure()); + } +}; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index b3e4d2bb..0d212b1b 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -289,12 +289,8 @@ export const setUpstream = config => async (dispatch) => { dispatch(setUpstreamRequest()); try { const values = { ...config }; - values.bootstrap_dns = ( - values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns) - ) || []; - values.upstream_dns = ( - values.upstream_dns && normalizeTextarea(values.upstream_dns) - ) || []; + values.bootstrap_dns = normalizeTextarea(values.bootstrap_dns); + values.upstream_dns = normalizeTextarea(values.upstream_dns); await apiClient.setUpstream(values); dispatch(addSuccessToast('updated_upstream_dns_toast')); @@ -313,12 +309,8 @@ export const testUpstream = config => async (dispatch) => { dispatch(testUpstreamRequest()); try { const values = { ...config }; - values.bootstrap_dns = ( - values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns) - ) || []; - values.upstream_dns = ( - values.upstream_dns && normalizeTextarea(values.upstream_dns) - ) || []; + values.bootstrap_dns = normalizeTextarea(values.bootstrap_dns); + values.upstream_dns = normalizeTextarea(values.upstream_dns); const upstreamResponse = await apiClient.testUpstream(values); const testMessages = Object.keys(upstreamResponse).map((key) => { diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js index 25897aab..7a12b203 100644 --- a/client/src/actions/stats.js +++ b/client/src/actions/stats.js @@ -2,7 +2,7 @@ import { createAction } from 'redux-actions'; import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './index'; -import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; +import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo, addClientStatus } from '../helpers/helpers'; export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST'); export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE'); @@ -46,12 +46,15 @@ export const getStats = () => async (dispatch) => { const normalizedTopClients = normalizeTopStats(stats.top_clients); const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); const clients = await apiClient.findClients(clientsParams); + const accessData = await apiClient.getAccessList(); + const { disallowed_clients } = accessData; const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); + const topClientsWithStatus = addClientStatus(topClientsWithInfo, disallowed_clients, 'name'); const normalizedStats = { ...stats, top_blocked_domains: normalizeTopStats(stats.top_blocked_domains), - top_clients: topClientsWithInfo, + top_clients: topClientsWithStatus, top_queried_domains: normalizeTopStats(stats.top_queried_domains), avg_processing_time: secondsToMilliseconds(stats.avg_processing_time), }; diff --git a/client/src/components/Dashboard/BlockedDomains.js b/client/src/components/Dashboard/BlockedDomains.js index 42288ca8..3823d52d 100644 --- a/client/src/components/Dashboard/BlockedDomains.js +++ b/client/src/components/Dashboard/BlockedDomains.js @@ -58,7 +58,7 @@ const BlockedDomains = ({ noDataText={t('no_domains_found')} minRows={6} defaultPageSize={100} - className="-striped -highlight card-table-overflow stats__table" + className="-highlight card-table-overflow stats__table" /> ); diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index e83addcb..04c3270b 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import ReactTable from 'react-table'; import PropTypes from 'prop-types'; import { Trans, withNamespaces } from 'react-i18next'; @@ -28,17 +28,58 @@ const countCell = dnsQueries => return ; }; -const clientCell = t => +const renderBlockingButton = (blocked, ip, handleClick, processing) => { + let buttonProps = { + className: 'btn-outline-danger', + text: 'block_btn', + type: 'block', + }; + + if (blocked) { + buttonProps = { + className: 'btn-outline-secondary', + text: 'unblock_btn', + type: 'unblock', + }; + } + + return ( +
+ +
+ ); +}; + +const clientCell = (t, toggleClientStatus, processing) => function cell(row) { + const { original, value } = row; + const { blocked } = original; + return ( -
- {formatClientCell(row, t)} -
+ +
+ {formatClientCell(row, t)} +
+ {renderBlockingButton(blocked, value, toggleClientStatus, processing)} +
); }; const Clients = ({ - t, refreshButton, topClients, subtitle, dnsQueries, + t, + refreshButton, + topClients, + subtitle, + dnsQueries, + toggleClientStatus, + processingAccessSet, }) => ( ({ + data={topClients.map(({ + name: ip, count, info, blocked, + }) => ({ ip, count, info, + blocked, }))} columns={[ { @@ -58,7 +102,7 @@ const Clients = ({ accessor: 'ip', sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10), - Cell: clientCell(t), + Cell: clientCell(t, toggleClientStatus, processingAccessSet), }, { Header: requests_count, @@ -72,7 +116,24 @@ const Clients = ({ noDataText={t('no_clients_found')} minRows={6} defaultPageSize={100} - className="-striped -highlight card-table-overflow" + className="-highlight card-table-overflow clients__table" + getTrProps={(_state, rowInfo) => { + if (!rowInfo) { + return {}; + } + + const { blocked } = rowInfo.original; + + if (blocked) { + return { + className: 'red', + }; + } + + return { + className: '', + }; + }} /> ); @@ -85,6 +146,8 @@ Clients.propTypes = { autoClients: PropTypes.array.isRequired, subtitle: PropTypes.string.isRequired, t: PropTypes.func.isRequired, + toggleClientStatus: PropTypes.func.isRequired, + processingAccessSet: PropTypes.bool.isRequired, }; export default withNamespaces()(Clients); diff --git a/client/src/components/Dashboard/QueriedDomains.js b/client/src/components/Dashboard/QueriedDomains.js index 0058ed64..85c39cfb 100644 --- a/client/src/components/Dashboard/QueriedDomains.js +++ b/client/src/components/Dashboard/QueriedDomains.js @@ -59,7 +59,7 @@ const QueriedDomains = ({ noDataText={t('no_domains_found')} minRows={6} defaultPageSize={100} - className="-striped -highlight card-table-overflow stats__table" + className="-highlight card-table-overflow stats__table" /> ); diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 2f871720..01ce0448 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -10,6 +10,7 @@ import BlockedDomains from './BlockedDomains'; import PageTitle from '../ui/PageTitle'; import Loading from '../ui/Loading'; +import { ACTION } from '../../helpers/constants'; import './Dashboard.css'; class Dashboard extends Component { @@ -39,9 +40,20 @@ class Dashboard extends Component { ); }; + toggleClientStatus = (type, ip) => { + const confirmMessage = type === ACTION.block ? 'client_confirm_block' : 'client_confirm_unblock'; + + if (window.confirm(this.props.t(confirmMessage, { ip }))) { + this.props.toggleClientBlock(type, ip); + } + }; + render() { - const { dashboard, stats, t } = this.props; - const statsProcessing = stats.processingStats || stats.processingGetConfig; + const { + dashboard, stats, access, t, + } = this.props; + const statsProcessing = stats.processingStats + || stats.processingGetConfig; const subtitle = stats.interval === 1 @@ -116,6 +128,8 @@ class Dashboard extends Component { clients={dashboard.clients} autoClients={dashboard.autoClients} refreshButton={refreshButton} + toggleClientStatus={this.toggleClientStatus} + processingAccessSet={access.processingSet} />
@@ -146,11 +160,14 @@ class Dashboard extends Component { Dashboard.propTypes = { dashboard: PropTypes.object.isRequired, stats: PropTypes.object.isRequired, + access: PropTypes.object.isRequired, getStats: PropTypes.func.isRequired, getStatsConfig: PropTypes.func.isRequired, toggleProtection: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired, t: PropTypes.func.isRequired, + toggleClientBlock: PropTypes.func.isRequired, + getAccessList: PropTypes.func.isRequired, }; export default withNamespaces()(Dashboard); diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 9b2c8b21..4eb4a0fb 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -61,9 +61,10 @@ margin-right: 5px; } -.logs__action { +.logs__action, +.table__action { position: absolute; - top: 10px; + top: 11px; right: 15px; background-color: #fff; border-radius: 4px; @@ -72,11 +73,13 @@ opacity: 0; } -.logs__table .rt-td { +.logs__table .rt-td, +.clients__table .rt-td { position: relative; } -.logs__table .rt-tr:hover .logs__action { +.logs__table .rt-tr:hover .logs__action, +.clients__table .rt-tr:hover .table__action { visibility: visible; opacity: 1; } diff --git a/client/src/containers/Dashboard.js b/client/src/containers/Dashboard.js index 8d40df18..2b2dce78 100644 --- a/client/src/containers/Dashboard.js +++ b/client/src/containers/Dashboard.js @@ -1,11 +1,12 @@ import { connect } from 'react-redux'; import { toggleProtection, getClients } from '../actions'; import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats'; +import { toggleClientBlock, getAccessList } from '../actions/access'; import Dashboard from '../components/Dashboard'; const mapStateToProps = (state) => { - const { dashboard, stats } = state; - const props = { dashboard, stats }; + const { dashboard, stats, access } = state; + const props = { dashboard, stats, access }; return props; }; @@ -15,6 +16,8 @@ const mapDispatchToProps = { getStats, getStatsConfig, setStatsConfig, + toggleClientBlock, + getAccessList, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 1f1ce1a4..5920552c 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -456,3 +456,8 @@ export const DETAILED_DATE_FORMAT_OPTIONS = { }; export const CUSTOM_FILTERING_RULES_ID = 0; + +export const ACTION = { + block: 'block', + unblock: 'unblock', +}; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 795ec054..e748463e 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -122,6 +122,17 @@ export const addClientInfo = (data, clients, param) => ( }) ); +export const addClientStatus = (data, disallowedClients, param) => ( + data.map((row) => { + const clientIp = row[param]; + const blocked = !!(disallowedClients && disallowedClients.includes(clientIp)); + return { + ...row, + blocked, + }; + }) +); + export const normalizeFilteringStatus = (filteringStatus) => { const { enabled, filters, user_rules: userRules, interval, @@ -275,7 +286,13 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => { } }; -export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n); +export const normalizeTextarea = (text) => { + if (!text) { + return []; + } + + return text.replace(/[;, ]/g, '\n').split('\n').filter(n => n); +}; /** * Normalizes the topClients array diff --git a/client/src/reducers/access.js b/client/src/reducers/access.js index b8fc14ed..3ff517cf 100644 --- a/client/src/reducers/access.js +++ b/client/src/reducers/access.js @@ -31,6 +31,10 @@ const access = handleActions( }; return newState; }, + + [actions.toggleClientBlockRequest]: state => ({ ...state, processingSet: true }), + [actions.toggleClientBlockFailure]: state => ({ ...state, processingSet: false }), + [actions.toggleClientBlockSuccess]: state => ({ ...state, processingSet: false }), }, { processing: true,