diff --git a/client/src/components/Settings/Dhcp/index.tsx b/client/src/components/Settings/Dhcp/index.tsx index 388518c7..9a323923 100644 --- a/client/src/components/Settings/Dhcp/index.tsx +++ b/client/src/components/Settings/Dhcp/index.tsx @@ -65,10 +65,12 @@ const Dhcp = () => { modalType, } = useSelector((state: RootState) => state.dhcp, shallowEqual); - const interface_name = - useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name); - const isInterfaceIncludesIpv4 = - useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses); + const interface_name = useSelector( + (state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name, + ); + const isInterfaceIncludesIpv4 = useSelector( + (state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses, + ); const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual); @@ -130,12 +132,7 @@ const Dhcp = () => { const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName; const getToggleDhcpButton = () => { - const filledConfig = - interface_name && - (Object.values(v4) - - .every(Boolean) || - Object.values(v6).every(Boolean)); + const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean)); const className = classNames('btn btn-sm', { 'btn-gray': enabled, @@ -200,11 +197,7 @@ const Dhcp = () => { const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean); const disabledLeasesButton = Boolean( - dhcp?.syncErrors || - !isInterfaceIncludesIpv4 || - isEmptyConfig || - processingConfig || - !inputtedIPv4values, + dhcp?.syncErrors || !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values, ); const cidr = inputtedIPv4values ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` diff --git a/client/src/components/Settings/Encryption/Form.tsx b/client/src/components/Settings/Encryption/Form.tsx index ae052ace..babcbec9 100644 --- a/client/src/components/Settings/Encryption/Form.tsx +++ b/client/src/components/Settings/Encryption/Form.tsx @@ -1,11 +1,9 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useEffect, useRef } from 'react'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; -import { Trans, withTranslation } from 'react-i18next'; -import flow from 'lodash/flow'; +import { Trans, useTranslation } from 'react-i18next'; -import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form'; +import { Controller, useForm } from 'react-hook-form'; +import i18next from 'i18next'; import { validateServerName, validateIsSafePort, @@ -14,7 +12,6 @@ import { validatePortTLS, validatePlainDns, } from '../../../helpers/validators'; -import i18n from '../../../i18n'; import KeyStatus from './KeyStatus'; @@ -22,51 +19,37 @@ import CertificateStatus from './CertificateStatus'; import { DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, - FORM_NAME, STANDARD_HTTPS_PORT, ENCRYPTION_SOURCE, } from '../../../helpers/constants'; +import { Checkbox } from '../../ui/Controls/Checkbox'; +import { Radio } from '../../ui/Controls/Radio'; +import { Input } from '../../ui/Controls/Input'; +import { Textarea } from '../../ui/Controls/Textarea'; -const validate = (values: any) => { - const errors: { port_dns_over_tls?: string; port_https?: string } = {}; +const certificateSourceOptions = [ + { + label: i18next.t('encryption_certificates_source_path'), + value: ENCRYPTION_SOURCE.PATH, + }, + { + label: i18next.t('encryption_certificates_source_content'), + value: ENCRYPTION_SOURCE.CONTENT, + }, +]; - if (values.port_dns_over_tls && values.port_https) { - if (values.port_dns_over_tls === values.port_https) { - errors.port_dns_over_tls = i18n.t('form_error_equal'); +const keySourceOptions = [ + { + label: i18next.t('encryption_key_source_path'), + value: ENCRYPTION_SOURCE.PATH, + }, + { + label: i18next.t('encryption_key_source_content'), + value: ENCRYPTION_SOURCE.CONTENT, + }, +]; - errors.port_https = i18n.t('form_error_equal'); - } - } - - return errors; -}; - -const clearFields = (change: any, setTlsConfig: any, validateTlsConfig: any, t: any) => { - const fields = { - private_key: '', - certificate_chain: '', - private_key_path: '', - certificate_path: '', - port_https: STANDARD_HTTPS_PORT, - port_dns_over_tls: DNS_OVER_TLS_PORT, - port_dns_over_quic: DNS_OVER_QUIC_PORT, - server_name: '', - force_https: false, - enabled: false, - private_key_saved: false, - serve_plain_dns: true, - }; - // eslint-disable-next-line no-alert - if (window.confirm(t('encryption_reset'))) { - Object.keys(fields) - - .forEach((field) => change(field, fields[field])); - setTlsConfig(fields); - validateTlsConfig(fields); - } -}; - -const validationMessage = (warningValidation: any, isWarning: any) => { +const validationMessage = (warningValidation: string, isWarning: boolean) => { if (!warningValidation) { return null; } @@ -88,19 +71,25 @@ const validationMessage = (warningValidation: any, isWarning: any) => { ); }; -interface FormProps { - handleSubmit: (...args: unknown[]) => string; - handleChange?: (...args: unknown[]) => unknown; - isEnabled: boolean; - servePlainDns: boolean; - certificateChain: string; - privateKey: string; - certificatePath: string; - privateKeyPath: string; - change: (...args: unknown[]) => unknown; - submitting: boolean; - invalid: boolean; - initialValues: object; +export type FormValues = { + enabled: boolean; + serve_plain_dns: boolean; + server_name: string; + force_https: boolean; + port_https: number; + port_dns_over_tls: number; + port_dns_over_quic: number; + certificate_chain: string; + private_key: string; + certificate_path: string; + private_key_path: string; + certificate_source: string; + key_source: string; + private_key_saved: boolean; +}; + +type Props = { + initialValues: FormValues; processingConfig: boolean; processingValidate: boolean; status_key?: string; @@ -114,71 +103,144 @@ interface FormProps { key_type?: string; issuer?: string; subject?: string; - t: (...args: unknown[]) => string; - setTlsConfig: (...args: unknown[]) => unknown; - validateTlsConfig: (...args: unknown[]) => unknown; - certificateSource?: string; - privateKeySource?: string; - privateKeySaved?: boolean; -} + onSubmit: (...args: unknown[]) => void; + onChange: (...args: unknown[]) => void; + setTlsConfig: (...args: unknown[]) => void; + validateTlsConfig: (...args: unknown[]) => void; +}; + +const defaultValues = { + enabled: false, + serve_plain_dns: true, + server_name: '', + force_https: false, + port_https: STANDARD_HTTPS_PORT, + port_dns_over_tls: DNS_OVER_TLS_PORT, + port_dns_over_quic: DNS_OVER_QUIC_PORT, + certificate_chain: '', + private_key: '', + certificate_path: '', + private_key_path: '', + certificate_source: ENCRYPTION_SOURCE.PATH, + key_source: ENCRYPTION_SOURCE.PATH, + private_key_saved: false, +}; + +export const Form = ({ + initialValues, + processingConfig, + processingValidate, + not_after, + valid_chain, + valid_key, + valid_cert, + valid_pair, + dns_names, + key_type, + issuer, + subject, + warning_validation, + onSubmit, + setTlsConfig, + validateTlsConfig, +}: Props) => { + const { t } = useTranslation(); + const previousValuesRef = useRef(initialValues); -let Form = (props: FormProps) => { const { - t, + control, handleSubmit, - handleChange, - isEnabled, - servePlainDns, - certificateChain, - privateKey, - certificatePath, - privateKeyPath, - change, - invalid, - submitting, - processingConfig, - processingValidate, - not_after, - valid_chain, - valid_key, - valid_cert, - valid_pair, - dns_names, - key_type, - issuer, - subject, - warning_validation, - setTlsConfig, - validateTlsConfig, - certificateSource, - privateKeySource, - privateKeySaved, - } = props; + watch, + reset, + setValue, + setError, + getValues, + formState: { isSubmitting, isValid }, + } = useForm({ + defaultValues: { + ...initialValues, + ...defaultValues, + }, + mode: 'onChange', + }); + + const watchedValues = watch(); + + const isEnabled = watch('enabled'); + const servePlainDns = watch('serve_plain_dns'); + const certificateChain = watch('certificate_chain'); + const privateKey = watch('private_key'); + const certificatePath = watch('certificate_path'); + const privateKeyPath = watch('private_key_path'); + const certificateSource = watch('certificate_source'); + const privateKeySaved = watch('private_key_saved'); + const privateKeySource = watch('key_source'); + + useEffect(() => { + const previousValues = previousValuesRef.current; + + if (JSON.stringify(previousValues) !== JSON.stringify(watchedValues)) { + // TODO onChange TLS config validation + console.log('TLS config validation'); + previousValuesRef.current = watchedValues; + } + }, [watchedValues]); const isSavingDisabled = () => { - const processing = submitting || processingConfig || processingValidate; + const processing = isSubmitting || processingConfig || processingValidate; if (servePlainDns && !isEnabled) { - return invalid || processing; + return !isValid || processing; } - return invalid || processing || !valid_key || !valid_cert || !valid_pair; + return !isValid || processing || !valid_key || !valid_cert || !valid_pair; + }; + + const clearFields = () => { + if (window.confirm(t('encryption_reset'))) { + reset(); + setTlsConfig(defaultValues); + validateTlsConfig(defaultValues); + } + }; + + const validatePorts = (values: FormValues) => { + const errors: { port_dns_over_tls?: string; port_https?: string } = {}; + + if (values.port_dns_over_tls && values.port_https) { + if (values.port_dns_over_tls === values.port_https) { + errors.port_dns_over_tls = i18next.t('form_error_equal'); + errors.port_https = i18next.t('form_error_equal'); + } + } + + return errors; + }; + + const onFormSubmit = (data: FormValues) => { + const validationErrors = validatePorts(data); + + if (Object.keys(validationErrors).length > 0) { + Object.entries(validationErrors).forEach(([field, message]) => { + setError(field as keyof FormValues, { type: 'manual', message }); + }); + } else { + onSubmit(data); + } }; const isDisabled = isSavingDisabled(); const isWarning = valid_key && valid_cert && valid_pair; return ( -
+
- } />
@@ -187,13 +249,13 @@ let Form = (props: FormProps) => {
- validatePlainDns(value, getValues()), + }} + render={({ field }) => } />
@@ -212,16 +274,19 @@ let Form = (props: FormProps) => {
- ( + + )} />
@@ -232,13 +297,12 @@ let Form = (props: FormProps) => {
- ( + + )} />
@@ -255,17 +319,19 @@ let Form = (props: FormProps) => { encryption_https - ( + + )} />
@@ -280,17 +346,19 @@ let Form = (props: FormProps) => { encryption_dot - ( + + )} />
@@ -305,17 +373,19 @@ let Form = (props: FormProps) => { encryption_doq - ( + + )} />
@@ -352,50 +422,42 @@ let Form = (props: FormProps) => {
- - - ( + + )} />
- {certificateSource === ENCRYPTION_SOURCE.CONTENT && ( - ( +