From 2e493e0226d3f22941fd09eed44ebb67a4d2874a Mon Sep 17 00:00:00 2001 From: Artem Baskal <a.baskal@adguard.com> Date: Thu, 12 Dec 2019 21:48:17 +0300 Subject: [PATCH] + client: add clients forms validation and cache findClients function --- client/src/__locales/en.json | 1 + client/src/actions/queryLogs.js | 28 ++-- client/src/actions/stats.js | 48 +++--- client/src/components/Logs/Filters/Form.js | 4 +- .../src/components/Settings/Clients/Form.js | 30 ++-- client/src/components/Settings/Dhcp/Form.js | 16 +- .../Settings/Dhcp/StaticLeases/Form.js | 8 +- .../components/Settings/Dns/Access/Form.js | 8 +- .../components/Settings/Dns/Rewrites/Form.js | 6 +- .../components/Settings/Encryption/Form.js | 12 +- client/src/helpers/constants.js | 7 +- client/src/helpers/form.js | 141 ++++++++++-------- client/src/install/Setup/Auth.js | 8 +- client/src/install/Setup/Settings.js | 6 +- client/src/install/Setup/renderField.js | 19 --- client/src/login/Login/Form.js | 7 +- 16 files changed, 187 insertions(+), 162 deletions(-) delete mode 100644 client/src/install/Setup/renderField.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 05794bcb..926ee7a5 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -23,6 +23,7 @@ "form_error_ip6_format": "Invalid IPv6 format", "form_error_ip_format": "Invalid IP format", "form_error_mac_format": "Invalid MAC format", + "form_error_client_id_format": "Invalid client ID format", "form_error_positive": "Must be greater than 0", "form_error_negative": "Must be equal to 0 or greater", "dhcp_form_gateway_input": "Gateway IP", diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index 155f0a76..84e5f992 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -5,20 +5,28 @@ import { addErrorToast, addSuccessToast } from './index'; import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants'; -const getLogsWithParams = async (config) => { - const { older_than, filter, ...values } = config; - const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); - const { data, oldest } = rawLogs; - const logs = normalizeLogs(data); - const clientsParams = getParamsForClientsSearch(logs, 'client'); - const clients = await apiClient.findClients(clientsParams); - const logsWithClientInfo = addClientInfo(logs, clients, 'client'); +// Cache clients in closure +const getLogsWithParamsWrapper = () => { + let clients = {}; + return async (config) => { + const { older_than, filter, ...values } = config; + const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); + const { data, oldest } = rawLogs; + const logs = normalizeLogs(data); + const clientsParams = getParamsForClientsSearch(logs, 'client'); + if (!Object.values(clientsParams).every(client => client in clients)) { + clients = await apiClient.findClients(clientsParams); + } + const logsWithClientInfo = addClientInfo(logs, clients, 'client'); - return { - logs: logsWithClientInfo, oldest, older_than, filter, ...values, + return { + logs: logsWithClientInfo, oldest, older_than, filter, ...values, + }; }; }; +const getLogsWithParams = getLogsWithParamsWrapper(); + export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUEST'); export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE'); export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js index 25897aab..b928b85a 100644 --- a/client/src/actions/stats.js +++ b/client/src/actions/stats.js @@ -39,30 +39,38 @@ export const getStatsRequest = createAction('GET_STATS_REQUEST'); export const getStatsFailure = createAction('GET_STATS_FAILURE'); export const getStatsSuccess = createAction('GET_STATS_SUCCESS'); -export const getStats = () => async (dispatch) => { - dispatch(getStatsRequest()); - try { - const stats = await apiClient.getStats(); - const normalizedTopClients = normalizeTopStats(stats.top_clients); - const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); - const clients = await apiClient.findClients(clientsParams); - const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); +// Cache clients in closure +const getStatsWrapper = () => { + let clients = {}; + return () => async (dispatch) => { + dispatch(getStatsRequest()); + try { + const stats = await apiClient.getStats(); + const normalizedTopClients = normalizeTopStats(stats.top_clients); + const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); + if (!Object.values(clientsParams).every(client => client in clients)) { + clients = await apiClient.findClients(clientsParams); + } + const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); - const normalizedStats = { - ...stats, - top_blocked_domains: normalizeTopStats(stats.top_blocked_domains), - top_clients: topClientsWithInfo, - top_queried_domains: normalizeTopStats(stats.top_queried_domains), - avg_processing_time: secondsToMilliseconds(stats.avg_processing_time), - }; + const normalizedStats = { + ...stats, + top_blocked_domains: normalizeTopStats(stats.top_blocked_domains), + top_clients: topClientsWithInfo, + top_queried_domains: normalizeTopStats(stats.top_queried_domains), + avg_processing_time: secondsToMilliseconds(stats.avg_processing_time), + }; - dispatch(getStatsSuccess(normalizedStats)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getStatsFailure()); - } + dispatch(getStatsSuccess(normalizedStats)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getStatsFailure()); + } + }; }; +export const getStats = getStatsWrapper(); + export const resetStatsRequest = createAction('RESET_STATS_REQUEST'); export const resetStatsFailure = createAction('RESET_STATS_FAILURE'); export const resetStatsSuccess = createAction('RESET_STATS_SUCCESS'); diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index cedc6f5e..9e4f4365 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { withNamespaces, Trans } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField } from '../../../helpers/form'; +import { renderInputField } from '../../../helpers/form'; import { RESPONSE_FILTER } from '../../../helpers/constants'; import Tooltip from '../../ui/Tooltip'; @@ -65,7 +65,7 @@ const Form = (props) => { <Field id="filter_question_type" name="filter_question_type" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('type_table_header')} diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 897e1fc5..1e33b0f2 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -10,7 +10,9 @@ import Tabs from '../../ui/Tabs'; import Examples from '../Dns/Upstream/Examples'; import { toggleAllServices } from '../../../helpers/helpers'; import { - renderField, + required, + clientId, + renderInputField, renderGroupField, renderSelectField, renderServiceField, @@ -40,38 +42,30 @@ const settingsCheckboxes = [ placeholder: 'enforce_safe_search', }, ]; - const validate = (values) => { const errors = {}; const { name, ids } = values; - - if (!name || !name.length) { - errors.name = i18n.t('form_error_required'); - } + errors.name = required(name); if (ids && ids.length) { const idArrayErrors = []; ids.forEach((id, idx) => { - if (!id || !id.length) { - idArrayErrors[idx] = i18n.t('form_error_required'); - } + idArrayErrors[idx] = required(id) || clientId(id); }); if (idArrayErrors.length) { errors.ids = idArrayErrors; } } - return errors; }; -const renderFields = (placeholder, buttonTitle) => + +const renderFieldsWrapper = (placeholder, buttonTitle) => function cell(row) { const { fields, - meta: { error }, } = row; - return ( <div className="form__group"> {fields.map((ip, index) => ( @@ -84,6 +78,7 @@ const renderFields = (placeholder, buttonTitle) => placeholder={placeholder} isActionAvailable={index !== 0} removeField={() => fields.remove(index)} + normalize={data => data && data.trim()} /> </div> ))} @@ -97,11 +92,13 @@ const renderFields = (placeholder, buttonTitle) => <use xlinkHref="#plus" /> </svg> </button> - {error && <div className="error">{error}</div>} </div> ); }; +// Should create function outside of component to prevent component re-renders +const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id')); + let Form = (props) => { const { t, @@ -126,10 +123,11 @@ let Form = (props) => { <Field id="name" name="name" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_client_name')} + normalize={data => data && data.trim()} /> </div> @@ -155,7 +153,7 @@ let Form = (props) => { <div className="form__group"> <FieldArray name="ids" - component={renderFields(t('form_enter_id'), t('form_add_id'))} + component={renderFields} /> </div> </div> diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js index 8bbef865..619b3f95 100644 --- a/client/src/components/Settings/Dhcp/Form.js +++ b/client/src/components/Settings/Dhcp/Form.js @@ -5,7 +5,7 @@ import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form'; +import { renderInputField, required, ipv4, isPositive, toNumber } from '../../../helpers/form'; const renderInterfaces = (interfaces => ( Object.keys(interfaces).map((item) => { @@ -116,8 +116,9 @@ let Form = (props) => { <div className="form__group form__group--settings"> <label>{t('dhcp_form_gateway_input')}</label> <Field + id="gateway_ip" name="gateway_ip" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('dhcp_form_gateway_input')} @@ -127,8 +128,9 @@ let Form = (props) => { <div className="form__group form__group--settings"> <label>{t('dhcp_form_subnet_input')}</label> <Field + id="subnet_mask" name="subnet_mask" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('dhcp_form_subnet_input')} @@ -144,8 +146,9 @@ let Form = (props) => { </div> <div className="col"> <Field + id="range_start" name="range_start" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('dhcp_form_range_start')} @@ -154,8 +157,9 @@ let Form = (props) => { </div> <div className="col"> <Field + id="range_end" name="range_end" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('dhcp_form_range_end')} @@ -168,7 +172,7 @@ let Form = (props) => { <label>{t('dhcp_form_lease_title')}</label> <Field name="lease_duration" - component={renderField} + component={renderInputField} type="number" className="form-control" placeholder={t('dhcp_form_lease_input')} diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Form.js b/client/src/components/Settings/Dhcp/StaticLeases/Form.js index 6695a6b3..2441603b 100644 --- a/client/src/components/Settings/Dhcp/StaticLeases/Form.js +++ b/client/src/components/Settings/Dhcp/StaticLeases/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, ipv4, mac, required } from '../../../../helpers/form'; +import { renderInputField, ipv4, mac, required } from '../../../../helpers/form'; const Form = (props) => { const { @@ -24,7 +24,7 @@ const Form = (props) => { <Field id="mac" name="mac" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_enter_mac')} @@ -35,7 +35,7 @@ const Form = (props) => { <Field id="ip" name="ip" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_enter_ip')} @@ -46,7 +46,7 @@ const Form = (props) => { <Field id="hostname" name="hostname" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_enter_hostname')} diff --git a/client/src/components/Settings/Dns/Access/Form.js b/client/src/components/Settings/Dns/Access/Form.js index 29c9bc8b..380e0ae2 100644 --- a/client/src/components/Settings/Dns/Access/Form.js +++ b/client/src/components/Settings/Dns/Access/Form.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; +import { renderTextareaField } from '../../../../helpers/form'; const Form = (props) => { const { @@ -21,7 +22,7 @@ const Form = (props) => { <Field id="allowed_clients" name="allowed_clients" - component="textarea" + component={renderTextareaField} type="text" className="form-control form-control--textarea" disabled={processingSet} @@ -37,7 +38,7 @@ const Form = (props) => { <Field id="disallowed_clients" name="disallowed_clients" - component="textarea" + component={renderTextareaField} type="text" className="form-control form-control--textarea" disabled={processingSet} @@ -53,7 +54,7 @@ const Form = (props) => { <Field id="blocked_hosts" name="blocked_hosts" - component="textarea" + component={renderTextareaField} type="text" className="form-control form-control--textarea" disabled={processingSet} @@ -81,6 +82,7 @@ Form.propTypes = { initialValues: PropTypes.object.isRequired, processingSet: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + textarea: PropTypes.bool, }; export default flow([withNamespaces(), reduxForm({ form: 'accessForm' })])(Form); diff --git a/client/src/components/Settings/Dns/Rewrites/Form.js b/client/src/components/Settings/Dns/Rewrites/Form.js index 2d6b27a3..5f983fc4 100644 --- a/client/src/components/Settings/Dns/Rewrites/Form.js +++ b/client/src/components/Settings/Dns/Rewrites/Form.js @@ -4,7 +4,7 @@ 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'; +import { renderInputField, required, domain, answer } from '../../../../helpers/form'; const Form = (props) => { const { @@ -24,7 +24,7 @@ const Form = (props) => { <Field id="domain" name="domain" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_domain')} @@ -35,7 +35,7 @@ const Form = (props) => { <Field id="answer" name="answer" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('form_answer')} diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index 401265b8..bc3ffde8 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; import { - renderField, + renderInputField, renderSelectField, renderRadioField, toNumber, @@ -117,7 +117,7 @@ let Form = (props) => { <Field id="server_name" name="server_name" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('encryption_server_enter')} @@ -154,7 +154,7 @@ let Form = (props) => { <Field id="port_https" name="port_https" - component={renderField} + component={renderInputField} type="number" className="form-control" placeholder={t('encryption_https')} @@ -176,7 +176,7 @@ let Form = (props) => { <Field id="port_dns_over_tls" name="port_dns_over_tls" - component={renderField} + component={renderInputField} type="number" className="form-control" placeholder={t('encryption_dot')} @@ -252,7 +252,7 @@ let Form = (props) => { <Field id="certificate_path" name="certificate_path" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('encryption_certificate_path')} @@ -321,7 +321,7 @@ let Form = (props) => { <Field id="private_key_path" name="private_key_path" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={t('encryption_private_key_path')} diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 4d329bcd..2bb50c0c 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -1,8 +1,9 @@ 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 R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/; +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]))$/; +export const R_CIDR = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/; +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]))$/; export const STATS_NAMES = { avg_processing_time: 'average_processing_time', diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index 283f0975..41483fba 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,33 +1,45 @@ import React, { Fragment } from 'react'; import { Trans } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { R_IPV4, R_MAC, R_HOST, R_IPV6, R_CIDR, UNSAFE_PORTS } from '../helpers/constants'; -import { R_IPV4, R_MAC, R_HOST, R_IPV6, UNSAFE_PORTS } from '../helpers/constants'; +export const renderField = (props, elementType) => { + const { + input, id, className, placeholder, type, disabled, + autoComplete, meta: { touched, error }, + } = props; -export const renderField = ({ - input, - id, - className, - placeholder, - type, - disabled, - autoComplete, - meta: { touched, error }, -}) => ( - <Fragment> - <input - {...input} - id={id} - placeholder={placeholder} - type={type} - className={className} - disabled={disabled} - autoComplete={autoComplete} - /> - {!disabled && - touched && - (error && <span className="form__message form__message--error">{error}</span>)} - </Fragment> -); + const element = React.createElement(elementType, { + ...input, + id, + className, + placeholder, + autoComplete, + disabled, + type, + }); + return ( + <Fragment> + {element} + {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)} + </Fragment> + ); +}; + +renderField.propTypes = { + id: PropTypes.string.isRequired, + input: PropTypes.object.isRequired, + meta: PropTypes.object.isRequired, + className: PropTypes.string, + placeholder: PropTypes.string, + type: PropTypes.string, + disabled: PropTypes.bool, + autoComplete: PropTypes.bool, +}; + +export const renderTextareaField = props => renderField(props, 'textarea'); + +export const renderInputField = props => renderField(props, 'input'); export const renderGroupField = ({ input, @@ -53,7 +65,7 @@ export const renderGroupField = ({ autoComplete={autoComplete} /> {isActionAvailable && - <span className="input-group-append"> + <span className="input-group-append"> <button type="button" className="btn btn-secondary btn-icon" @@ -66,10 +78,9 @@ export const renderGroupField = ({ </span> } </div> - {!disabled && - touched && - (error && <span className="form__message form__message--error">{error}</span>)} + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); @@ -82,8 +93,8 @@ export const renderRadioField = ({ <span className="custom-control-label">{placeholder}</span> </label> {!disabled && - touched && - (error && <span className="form__message form__message--error">{error}</span>)} + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); @@ -112,8 +123,8 @@ export const renderSelectField = ({ </span> </label> {!disabled && - touched && - (error && <span className="form__message form__message--error">{error}</span>)} + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); @@ -141,52 +152,67 @@ export const renderServiceField = ({ </svg> </label> {!disabled && - touched && - (error && <span className="form__message form__message--error">{error}</span>)} + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); // Validation functions +// If the value is valid, the validation function should return undefined. +// https://redux-form.com/6.6.3/examples/fieldlevelvalidation/ export const required = (value) => { - if (value || value === 0) { - return false; + const formattedValue = typeof value === 'string' ? value.trim() : value; + if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) { + return undefined; } return <Trans>form_error_required</Trans>; }; export const ipv4 = (value) => { - if (value && !new RegExp(R_IPV4).test(value)) { + if (value && !R_IPV4.test(value)) { return <Trans>form_error_ip4_format</Trans>; } - return false; + return undefined; +}; + +export const clientId = (value) => { + if (!value) { + return undefined; + } + const formattedValue = value ? value.trim() : value; + if (formattedValue && !(R_IPV4.test(formattedValue) || R_IPV6.test(formattedValue) + || R_MAC.test(formattedValue) || R_CIDR.test(formattedValue))) { + return <Trans>form_error_client_id_format</Trans>; + } + return undefined; }; export const ipv6 = (value) => { - if (value && !new RegExp(R_IPV6).test(value)) { + if (value && !R_IPV6.test(value)) { return <Trans>form_error_ip6_format</Trans>; } - return false; + return undefined; }; export const ip = (value) => { - if (value && !new RegExp(R_IPV4).test(value) && !new RegExp(R_IPV6).test(value)) { + if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) { return <Trans>form_error_ip_format</Trans>; } - return false; + return undefined; }; export const mac = (value) => { - if (value && !new RegExp(R_MAC).test(value)) { + if (value && !R_MAC.test(value)) { return <Trans>form_error_mac_format</Trans>; } - return false; + return undefined; }; export const isPositive = (value) => { if ((value || value === 0) && value <= 0) { return <Trans>form_error_positive</Trans>; } - return false; + return undefined; }; export const biggerOrEqualZero = (value) => { @@ -200,42 +226,37 @@ export const port = (value) => { if ((value || value === 0) && (value < 80 || value > 65535)) { return <Trans>form_error_port_range</Trans>; } - return false; + return undefined; }; export const portTLS = (value) => { if (value === 0) { - return false; + return undefined; } else if (value && (value < 80 || value > 65535)) { return <Trans>form_error_port_range</Trans>; } - return false; + return undefined; }; export const isSafePort = (value) => { if (UNSAFE_PORTS.includes(value)) { return <Trans>form_error_port_unsafe</Trans>; } - return false; + return undefined; }; export const domain = (value) => { - if (value && !new RegExp(R_HOST).test(value)) { + if (value && !R_HOST.test(value)) { return <Trans>form_error_domain_format</Trans>; } - return false; + return undefined; }; export const answer = (value) => { - if ( - value && - (!new RegExp(R_IPV4).test(value) && - !new RegExp(R_IPV6).test(value) && - !new RegExp(R_HOST).test(value)) - ) { + if (value && (!R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value))) { return <Trans>form_error_answer_format</Trans>; } - return false; + return undefined; }; export const toNumber = value => value && parseInt(value, 10); diff --git a/client/src/install/Setup/Auth.js b/client/src/install/Setup/Auth.js index d8234d94..d055d17b 100644 --- a/client/src/install/Setup/Auth.js +++ b/client/src/install/Setup/Auth.js @@ -6,7 +6,7 @@ import flow from 'lodash/flow'; import i18n from '../../i18n'; import Controls from './Controls'; -import renderField from './renderField'; +import { renderInputField } from '../../helpers/form'; const required = (value) => { if (value || value === 0) { @@ -48,7 +48,7 @@ const Auth = (props) => { </label> <Field name="username" - component={renderField} + component={renderInputField} type="text" className="form-control" placeholder={ t('install_auth_username_enter') } @@ -62,7 +62,7 @@ const Auth = (props) => { </label> <Field name="password" - component={renderField} + component={renderInputField} type="password" className="form-control" placeholder={ t('install_auth_password_enter') } @@ -76,7 +76,7 @@ const Auth = (props) => { </label> <Field name="confirm_password" - component={renderField} + component={renderInputField} type="password" className="form-control" placeholder={ t('install_auth_confirm') } diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js index 0eec2536..dd059fac 100644 --- a/client/src/install/Setup/Settings.js +++ b/client/src/install/Setup/Settings.js @@ -7,9 +7,9 @@ import flow from 'lodash/flow'; import Controls from './Controls'; import AddressList from './AddressList'; -import renderField from './renderField'; import { getInterfaceIp } from '../../helpers/helpers'; import { ALL_INTERFACES_IP } from '../../helpers/constants'; +import { renderInputField } from '../../helpers/form'; const required = (value) => { if (value || value === 0) { @@ -133,7 +133,7 @@ class Settings extends Component { </label> <Field name="web.port" - component={renderField} + component={renderInputField} type="number" className="form-control" placeholder="80" @@ -201,7 +201,7 @@ class Settings extends Component { </label> <Field name="dns.port" - component={renderField} + component={renderInputField} type="number" className="form-control" placeholder="80" diff --git a/client/src/install/Setup/renderField.js b/client/src/install/Setup/renderField.js deleted file mode 100644 index a323f17c..00000000 --- a/client/src/install/Setup/renderField.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, { Fragment } from 'react'; - -const renderField = ({ - input, className, placeholder, type, disabled, autoComplete, meta: { touched, error }, -}) => ( - <Fragment> - <input - {...input} - placeholder={placeholder} - type={type} - className={className} - disabled={disabled} - autoComplete={autoComplete} - /> - {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)} - </Fragment> -); - -export default renderField; diff --git a/client/src/login/Login/Form.js b/client/src/login/Login/Form.js index 0524129c..45274b55 100644 --- a/client/src/login/Login/Form.js +++ b/client/src/login/Login/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, required } from '../../helpers/form'; +import { renderInputField, required } from '../../helpers/form'; const Form = (props) => { const { @@ -19,10 +19,11 @@ const Form = (props) => { <Trans>username_label</Trans> </label> <Field + id="username1" name="username" type="text" className="form-control" - component={renderField} + component={renderInputField} placeholder={t('username_placeholder')} autoComplete="username" disabled={processing} @@ -38,7 +39,7 @@ const Form = (props) => { name="password" type="password" className="form-control" - component={renderField} + component={renderInputField} placeholder={t('password_placeholder')} autoComplete="current-password" disabled={processing}