diff --git a/Makefile b/Makefile index 23fc4a6d..5412d596 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ GO.MACRO = $${GO:-go} VERBOSE.MACRO = $${VERBOSE:-0} CHANNEL = development -# TODO: Update CLIENT_DIR to client_v2 for new frontend migration +# TODO(ik): Update CLIENT_DIR to client_v2 for new frontend migration CLIENT_DIR = client DEPLOY_SCRIPT_PATH = not/a/real/path DIST_DIR = dist diff --git a/client_v2/src/__locales/en.json b/client_v2/src/__locales/en.json index b5d11521..ea52611f 100644 --- a/client_v2/src/__locales/en.json +++ b/client_v2/src/__locales/en.json @@ -393,40 +393,23 @@ "next": "Next", "open_dashboard": "Open Dashboard", "install_saved": "Saved successfully", - "encryption_title": "Encryption", "encryption_desc": "Encryption (HTTPS/QUIC/TLS) support for both DNS and admin web interface", "encryption_config_saved": "Encryption configuration saved", - "encryption_server": "Server name", "encryption_server_enter": "Enter your domain name", "encryption_server_desc": "If set, AdGuard Home detects ClientIDs, responds to DDR queries, and performs additional connection validations. If not set, these features are disabled. Must match one of the DNS Names in the certificate.", "encryption_redirect": "Redirect to HTTPS automatically", "encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.", - "encryption_https": "HTTPS port", "encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.", - "encryption_dot": "DNS-over-TLS port", "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.", - "encryption_doq": "DNS-over-QUIC port", "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port.", - "encryption_certificates": "Certificates", - "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}} or you can buy it from one of the trusted Certificate Authorities.", "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.", "encryption_status": "Status", - "encryption_expire": "Expires", - "encryption_key": "Private key", "encryption_key_input": "Copy/paste your PEM-encoded private key for your certificate here.", "encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)", "encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.", - "encryption_chain_valid": "Certificate chain is valid", - "encryption_chain_invalid": "Certificate chain is invalid", - "encryption_key_valid": "This is a valid {{type}} private key", - "encryption_key_invalid": "This is an invalid {{type}} private key", - "encryption_subject": "Subject", - "encryption_issuer": "Issuer", - "encryption_hostnames": "Hostnames", "encryption_reset": "Are you sure you want to reset encryption settings?", "encryption_warning": "Warning", "encryption_plain_dns_enable": "Enable plain DNS", - "encryption_plain_dns_desc": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must enable at least one encrypted DNS protocol", "encryption_plain_dns_error": "To disable plain DNS, enable at least one encrypted DNS protocol", "topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings.", "topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings.", @@ -805,5 +788,46 @@ "settings_tooltip_examples": "Examples:", "settings_notify_changes_saved": "Changes saved", "settings_notify_query_log_cleared": "Query log cleared", - "settings_notify_statistics_cleared": "Statistics cleared" + "settings_notify_statistics_cleared": "Statistics cleared", + "encryption_title": "Encryption", + "encryption_encrypted_dns": "Encrypted DNS", + "encryption_encrypted_dns_desc": "The AdGuard Home admin interface uses HTTPS. The DNS server supports DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC", + "encryption_plain_dns": "Plain DNS", + "encryption_plain_dns_desc": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must specify at least one encrypted DNS protocol", + "encryption_server": "Server name", + "encryption_server_tooltip_1": "If set, AdGuard Home will detect ClientIDs, respond to DDR queries, and perform additional connection validations. If not set, these features are disabled", + "encryption_server_tooltip_2": "Must match a DNS name in the certificate", + "encryption_https": "HTTPS port", + "encryption_https_tooltip": "This port provides HTTPS access for the AdGuard Home admin interface. It also provides DNS-over-HTTPS at the '/dns-query' location", + "encryption_dot": "DNS-over-TLS port", + "encryption_dot_tooltip": "This port is used to run a DNS-over-TLS server", + "encryption_doq": "DNS-over-QUIC port", + "encryption_doq_tooltip": "This port is used to run a DNS-over-QUIC server", + "encryption_force_redirect": "Automatically redirect from HTTP to HTTPS", + "encryption_certificates": "Certificates", + "encryption_certificates_desc": "To use encryption, you must provide a valid SSL certificate chain for your domain. You can get a free SSL certificate from letsencrypt.org or buy one from a trusted Certificate Authority", + "encryption_key": "Private key", + "encryption_disable_plain_dns": "Disable plain DNS?", + "encryption_disable_plain_dns_desc": "Devices that do not support encrypted DNS may not work properly", + "disable": "Disable", + "encryption_chain_valid": "Certificate chain is valid", + "encryption_chain_invalid": "Certificate chain is invalid", + "encryption_key_valid": "Private key is valid", + "encryption_key_type": "Encryption algorithm: %value%", + "encryption_key_invalid": "Private key is invalid", + "encryption_subject": "Subject: %value%", + "encryption_issuer": "Issuer: %value%", + "encryption_expire": "Expires: %value%", + "encryption_hostnames": "Hostnames: %value%", + "encryption_certificate_has_issues": "Certificate has issues", + "encryption_confirm_clear": "Reset encryption settings", + "encryption_confirm_clear_desc": "All encryption settings will be reset to their default values", + "reset": "Reset", + "confirm": "Confirm", + "encryption_certificate_error": "Unable to read certificate file", + "encryption_private_key_error": "Private key does not match certificate", + "encryption_certificate_name_error": "The server certificate does not include the server name %value%", + "encryption_invalid_data": "Invalid data", + "encryption_certificate_no_ip": "This certificate does not contain IP addresses. DDR and DNS-over-TLS may not work properly", + "encryption_certificate_self_signed": "This is a self-signed certificate — it may not work on all devices. If you want to use it, make sure that all your devices will accept it" } diff --git a/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss index 0561a769..6bce1db5 100644 --- a/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss +++ b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss @@ -36,13 +36,9 @@ .label { display: grid; align-items: center; - font-size: 14px; + font-size: 16px; font-weight: var(--weight-regular); user-select: none; - - @media (min-width: 1024px) { - font-size: 16px; - } } .icon { diff --git a/client_v2/src/common/controls/Input/Input.module.pcss b/client_v2/src/common/controls/Input/Input.module.pcss index c4b148cc..f46029a9 100644 --- a/client_v2/src/common/controls/Input/Input.module.pcss +++ b/client_v2/src/common/controls/Input/Input.module.pcss @@ -99,7 +99,9 @@ } .inputLabel { - display: block; + display: flex; + align-items: center; + gap: 8px; margin-bottom: 4px; font-size: 14px; line-height: 24px; diff --git a/client_v2/src/common/controls/Input/Input.tsx b/client_v2/src/common/controls/Input/Input.tsx index 10dfc86e..5811b006 100644 --- a/client_v2/src/common/controls/Input/Input.tsx +++ b/client_v2/src/common/controls/Input/Input.tsx @@ -3,7 +3,7 @@ import cn from 'clsx'; import s from './Input.module.pcss'; -type Props = ComponentProps<'input'> & { +type Props = Omit, 'size'> & { label?: ReactNode; className?: string; innerClassName?: string; diff --git a/client_v2/src/common/controls/Radio/Radio.module.pcss b/client_v2/src/common/controls/Radio/Radio.module.pcss index 1b25b4e8..0c89e852 100644 --- a/client_v2/src/common/controls/Radio/Radio.module.pcss +++ b/client_v2/src/common/controls/Radio/Radio.module.pcss @@ -33,5 +33,9 @@ } .input:disabled + .handler .icon { + color: var(--disabled-gray-icons); +} + +.input:disabled:checked + .handler .icon { color: var(--disabled-main-button); } diff --git a/client_v2/src/common/ui/Button/Button.module.pcss b/client_v2/src/common/ui/Button/Button.module.pcss index 91585bd8..f8b92881 100644 --- a/client_v2/src/common/ui/Button/Button.module.pcss +++ b/client_v2/src/common/ui/Button/Button.module.pcss @@ -115,6 +115,27 @@ } } +.secondary-danger { + color: var(--default-danger-button); + border-color: var(--default-danger-button); + + &:hover, + &:focus { + color: var(--hovered-danger-button); + border-color: var(--hovered-danger-button); + } + + &:active { + color: var(--pressed-danger-button); + border-color: var(--pressed-danger-button); + } + + &:disabled { + color: var(--disabled-danger-button); + border-color: var(--disabled-danger-button); + } +} + .danger { color: var(--gray-0); background: var(--default-danger-button); diff --git a/client_v2/src/common/ui/Button/Button.tsx b/client_v2/src/common/ui/Button/Button.tsx index bda4f4a7..0e13cf52 100644 --- a/client_v2/src/common/ui/Button/Button.tsx +++ b/client_v2/src/common/ui/Button/Button.tsx @@ -5,7 +5,7 @@ import s from './Button.module.pcss'; export type ButtonProps = ComponentProps<'button'> & { size?: 'small' | 'medium' | 'big'; - variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; + variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'secondary-danger'; children?: ReactNode; leftAddon?: ReactNode; rightAddon?: ReactNode; diff --git a/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx new file mode 100644 index 00000000..1accd3b2 --- /dev/null +++ b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from 'react'; +import cn from 'clsx'; + +import { Dropdown } from 'panel/common/ui/Dropdown'; +import theme from 'panel/lib/theme'; +import { Icon } from 'panel/common/ui/Icon'; + +import s from './styles.module.pcss'; + +type Props = { + text: ReactNode; + menuSize?: 'small' | 'large'; +}; + +export const FaqTooltip = ({ text, menuSize = 'small' }: Props) => { + return ( + + {text} + + } + className={s.dropdown} + position="bottomLeft" + noIcon> +
+ +
+
+ ); +}; diff --git a/client_v2/src/common/ui/FaqTooltip/index.ts b/client_v2/src/common/ui/FaqTooltip/index.ts new file mode 100644 index 00000000..1e21f015 --- /dev/null +++ b/client_v2/src/common/ui/FaqTooltip/index.ts @@ -0,0 +1 @@ +export { FaqTooltip } from './FaqTooltip'; diff --git a/client_v2/src/common/ui/FaqTooltip/styles.module.pcss b/client_v2/src/common/ui/FaqTooltip/styles.module.pcss new file mode 100644 index 00000000..404373b0 --- /dev/null +++ b/client_v2/src/common/ui/FaqTooltip/styles.module.pcss @@ -0,0 +1,19 @@ +.trigger { + cursor: pointer; + color: var(--default-gray-icons); +} + +.menu { + padding: 16px; + background-color: var(--fills-backgrounds-page-background-additional); + + @media (min-width: 768px) { + max-width: 280px; + } + + &_large { + @media (min-width: 768px) { + max-width: 340px; + } + } +} diff --git a/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx b/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx new file mode 100644 index 00000000..2c8f6520 --- /dev/null +++ b/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx @@ -0,0 +1,50 @@ +import React, { ReactNode } from 'react'; +import cn from 'clsx'; + +import { Radio } from 'panel/common/controls/Radio'; +import theme from 'panel/lib/theme'; + +import s from './styles.module.pcss'; + +type Option = { text: string; value: T }; + +type Props = { + title: string; + description?: string; + disabled?: boolean; + value: T; + options: Option[]; + onChange: (value: T) => void; + className?: string; + children?: ReactNode; + name?: string; +}; + +export const RadioGroup = ({ + title, + description, + disabled, + value, + options, + onChange, + className, + children, + name, +}: Props) => { + return ( +
+
+
+
{title}
+ {description &&
{description}
} +
+
+
+ +
+ disabled={disabled} value={value} options={options} handleChange={onChange} name={name} /> + {children} +
+
+ ); +}; diff --git a/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx b/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx new file mode 100644 index 00000000..56a3350f --- /dev/null +++ b/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx @@ -0,0 +1,46 @@ +import React, { ReactNode, useRef } from 'react'; +import cn from 'clsx'; + +import { Switch } from 'panel/common/controls/Switch'; +import theme from 'panel/lib/theme'; + +import s from './styles.module.pcss'; + +type Props = { + title: string; + description?: string; + id: string; + className?: string; + checked: boolean; + onChange: (e: React.ChangeEvent) => void; + disabled?: boolean; + children?: ReactNode; +}; + +export const SwitchGroup = ({ title, description, id, className, checked, onChange, disabled, children }: Props) => { + const inputRef = useRef(null); + + const handleRowClick = () => { + if (disabled || !inputRef.current) { + return; + } + + inputRef.current?.click(); + }; + + return ( +
+
+
+
{title}
+ {description &&
{description}
} +
+
e.stopPropagation()}> + +
+
+ + {children &&
{children}
} +
+ ); +}; diff --git a/client_v2/src/common/ui/SettingsGroup/index.ts b/client_v2/src/common/ui/SettingsGroup/index.ts new file mode 100644 index 00000000..b1ed7a80 --- /dev/null +++ b/client_v2/src/common/ui/SettingsGroup/index.ts @@ -0,0 +1,2 @@ +export { RadioGroup } from './RadioGroup'; +export { SwitchGroup } from './SwitchGroup'; diff --git a/client_v2/src/common/ui/SettingsGroup/styles.module.pcss b/client_v2/src/common/ui/SettingsGroup/styles.module.pcss new file mode 100644 index 00000000..9739abd8 --- /dev/null +++ b/client_v2/src/common/ui/SettingsGroup/styles.module.pcss @@ -0,0 +1,40 @@ +.switch { + width: 100%; + padding: 8px 16px; + transition: background-color var(--t2); + border-radius: 8px; + + &:hover, + &:focus { + background-color: var(--fills-backgrounds-page-background-additional); + } +} + +.row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + cursor: pointer; + + @media (min-width: 1024px) { + gap: 40px; + } +} + +.text { + padding: 8px 0; +} + +.title { + color: var(--default-main-text); +} + +.desc { + color: var(--default-description-text); +} + +.input, +.content { + padding: 8px 0; +} diff --git a/client_v2/src/components/App/index.tsx b/client_v2/src/components/App/index.tsx index d329b967..06c8e535 100644 --- a/client_v2/src/components/App/index.tsx +++ b/client_v2/src/components/App/index.tsx @@ -8,8 +8,9 @@ import { Icons } from 'panel/common/ui/Icons'; import { Footer } from 'panel/common/ui/Footer'; import { Header } from 'panel/common/ui/Header'; import { Settings } from 'panel/components/Settings'; - import { LocalesType } from 'panel/common/intl'; +import { Encryption } from 'panel/components/Encryption'; + import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper'; import Toasts from '../Toasts'; import i18n from '../../i18n'; @@ -32,6 +33,11 @@ const ROUTES: RouteConfig[] = [ component: Settings, exact: true, }, + { + path: '/encryption', + component: Encryption, + exact: true, + }, ]; const App = () => { diff --git a/client_v2/src/components/Encryption/Form.tsx b/client_v2/src/components/Encryption/Form.tsx new file mode 100644 index 00000000..41dfb574 --- /dev/null +++ b/client_v2/src/components/Encryption/Form.tsx @@ -0,0 +1,636 @@ +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import cn from 'clsx'; + +import { toNumber } from 'panel/helpers/form'; +import { DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, STANDARD_HTTPS_PORT, ENCRYPTION_SOURCE } from 'panel/helpers/constants'; +import { EncryptionData } from 'panel/initialState'; +import { + validateServerName, + validateIsSafePort, + validatePort, + validatePortQuic, + validatePortTLS, + validatePlainDns, +} from 'panel/helpers/validators'; +import { Checkbox } from 'panel/common/controls/Checkbox'; +import { Input } from 'panel/common/controls/Input'; +import { Radio } from 'panel/common/controls/Radio'; +import { Textarea } from 'panel/common/controls/Textarea'; +import { useDispatch } from 'react-redux'; +import { setTlsConfig, validateTlsConfig } from 'panel/actions/encryption'; +import { Button } from 'panel/common/ui/Button'; +import intl from 'panel/common/intl'; +import { SwitchGroup } from 'panel/common/ui/SettingsGroup'; +import theme from 'panel/lib/theme'; + +import { FaqTooltip } from 'panel/common/ui/FaqTooltip'; +import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog'; +import { KeyStatus, CertificateStatus, ValidationStatus } from './Status'; + +import s from './styles.module.pcss'; + +const certificateSourceOptions = [ + { + text: intl.getMessage('encryption_certificates_source_path'), + value: ENCRYPTION_SOURCE.PATH, + }, + { + text: intl.getMessage('encryption_certificates_source_content'), + value: ENCRYPTION_SOURCE.CONTENT, + }, +]; + +const keySourceOptions = [ + { + text: intl.getMessage('encryption_key_source_path'), + value: ENCRYPTION_SOURCE.PATH, + }, + { + text: intl.getMessage('encryption_key_source_content'), + value: ENCRYPTION_SOURCE.CONTENT, + }, +]; + +export type EncryptionFormValues = { + 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: EncryptionFormValues; + encryption: EncryptionData; + onSubmit: (values: EncryptionFormValues) => void; + debouncedConfigValidation: (values: EncryptionFormValues) => 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, encryption, onSubmit, debouncedConfigValidation }: Props) => { + const dispatch = useDispatch(); + const [openConfirmReset, setOpenConfirmReset] = useState(false); + const [openPlainDnsDisable, setOpenPlainDnsDisable] = useState(false); + const [stagedFormValues, setStagedFormValues] = useState(null); + + const { + not_after, + valid_chain, + valid_key, + valid_cert, + valid_pair, + key_type, + dns_names, + issuer, + subject, + warning_validation, + processingConfig, + processingValidate, + } = encryption; + + const { + control, + handleSubmit, + watch, + reset, + setValue, + setError, + getValues, + formState: { isSubmitting, isValid }, + } = useForm({ + defaultValues: { + ...defaultValues, + ...initialValues, + }, + mode: 'onBlur', + }); + + const { + enabled: isEnabled, + serve_plain_dns: servePlainDns, + certificate_chain: certificateChain, + private_key: privateKey, + private_key_path: privateKeyPath, + key_source: privateKeySource, + private_key_saved: privateKeySaved, + certificate_path: certificatePath, + certificate_source: certificateSource, + } = watch(); + + const handleBlur = () => { + debouncedConfigValidation(getValues()); + }; + + const isSavingDisabled = () => { + const processing = isSubmitting || processingConfig || processingValidate; + + if (servePlainDns && !isEnabled) { + return !isValid || processing; + } + + return !isValid || processing || !valid_key || !valid_cert || !valid_pair; + }; + + const handleResetOpen = () => setOpenConfirmReset(true); + + const handleResetClose = () => setOpenConfirmReset(false); + + const handlePlainDnsDisableOpen = () => setOpenPlainDnsDisable(true); + + const handlePlainDnsDisableClose = () => { + setOpenPlainDnsDisable(false); + setStagedFormValues(null); + }; + + const handlePlainDnsDisableConfirm = () => { + if (stagedFormValues) { + onSubmit(stagedFormValues); + setStagedFormValues(null); + } + setOpenPlainDnsDisable(false); + }; + + const handleReset = () => { + reset(); + dispatch(setTlsConfig(defaultValues)); + dispatch(validateTlsConfig(defaultValues)); + }; + + const validatePorts = (values: EncryptionFormValues) => { + 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 = intl.getMessage('form_error_equal'); + errors.port_https = intl.getMessage('form_error_equal'); + } + } + + return errors; + }; + + const onFormSubmit = (data: EncryptionFormValues) => { + const validationErrors = validatePorts(data); + + if (Object.keys(validationErrors).length > 0) { + Object.entries(validationErrors).forEach(([field, message]) => { + setError(field as keyof EncryptionFormValues, { type: 'manual', message }); + }); + return; + } + + if (data.serve_plain_dns === false) { + setStagedFormValues(data); + handlePlainDnsDisableOpen(); + return; + } + + onSubmit(data); + }; + + const renderCertificateStatus = () => { + if (warning_validation) { + const isWarning = valid_key && valid_cert && valid_pair; + + return ; + } + + if (!certificateChain && !certificatePath) { + return null; + } + + return ( + + ); + }; + + const isDisabled = isSavingDisabled(); + + return ( +
+ ( + +
+
+ ( + + {intl.getMessage('encryption_server')} + + +
+ {intl.getMessage('encryption_server_tooltip_1')} +
+
+ {intl.getMessage('encryption_server_tooltip_2')} +
+ + } + menuSize="large" + /> + + } + placeholder={intl.getMessage('encryption_server_enter')} + errorMessage={fieldState.error?.message} + disabled={!isEnabled} + onBlur={() => { + field.onBlur(); + handleBlur(); + }} + /> + )} + /> +
+
+ ( + + {intl.getMessage('encryption_https')} + + + + } + placeholder={intl.getMessage('encryption_https')} + errorMessage={fieldState.error?.message} + disabled={!isEnabled} + onChange={(e) => { + const { value } = e.target; + field.onChange(toNumber(value)); + }} + onBlur={() => { + field.onBlur(); + handleBlur(); + }} + /> + )} + /> +
+
+ ( + + {intl.getMessage('encryption_dot')} + + + + } + placeholder={intl.getMessage('encryption_dot')} + errorMessage={fieldState.error?.message} + disabled={!isEnabled} + onChange={(e) => { + const { value } = e.target; + field.onChange(toNumber(value)); + }} + onBlur={() => { + field.onBlur(); + handleBlur(); + }} + /> + )} + /> +
+
+ ( + + {intl.getMessage('encryption_doq')} + + + + } + placeholder={intl.getMessage('encryption_doq')} + errorMessage={fieldState.error?.message} + disabled={!isEnabled} + onChange={(e) => { + const { value } = e.target; + field.onChange(toNumber(value)); + }} + onBlur={() => { + field.onBlur(); + handleBlur(); + }} + /> + )} + /> +
+
+
+ )} + /> + + validatePlainDns(value, getValues()), + }} + render={({ field }) => ( + + )} + /> + + ( + + )} + /> + +

+ {intl.getMessage('encryption_certificates')} +

+ +

+ {intl.getMessage('encryption_certificates_desc', { + a: (text: string) => ( + + {text} + + ), + })} +

+ +
+ ( + + )} + /> + +
+ {certificateSource === ENCRYPTION_SOURCE.CONTENT ? ( + ( +