From e95aae5744ffd1de0809c95a8df50ea3f5f1af28 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Mon, 22 Jul 2019 15:32:12 +0300 Subject: [PATCH] + client: handle DNS rewrites --- client/package-lock.json | 86 +++++++++--------- client/package.json | 5 +- client/src/__locales/en.json | 17 +++- client/src/actions/rewrites.js | 58 ++++++++++++ client/src/api/Api.js | 28 ++++++ client/src/components/Logs/Logs.css | 6 ++ client/src/components/Logs/index.js | 28 +++++- .../components/Settings/Dns/Rewrites/Form.js | 89 +++++++++++++++++++ .../components/Settings/Dns/Rewrites/Modal.js | 52 +++++++++++ .../components/Settings/Dns/Rewrites/Table.js | 87 ++++++++++++++++++ .../components/Settings/Dns/Rewrites/index.js | 83 +++++++++++++++++ client/src/components/Settings/Dns/index.js | 19 ++++ client/src/components/ui/Card.js | 3 +- client/src/components/ui/ReactTable.css | 4 + client/src/containers/Dns.js | 15 +++- client/src/helpers/constants.js | 2 + client/src/helpers/form.js | 48 +++++++--- client/src/reducers/index.js | 2 + client/src/reducers/rewrites.js | 50 +++++++++++ client/webpack.dev.js | 19 ++-- 20 files changed, 631 insertions(+), 70 deletions(-) create mode 100644 client/src/actions/rewrites.js create mode 100644 client/src/components/Settings/Dns/Rewrites/Form.js create mode 100644 client/src/components/Settings/Dns/Rewrites/Modal.js create mode 100644 client/src/components/Settings/Dns/Rewrites/Table.js create mode 100644 client/src/components/Settings/Dns/Rewrites/index.js create mode 100644 client/src/reducers/rewrites.js diff --git a/client/package-lock.json b/client/package-lock.json index 3edaa670..cedee41d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -945,12 +945,27 @@ } }, "axios": { - "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "requires": { - "follow-redirects": "^1.3.0", - "is-buffer": "^1.1.5" + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } } }, "axobject-query": { @@ -5124,6 +5139,7 @@ "version": "1.5.7", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz", "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==", + "dev": true, "requires": { "debug": "^3.1.0" } @@ -6933,7 +6949,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -7386,9 +7403,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash-es": { "version": "4.17.10", @@ -7767,9 +7784,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -10191,6 +10208,14 @@ } } }, + "react-router-hash-link": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-1.2.2.tgz", + "integrity": "sha512-LBthLVHdqPeKDVt3+cFRhy15Z7veikOvdKRZRfyBR2vjqIE7rxn+tKLjb6DOmLm6JpoQVemVDnxQ35RVnEHdQA==", + "requires": { + "prop-types": "^15.6.0" + } + }, "react-table": { "version": "6.8.6", "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz", @@ -10848,9 +10873,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -12478,38 +12503,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "uniq": { diff --git a/client/package.json b/client/package.json index 05d91b7a..dc64d590 100644 --- a/client/package.json +++ b/client/package.json @@ -10,13 +10,13 @@ }, "dependencies": { "@nivo/line": "^0.49.1", - "axios": "^0.18.0", + "axios": "^0.18.1", "classnames": "^2.2.6", "date-fns": "^1.29.0", "file-saver": "^1.3.8", "i18next": "^12.0.0", "i18next-browser-languagedetector": "^2.2.3", - "lodash": "^4.17.11", + "lodash": "^4.17.15", "nanoid": "^1.2.3", "prop-types": "^15.6.1", "react": "^16.4.0", @@ -27,6 +27,7 @@ "react-redux": "^5.0.7", "react-redux-loading-bar": "^4.0.7", "react-router-dom": "^4.2.2", + "react-router-hash-link": "^1.2.2", "react-table": "^6.8.6", "react-transition-group": "^2.4.0", "redux": "^4.0.0", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 78f8e94f..8fc269ca 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -331,5 +331,18 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy supports <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox supports <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "You will find more implementations <0>here and <1>here.", - "setup_dns_notice": "In order to use <1>DNS-over-HTTPS or <1>DNS-over-TLS, you need to <0>configure Encryption in AdGuard Home settings." -} \ No newline at end of file + "setup_dns_notice": "In order to use <1>DNS-over-HTTPS or <1>DNS-over-TLS, you need to <0>configure Encryption in AdGuard Home settings.", + "rewrite_added": "DNS rewrite for \"{{key}}\" successfully added", + "rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted", + "rewrite_add": "Add DNS rewrite", + "rewrite_not_found": "No DNS rewrites found", + "rewrite_confirm_delete": "Are you sure you want to delete DNS rewrite for \"{{key}}\"?", + "rewrite_desc": "Allows to easily configure custom DNS response for a specific domain name.", + "rewrite_applied": "Applied Rewrite rule", + "dns_rewrites": "DNS rewrites", + "form_domain": "Enter domain", + "form_answer": "Enter IP address or domain name", + "form_error_domain_format": "Invalid domain format", + "form_error_answer_format": "Invalid answer format", + "configure": "Configure" +} diff --git a/client/src/actions/rewrites.js b/client/src/actions/rewrites.js new file mode 100644 index 00000000..df846fdd --- /dev/null +++ b/client/src/actions/rewrites.js @@ -0,0 +1,58 @@ +import { createAction } from 'redux-actions'; +import { t } from 'i18next'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast } from './index'; + +const apiClient = new Api(); + +export const toggleRewritesModal = createAction('TOGGLE_REWRITES_MODAL'); + +export const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST'); +export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE'); +export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS'); + +export const getRewritesList = () => async (dispatch) => { + dispatch(getRewritesListRequest()); + try { + const data = await apiClient.getRewritesList(); + dispatch(getRewritesListSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getRewritesListFailure()); + } +}; + +export const addRewriteRequest = createAction('ADD_REWRITE_REQUEST'); +export const addRewriteFailure = createAction('ADD_REWRITE_FAILURE'); +export const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS'); + +export const addRewrite = config => async (dispatch) => { + dispatch(addRewriteRequest()); + try { + await apiClient.addRewrite(config); + dispatch(addRewriteSuccess(config)); + dispatch(toggleRewritesModal()); + dispatch(getRewritesList()); + dispatch(addSuccessToast(t('rewrite_added', { key: config.domain }))); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addRewriteFailure()); + } +}; + +export const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST'); +export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE'); +export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS'); + +export const deleteRewrite = config => async (dispatch) => { + dispatch(deleteRewriteRequest()); + try { + await apiClient.deleteRewrite(config); + dispatch(deleteRewriteSuccess()); + dispatch(getRewritesList()); + dispatch(addSuccessToast(t('rewrite_deleted', { key: config.domain }))); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(deleteRewriteFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 76b17888..766cd499 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -481,4 +481,32 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // DNS rewrites + REWRITES_LIST = { path: 'rewrite/list', method: 'GET' }; + REWRITE_ADD = { path: 'rewrite/add', method: 'POST' }; + REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' }; + + getRewritesList() { + const { path, method } = this.REWRITES_LIST; + return this.makeRequest(path, method); + } + + addRewrite(config) { + const { path, method } = this.REWRITE_ADD; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + deleteRewrite(config) { + const { path, method } = this.REWRITE_DELETE; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 1b39f592..3205e424 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -13,6 +13,12 @@ overflow: hidden; } +.logs__row--column { + flex-direction: column; + align-items: flex-start; + overflow: hidden; +} + .logs__row .list-unstyled { margin-bottom: 0; overflow: hidden; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 8a675a05..f8891206 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -5,6 +5,7 @@ import { saveAs } from 'file-saver/FileSaver'; import escapeRegExp from 'lodash/escapeRegExp'; import endsWith from 'lodash/endsWith'; import { Trans, withNamespaces } from 'react-i18next'; +import { HashLink as Link } from 'react-router-hash-link'; import { formatTime, getClientName } from '../../helpers/helpers'; import { getTrackerData } from '../../helpers/trackers/trackers'; @@ -125,6 +126,7 @@ class Logs extends Component { const rule = row && row.original && row.original.rule; const { filterId } = row.original; const { filters } = this.props.filtering; + const isRewrite = reason && reason === 'Rewrite'; let filterName = ''; if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') { @@ -161,14 +163,16 @@ class Logs extends Component { const isRenderTooltip = reason === 'NotFilteredWhiteList'; return ( -
+
+ {isRewrite && rewrite_applied}
    {liNodes}
{this.renderTooltip(isRenderTooltip, rule, filterName)}
); } return ( -
+
+ {isRewrite && rewrite_applied} empty_response_status {this.renderTooltip(isFiltered, rule, filterName)}
@@ -197,6 +201,7 @@ class Logs extends Component { Cell: (row) => { const { reason } = row.original; const isFiltered = row ? reason.indexOf('Filtered') === 0 : false; + const isRewrite = reason && reason === 'Rewrite'; const clientName = getClientName(dashboard.clients, row.value) || getClientName(dashboard.autoClients, row.value); let client; @@ -207,6 +212,21 @@ class Logs extends Component { client = row.value; } + if (isRewrite) { + return ( + +
+ {client} +
+
+ + configure + +
+
+ ); + } + return (
@@ -261,6 +281,10 @@ class Logs extends Component { return { className: 'green', }; + } else if (rowInfo.original.reason === 'Rewrite') { + return { + className: 'blue', + }; } return { diff --git a/client/src/components/Settings/Dns/Rewrites/Form.js b/client/src/components/Settings/Dns/Rewrites/Form.js new file mode 100644 index 00000000..2d6b27a3 --- /dev/null +++ b/client/src/components/Settings/Dns/Rewrites/Form.js @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, withNamespaces } from 'react-i18next'; +import flow from 'lodash/flow'; + +import { renderField, required, domain, answer } from '../../../../helpers/form'; + +const Form = (props) => { + const { + t, + handleSubmit, + reset, + pristine, + submitting, + toggleRewritesModal, + processingAdd, + } = props; + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ ); +}; + +Form.propTypes = { + pristine: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + toggleRewritesModal: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + processingAdd: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ + form: 'rewritesForm', + enableReinitialize: true, + }), +])(Form); diff --git a/client/src/components/Settings/Dns/Rewrites/Modal.js b/client/src/components/Settings/Dns/Rewrites/Modal.js new file mode 100644 index 00000000..eba58bb9 --- /dev/null +++ b/client/src/components/Settings/Dns/Rewrites/Modal.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; +import ReactModal from 'react-modal'; + +import Form from './Form'; + +const Modal = (props) => { + const { + isModalOpen, + handleSubmit, + toggleRewritesModal, + processingAdd, + processingDelete, + } = props; + + return ( + toggleRewritesModal()} + > +
+
+

+ Add DNS rewrite +

+ +
+
+
+
+ ); +}; + +Modal.propTypes = { + isModalOpen: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + toggleRewritesModal: PropTypes.func.isRequired, + processingAdd: PropTypes.bool.isRequired, + processingDelete: PropTypes.bool.isRequired, +}; + +export default withNamespaces()(Modal); diff --git a/client/src/components/Settings/Dns/Rewrites/Table.js b/client/src/components/Settings/Dns/Rewrites/Table.js new file mode 100644 index 00000000..78d5489c --- /dev/null +++ b/client/src/components/Settings/Dns/Rewrites/Table.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactTable from 'react-table'; +import { withNamespaces } from 'react-i18next'; + +class Table extends Component { + cellWrap = ({ value }) => ( +
+ + {value} + +
+ ); + + columns = [ + { + Header: 'Domain', + accessor: 'domain', + Cell: this.cellWrap, + }, + { + Header: 'Answer', + accessor: 'answer', + Cell: this.cellWrap, + }, + { + Header: this.props.t('actions_table_header'), + accessor: 'actions', + maxWidth: 100, + Cell: value => ( +
+ +
+ ), + }, + ]; + + render() { + const { + t, list, processing, processingAdd, processingDelete, + } = this.props; + + return ( + + ); + } +} + +Table.propTypes = { + t: PropTypes.func.isRequired, + list: PropTypes.array.isRequired, + processing: PropTypes.bool.isRequired, + processingAdd: PropTypes.bool.isRequired, + processingDelete: PropTypes.bool.isRequired, + handleDelete: PropTypes.func.isRequired, +}; + +export default withNamespaces()(Table); diff --git a/client/src/components/Settings/Dns/Rewrites/index.js b/client/src/components/Settings/Dns/Rewrites/index.js new file mode 100644 index 00000000..e4e0b193 --- /dev/null +++ b/client/src/components/Settings/Dns/Rewrites/index.js @@ -0,0 +1,83 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; + +import Table from './Table'; +import Modal from './Modal'; +import Card from '../../../ui/Card'; + +class Rewrites extends Component { + handleSubmit = (values) => { + this.props.addRewrite(values); + }; + + handleDelete = (values) => { + // eslint-disable-next-line no-alert + if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) { + this.props.deleteRewrite(values); + } + }; + + render() { + const { + t, + rewrites, + toggleRewritesModal, + } = this.props; + + const { + list, + isModalOpen, + processing, + processingAdd, + processingDelete, + } = rewrites; + + return ( + + + + + + + + + + ); + } +} + +Rewrites.propTypes = { + t: PropTypes.func.isRequired, + getRewritesList: PropTypes.func.isRequired, + toggleRewritesModal: PropTypes.func.isRequired, + addRewrite: PropTypes.func.isRequired, + deleteRewrite: PropTypes.func.isRequired, + rewrites: PropTypes.object.isRequired, +}; + +export default withNamespaces()(Rewrites); diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js index 2b038b1f..cb9c9e4a 100644 --- a/client/src/components/Settings/Dns/index.js +++ b/client/src/components/Settings/Dns/index.js @@ -4,12 +4,14 @@ import { withNamespaces } from 'react-i18next'; import Upstream from './Upstream'; import Access from './Access'; +import Rewrites from './Rewrites'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; class Dns extends Component { componentDidMount() { this.props.getAccessList(); + this.props.getRewritesList(); } render() { @@ -18,9 +20,14 @@ class Dns extends Component { dashboard, settings, access, + rewrites, setAccessList, testUpstream, setUpstream, + getRewritesList, + addRewrite, + deleteRewrite, + toggleRewritesModal, } = this.props; return ( @@ -39,6 +46,13 @@ class Dns extends Component { testUpstream={testUpstream} /> + )} @@ -54,6 +68,11 @@ Dns.propTypes = { getAccessList: PropTypes.func.isRequired, setAccessList: PropTypes.func.isRequired, access: PropTypes.object.isRequired, + rewrites: PropTypes.object.isRequired, + getRewritesList: PropTypes.func.isRequired, + addRewrite: PropTypes.func.isRequired, + deleteRewrite: PropTypes.func.isRequired, + toggleRewritesModal: PropTypes.func.isRequired, t: PropTypes.func.isRequired, }; diff --git a/client/src/components/ui/Card.js b/client/src/components/ui/Card.js index 01a06dac..65c15487 100644 --- a/client/src/components/ui/Card.js +++ b/client/src/components/ui/Card.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import './Card.css'; const Card = props => ( -
+
{props.title &&
@@ -30,6 +30,7 @@ const Card = props => ( ); Card.propTypes = { + id: PropTypes.string, title: PropTypes.string, subtitle: PropTypes.string, bodyType: PropTypes.string, diff --git a/client/src/components/ui/ReactTable.css b/client/src/components/ui/ReactTable.css index e1de27b4..48a35dc7 100644 --- a/client/src/components/ui/ReactTable.css +++ b/client/src/components/ui/ReactTable.css @@ -15,3 +15,7 @@ .rt-tr-group .green { background-color: #f1faf3; } + +.rt-tr-group .blue { + background-color: #ecf7ff; +} diff --git a/client/src/containers/Dns.js b/client/src/containers/Dns.js index aa3c78ad..4f245c1a 100644 --- a/client/src/containers/Dns.js +++ b/client/src/containers/Dns.js @@ -1,14 +1,23 @@ import { connect } from 'react-redux'; import { handleUpstreamChange, setUpstream, testUpstream } from '../actions'; import { getAccessList, setAccessList } from '../actions/access'; +import { + getRewritesList, + addRewrite, + deleteRewrite, + toggleRewritesModal, +} from '../actions/rewrites'; import Dns from '../components/Settings/Dns'; const mapStateToProps = (state) => { - const { dashboard, settings, access } = state; + const { + dashboard, settings, access, rewrites, + } = state; const props = { dashboard, settings, access, + rewrites, }; return props; }; @@ -19,6 +28,10 @@ const mapDispatchToProps = { testUpstream, getAccessList, setAccessList, + getRewritesList, + addRewrite, + deleteRewrite, + toggleRewritesModal, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 9faf2e5f..896d873e 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -1,5 +1,7 @@ export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/; +export const R_HOST = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$/; export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g; +export const R_IPV6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/g; export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g; export const STATS_NAMES = { diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index 39c4b7aa..c4ffcb73 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,10 +1,16 @@ import React, { Fragment } from 'react'; import { Trans } from 'react-i18next'; -import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants'; +import { R_IPV4, R_MAC, R_HOST, R_IPV6, UNSAFE_PORTS } from '../helpers/constants'; export const renderField = ({ - input, id, className, placeholder, type, disabled, meta: { touched, error }, + input, + id, + className, + placeholder, + type, + disabled, + meta: { touched, error }, }) => ( - {!disabled && touched && (error && {error})} + {!disabled && + touched && + (error && {error})} ); @@ -24,20 +32,17 @@ export const renderSelectField = ({ }) => ( - {!disabled && touched && (error && {error})} + {!disabled && + touched && + (error && {error})} ); @@ -63,7 +68,7 @@ export const mac = (value) => { }; export const isPositive = (value) => { - if ((value || value === 0) && (value <= 0)) { + if ((value || value === 0) && value <= 0) { return form_error_positive; } return false; @@ -92,4 +97,23 @@ export const isSafePort = (value) => { return false; }; +export const domain = (value) => { + if (value && !new RegExp(R_HOST).test(value)) { + return form_error_domain_format; + } + return false; +}; + +export const answer = (value) => { + if ( + value && + (!new RegExp(R_IPV4).test(value) && + !new RegExp(R_IPV6).test(value) && + !new RegExp(R_HOST).test(value)) + ) { + return form_error_answer_format; + } + return false; +}; + export const toNumber = value => value && parseInt(value, 10); diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index d3eb7342..32026a08 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -9,6 +9,7 @@ import toasts from './toasts'; import encryption from './encryption'; import clients from './clients'; import access from './access'; +import rewrites from './rewrites'; const settings = handleActions({ [actions.initSettingsRequest]: state => ({ ...state, processing: true }), @@ -422,6 +423,7 @@ export default combineReducers({ encryption, clients, access, + rewrites, loadingBar: loadingBarReducer, form: formReducer, }); diff --git a/client/src/reducers/rewrites.js b/client/src/reducers/rewrites.js new file mode 100644 index 00000000..d680b830 --- /dev/null +++ b/client/src/reducers/rewrites.js @@ -0,0 +1,50 @@ +import { handleActions } from 'redux-actions'; + +import * as actions from '../actions/rewrites'; + +const rewrites = handleActions( + { + [actions.getRewritesListRequest]: state => ({ ...state, processing: true }), + [actions.getRewritesListFailure]: state => ({ ...state, processing: false }), + [actions.getRewritesListSuccess]: (state, { payload }) => { + const newState = { + ...state, + list: payload, + processing: false, + }; + return newState; + }, + + [actions.addRewriteRequest]: state => ({ ...state, processingAdd: true }), + [actions.addRewriteFailure]: state => ({ ...state, processingAdd: false }), + [actions.addRewriteSuccess]: (state, { payload }) => { + const newState = { + ...state, + list: [...state.list, ...payload], + processingAdd: false, + }; + return newState; + }, + + [actions.deleteRewriteRequest]: state => ({ ...state, processingDelete: true }), + [actions.deleteRewriteFailure]: state => ({ ...state, processingDelete: false }), + [actions.deleteRewriteSuccess]: state => ({ ...state, processingDelete: false }), + + [actions.toggleRewritesModal]: (state) => { + const newState = { + ...state, + isModalOpen: !state.isModalOpen, + }; + return newState; + }, + }, + { + processing: true, + processingAdd: false, + processingDelete: false, + isModalOpen: false, + list: [], + }, +); + +export default rewrites; diff --git a/client/webpack.dev.js b/client/webpack.dev.js index 79589a08..442921e4 100644 --- a/client/webpack.dev.js +++ b/client/webpack.dev.js @@ -2,15 +2,18 @@ const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { + devtool: 'eval-source-map', module: { - rules: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: 'eslint-loader', - options: { - emitWarning: true, - configFile: 'dev.eslintrc', + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'eslint-loader', + options: { + emitWarning: true, + configFile: 'dev.eslintrc', + }, }, - }], + ], }, });