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}