+ client: handle DNS rewrites

This commit is contained in:
Ildar Kamalov 2019-07-22 15:32:12 +03:00
parent 70b8cf6ec8
commit e95aae5744
20 changed files with 631 additions and 70 deletions

86
client/package-lock.json generated vendored
View File

@ -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": {

5
client/package.json vendored
View File

@ -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",

View File

@ -331,5 +331,18 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings."
}
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> 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"
}

View File

@ -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());
}
};

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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 (
<div className="logs__row">
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<ul className="list-unstyled">{liNodes}</ul>
{this.renderTooltip(isRenderTooltip, rule, filterName)}
</div>
);
}
return (
<div className="logs__row">
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<span><Trans>empty_response_status</Trans></span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
@ -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 (
<Fragment>
<div className="logs__row">
{client}
</div>
<div className="logs__action">
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
<Trans>configure</Trans>
</Link>
</div>
</Fragment>
);
}
return (
<Fragment>
<div className="logs__row">
@ -261,6 +281,10 @@ class Logs extends Component {
return {
className: 'green',
};
} else if (rowInfo.original.reason === 'Rewrite') {
return {
className: 'blue',
};
}
return {

View File

@ -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 onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group">
<Field
id="domain"
name="domain"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_domain')}
validate={[required, domain]}
/>
</div>
<div className="form__group">
<Field
id="answer"
name="answer"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_answer')}
validate={[required, answer]}
/>
</div>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting || processingAdd}
onClick={() => {
reset();
toggleRewritesModal();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdd}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
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);

View File

@ -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 (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={() => toggleRewritesModal()}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
<Trans>Add DNS rewrite</Trans>
</h4>
<button type="button" className="close" onClick={() => toggleRewritesModal()}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
onSubmit={handleSubmit}
toggleRewritesModal={toggleRewritesModal}
processingAdd={processingAdd}
processingDelete={processingDelete}
/>
</div>
</ReactModal>
);
};
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);

View File

@ -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 }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
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 => (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() =>
this.props.handleDelete({
answer: value.row.answer,
domain: value.row.domain,
})
}
title={this.props.t('delete_table_action')}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
),
},
];
render() {
const {
t, list, processing, processingAdd, processingDelete,
} = this.props;
return (
<ReactTable
data={list || []}
columns={this.columns}
loading={processing || processingAdd || processingDelete}
className="-striped -highlight card-table-overflow"
showPagination={true}
defaultPageSize={10}
minRows={5}
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
rowsText={t('rows_table_footer_text')}
noDataText={t('rewrite_not_found')}
/>
);
}
}
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);

View File

@ -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 (
<Card
id="rewrites"
title={t('dns_rewrites')}
subtitle={t('rewrite_desc')}
bodyType="card-body box-body--settings"
>
<Fragment>
<Table
list={list}
processing={processing}
processingAdd={processingAdd}
processingDelete={processingDelete}
handleDelete={this.handleDelete}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleRewritesModal()}
disabled={processingAdd}
>
<Trans>rewrite_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
toggleRewritesModal={toggleRewritesModal}
handleSubmit={this.handleSubmit}
processingAdd={processingAdd}
processingDelete={processingDelete}
/>
</Fragment>
</Card>
);
}
}
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);

View File

@ -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}
/>
<Access access={access} setAccessList={setAccessList} />
<Rewrites
rewrites={rewrites}
getRewritesList={getRewritesList}
addRewrite={addRewrite}
deleteRewrite={deleteRewrite}
toggleRewritesModal={toggleRewritesModal}
/>
</Fragment>
)}
</Fragment>
@ -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,
};

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import './Card.css';
const Card = props => (
<div className={props.type ? `card ${props.type}` : 'card'}>
<div className={props.type ? `card ${props.type}` : 'card'} id={props.id ? props.id : ''}>
{props.title &&
<div className="card-header with-border">
<div className="card-inner">
@ -30,6 +30,7 @@ const Card = props => (
);
Card.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
bodyType: PropTypes.string,

View File

@ -15,3 +15,7 @@
.rt-tr-group .green {
background-color: #f1faf3;
}
.rt-tr-group .blue {
background-color: #ecf7ff;
}

View File

@ -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(

View File

@ -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 = {

View File

@ -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 },
}) => (
<Fragment>
<input
@ -15,7 +21,9 @@ export const renderField = ({
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
@ -24,20 +32,17 @@ export const renderSelectField = ({
}) => (
<Fragment>
<label className="checkbox checkbox--form">
<span className="checkbox__marker"/>
<input
{...input}
type="checkbox"
className="checkbox__input"
disabled={disabled}
/>
<span className="checkbox__marker" />
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
</span>
</span>
</label>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
@ -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 <Trans>form_error_positive</Trans>;
}
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 <Trans>form_error_domain_format</Trans>;
}
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 <Trans>form_error_answer_format</Trans>;
}
return false;
};
export const toNumber = value => value && parseInt(value, 10);

View File

@ -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,
});

View File

@ -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;

19
client/webpack.dev.js vendored
View File

@ -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',
},
},
}],
],
},
});