add encryption form and common components

This commit is contained in:
Ildar Kamalov
2025-01-18 19:21:20 +03:00
parent b4aa411826
commit 93890c2c6f
9 changed files with 525 additions and 422 deletions

View File

@@ -65,10 +65,12 @@ const Dhcp = () => {
modalType, modalType,
} = useSelector((state: RootState) => state.dhcp, shallowEqual); } = useSelector((state: RootState) => state.dhcp, shallowEqual);
const interface_name = const interface_name = useSelector(
useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name); (state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name,
const isInterfaceIncludesIpv4 = );
useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses); const isInterfaceIncludesIpv4 = useSelector(
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
);
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual); const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
@@ -130,12 +132,7 @@ const Dhcp = () => {
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName; const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
const getToggleDhcpButton = () => { const getToggleDhcpButton = () => {
const filledConfig = const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean));
interface_name &&
(Object.values(v4)
.every(Boolean) ||
Object.values(v6).every(Boolean));
const className = classNames('btn btn-sm', { const className = classNames('btn btn-sm', {
'btn-gray': enabled, 'btn-gray': enabled,
@@ -200,11 +197,7 @@ const Dhcp = () => {
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean); const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
const disabledLeasesButton = Boolean( const disabledLeasesButton = Boolean(
dhcp?.syncErrors || dhcp?.syncErrors || !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
!isInterfaceIncludesIpv4 ||
isEmptyConfig ||
processingConfig ||
!inputtedIPv4values,
); );
const cidr = inputtedIPv4values const cidr = inputtedIPv4values
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`

View File

@@ -1,11 +1,9 @@
import React from 'react'; import React, { useEffect, useRef } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, useTranslation } from 'react-i18next';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form'; import { Controller, useForm } from 'react-hook-form';
import i18next from 'i18next';
import { import {
validateServerName, validateServerName,
validateIsSafePort, validateIsSafePort,
@@ -14,7 +12,6 @@ import {
validatePortTLS, validatePortTLS,
validatePlainDns, validatePlainDns,
} from '../../../helpers/validators'; } from '../../../helpers/validators';
import i18n from '../../../i18n';
import KeyStatus from './KeyStatus'; import KeyStatus from './KeyStatus';
@@ -22,51 +19,37 @@ import CertificateStatus from './CertificateStatus';
import { import {
DNS_OVER_QUIC_PORT, DNS_OVER_QUIC_PORT,
DNS_OVER_TLS_PORT, DNS_OVER_TLS_PORT,
FORM_NAME,
STANDARD_HTTPS_PORT, STANDARD_HTTPS_PORT,
ENCRYPTION_SOURCE, ENCRYPTION_SOURCE,
} from '../../../helpers/constants'; } 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 certificateSourceOptions = [
const errors: { port_dns_over_tls?: string; port_https?: string } = {}; {
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) { const keySourceOptions = [
if (values.port_dns_over_tls === values.port_https) { {
errors.port_dns_over_tls = i18n.t('form_error_equal'); 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'); const validationMessage = (warningValidation: string, isWarning: boolean) => {
}
}
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) => {
if (!warningValidation) { if (!warningValidation) {
return null; return null;
} }
@@ -88,19 +71,25 @@ const validationMessage = (warningValidation: any, isWarning: any) => {
); );
}; };
interface FormProps { export type FormValues = {
handleSubmit: (...args: unknown[]) => string; enabled: boolean;
handleChange?: (...args: unknown[]) => unknown; serve_plain_dns: boolean;
isEnabled: boolean; server_name: string;
servePlainDns: boolean; force_https: boolean;
certificateChain: string; port_https: number;
privateKey: string; port_dns_over_tls: number;
certificatePath: string; port_dns_over_quic: number;
privateKeyPath: string; certificate_chain: string;
change: (...args: unknown[]) => unknown; private_key: string;
submitting: boolean; certificate_path: string;
invalid: boolean; private_key_path: string;
initialValues: object; certificate_source: string;
key_source: string;
private_key_saved: boolean;
};
type Props = {
initialValues: FormValues;
processingConfig: boolean; processingConfig: boolean;
processingValidate: boolean; processingValidate: boolean;
status_key?: string; status_key?: string;
@@ -114,71 +103,144 @@ interface FormProps {
key_type?: string; key_type?: string;
issuer?: string; issuer?: string;
subject?: string; subject?: string;
t: (...args: unknown[]) => string; onSubmit: (...args: unknown[]) => void;
setTlsConfig: (...args: unknown[]) => unknown; onChange: (...args: unknown[]) => void;
validateTlsConfig: (...args: unknown[]) => unknown; setTlsConfig: (...args: unknown[]) => void;
certificateSource?: string; validateTlsConfig: (...args: unknown[]) => void;
privateKeySource?: string; };
privateKeySaved?: boolean;
} 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<FormValues>(initialValues);
let Form = (props: FormProps) => {
const { const {
t, control,
handleSubmit, handleSubmit,
handleChange, watch,
isEnabled, reset,
servePlainDns, setValue,
certificateChain, setError,
privateKey, getValues,
certificatePath, formState: { isSubmitting, isValid },
privateKeyPath, } = useForm<FormValues>({
change, defaultValues: {
invalid, ...initialValues,
submitting, ...defaultValues,
processingConfig, },
processingValidate, mode: 'onChange',
not_after, });
valid_chain,
valid_key, const watchedValues = watch();
valid_cert,
valid_pair, const isEnabled = watch('enabled');
dns_names, const servePlainDns = watch('serve_plain_dns');
key_type, const certificateChain = watch('certificate_chain');
issuer, const privateKey = watch('private_key');
subject, const certificatePath = watch('certificate_path');
warning_validation, const privateKeyPath = watch('private_key_path');
setTlsConfig, const certificateSource = watch('certificate_source');
validateTlsConfig, const privateKeySaved = watch('private_key_saved');
certificateSource, const privateKeySource = watch('key_source');
privateKeySource,
privateKeySaved, useEffect(() => {
} = props; 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 isSavingDisabled = () => {
const processing = submitting || processingConfig || processingValidate; const processing = isSubmitting || processingConfig || processingValidate;
if (servePlainDns && !isEnabled) { 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 isDisabled = isSavingDisabled();
const isWarning = valid_key && valid_cert && valid_pair; const isWarning = valid_key && valid_cert && valid_pair;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit(onFormSubmit)}>
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<div className="form__group form__group--settings mb-3"> <div className="form__group form__group--settings mb-3">
<Field <Controller
name="enabled" name="enabled"
type="checkbox" control={control}
component={CheckboxField} render={({ field }) => <Checkbox {...field} title={t('encryption_enable')} />}
placeholder={t('encryption_enable')}
onChange={handleChange}
/> />
</div> </div>
@@ -187,13 +249,13 @@ let Form = (props: FormProps) => {
</div> </div>
<div className="form__group mb-3 mt-5"> <div className="form__group mb-3 mt-5">
<Field <Controller
name="serve_plain_dns" name="serve_plain_dns"
type="checkbox" control={control}
component={CheckboxField} rules={{
placeholder={t('encryption_plain_dns_enable')} validate: (value) => validatePlainDns(value, getValues()),
onChange={handleChange} }}
validate={validatePlainDns} render={({ field }) => <Checkbox {...field} title={t('encryption_plain_dns_enable')} />}
/> />
</div> </div>
@@ -212,16 +274,19 @@ let Form = (props: FormProps) => {
<div className="col-lg-6"> <div className="col-lg-6">
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<Field <Controller
id="server_name"
name="server_name" name="server_name"
component={renderInputField} control={control}
type="text" rules={{ validate: validateServerName }}
className="form-control" render={({ field, fieldState }) => (
placeholder={t('encryption_server_enter')} <Input
onChange={handleChange} {...field}
disabled={!isEnabled} type="text"
validate={validateServerName} placeholder={t('encryption_server_enter')}
error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
<div className="form__desc"> <div className="form__desc">
@@ -232,13 +297,12 @@ let Form = (props: FormProps) => {
<div className="col-lg-6"> <div className="col-lg-6">
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<Field <Controller
name="force_https" name="force_https"
type="checkbox" control={control}
component={CheckboxField} render={({ field }) => (
placeholder={t('encryption_redirect')} <Checkbox {...field} title={t('encryption_redirect')} disabled={!isEnabled} />
onChange={handleChange} )}
disabled={!isEnabled}
/> />
<div className="form__desc"> <div className="form__desc">
@@ -255,17 +319,19 @@ let Form = (props: FormProps) => {
<Trans>encryption_https</Trans> <Trans>encryption_https</Trans>
</label> </label>
<Field <Controller
id="port_https"
name="port_https" name="port_https"
component={renderInputField} control={control}
type="number" rules={{ validate: { validatePort, validateIsSafePort } }}
className="form-control" render={({ field, fieldState }) => (
placeholder={t('encryption_https')} <Input
validate={[validatePort, validateIsSafePort]} {...field}
normalize={toNumber} type="number"
onChange={handleChange} placeholder={t('encryption_https')}
disabled={!isEnabled} error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
<div className="form__desc"> <div className="form__desc">
@@ -280,17 +346,19 @@ let Form = (props: FormProps) => {
<Trans>encryption_dot</Trans> <Trans>encryption_dot</Trans>
</label> </label>
<Field <Controller
id="port_dns_over_tls"
name="port_dns_over_tls" name="port_dns_over_tls"
component={renderInputField} control={control}
type="number" rules={{ validate: validatePortTLS }}
className="form-control" render={({ field, fieldState }) => (
placeholder={t('encryption_dot')} <Input
validate={[validatePortTLS]} {...field}
normalize={toNumber} type="number"
onChange={handleChange} placeholder={t('encryption_dot')}
disabled={!isEnabled} error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
<div className="form__desc"> <div className="form__desc">
@@ -305,17 +373,19 @@ let Form = (props: FormProps) => {
<Trans>encryption_doq</Trans> <Trans>encryption_doq</Trans>
</label> </label>
<Field <Controller
id="port_dns_over_quic"
name="port_dns_over_quic" name="port_dns_over_quic"
component={renderInputField} control={control}
type="number" rules={{ validate: validatePortQuic }}
className="form-control" render={({ field, fieldState }) => (
placeholder={t('encryption_doq')} <Input
validate={[validatePortQuic]} {...field}
normalize={toNumber} type="number"
onChange={handleChange} placeholder={t('encryption_doq')}
disabled={!isEnabled} error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
<div className="form__desc"> <div className="form__desc">
@@ -352,50 +422,42 @@ let Form = (props: FormProps) => {
<div className="form__inline mb-2"> <div className="form__inline mb-2">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field <Controller
name="certificate_source" name="certificate_source"
component={renderRadioField} control={control}
type="radio" render={({ field }) => (
className="form-control mr-2" <Radio {...field} options={certificateSourceOptions} disabled={!isEnabled} />
value="path" )}
placeholder={t('encryption_certificates_source_path')}
disabled={!isEnabled}
/>
<Field
name="certificate_source"
component={renderRadioField}
type="radio"
className="form-control mr-2"
value="content"
placeholder={t('encryption_certificates_source_content')}
disabled={!isEnabled}
/> />
</div> </div>
</div> </div>
{certificateSource === ENCRYPTION_SOURCE.CONTENT && ( {certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
<Field <Controller
id="certificate_chain"
name="certificate_chain" name="certificate_chain"
component="textarea" control={control}
type="text" render={({ field, fieldState }) => (
className="form-control form-control--textarea" <Textarea
placeholder={t('encryption_certificates_input')} {...field}
onChange={handleChange} placeholder={t('encryption_certificates_input')}
disabled={!isEnabled} disabled={!isEnabled}
error={fieldState.error?.message}
/>
)}
/> />
)} ) : (
{certificateSource === ENCRYPTION_SOURCE.PATH && ( <Controller
<Field
id="certificate_path"
name="certificate_path" name="certificate_path"
component={renderInputField} control={control}
type="text" render={({ field, fieldState }) => (
className="form-control" <Input
placeholder={t('encryption_certificate_path')} {...field}
onChange={handleChange} type="text"
disabled={!isEnabled} placeholder={t('encryption_certificate_path')}
error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
)} )}
</div> </div>
@@ -424,70 +486,64 @@ let Form = (props: FormProps) => {
<div className="form__inline mb-2"> <div className="form__inline mb-2">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field <Controller
name="key_source" name="key_source"
component={renderRadioField} control={control}
type="radio" render={({ field }) => (
className="form-control mr-2" <Radio {...field} options={keySourceOptions} disabled={!isEnabled} />
value={ENCRYPTION_SOURCE.PATH} )}
placeholder={t('encryption_key_source_path')}
disabled={!isEnabled}
/>
<Field
name="key_source"
component={renderRadioField}
type="radio"
className="form-control mr-2"
value={ENCRYPTION_SOURCE.CONTENT}
placeholder={t('encryption_key_source_content')}
disabled={!isEnabled}
/> />
</div> </div>
</div> </div>
{privateKeySource === ENCRYPTION_SOURCE.PATH && ( {privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
<Field <>
<Controller
name="private_key_saved"
control={control}
render={({ field }) => (
<Checkbox
{...field}
title={t('use_saved_key')}
disabled={!isEnabled}
onChange={(checked: boolean) => {
if (checked) {
setValue('private_key', '');
}
field.onChange(checked);
}}
/>
)}
/>
<Controller
name="private_key"
control={control}
render={({ field, fieldState }) => (
<Textarea
{...field}
placeholder={t('encryption_key_input')}
disabled={!isEnabled || privateKeySaved}
error={fieldState.error?.message}
/>
)}
/>
</>
) : (
<Controller
name="private_key_path" name="private_key_path"
component={renderInputField} control={control}
type="text" render={({ field, fieldState }) => (
className="form-control" <Input
placeholder={t('encryption_private_key_path')} {...field}
onChange={handleChange} type="text"
disabled={!isEnabled} placeholder={t('encryption_private_key_path')}
error={fieldState.error?.message}
disabled={!isEnabled}
/>
)}
/> />
)} )}
{privateKeySource === ENCRYPTION_SOURCE.CONTENT && [
<Field
key="private_key_saved"
name="private_key_saved"
type="checkbox"
className="form__group form__group--settings mb-2"
component={CheckboxField}
disabled={!isEnabled}
placeholder={t('use_saved_key')}
onChange={(event: any) => {
if (event.target.checked) {
change('private_key', '');
}
if (handleChange) {
handleChange(event);
}
}}
/>,
<Field
id="private_key"
key="private_key"
name="private_key"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('encryption_key_input')}
onChange={handleChange}
disabled={!isEnabled || privateKeySaved}
/>,
]}
</div> </div>
<div className="form__status"> <div className="form__status">
@@ -505,44 +561,11 @@ let Form = (props: FormProps) => {
<button <button
type="button" type="button"
className="btn btn-secondary btn-standart" className="btn btn-secondary btn-standart"
disabled={submitting || processingConfig} disabled={isSubmitting || processingConfig}
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}> onClick={clearFields}>
<Trans>reset_settings</Trans> <Trans>reset_settings</Trans>
</button> </button>
</div> </div>
</form> </form>
); );
}; };
const selector = formValueSelector(FORM_NAME.ENCRYPTION);
Form = connect((state) => {
const isEnabled = selector(state, 'enabled');
const servePlainDns = selector(state, 'serve_plain_dns');
const certificateChain = selector(state, 'certificate_chain');
const privateKey = selector(state, 'private_key');
const certificatePath = selector(state, 'certificate_path');
const privateKeyPath = selector(state, 'private_key_path');
const certificateSource = selector(state, 'certificate_source');
const privateKeySource = selector(state, 'key_source');
const privateKeySaved = selector(state, 'private_key_saved');
return {
isEnabled,
servePlainDns,
certificateChain,
privateKey,
certificatePath,
privateKeyPath,
certificateSource,
privateKeySource,
privateKeySaved,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.ENCRYPTION,
validate,
}),
])(Form);

View File

@@ -1,49 +1,30 @@
import React, { Component } from 'react'; import React, { useEffect, useCallback } from 'react';
import { withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import debounce from 'lodash/debounce'; import { debounce } from 'lodash';
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants'; import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
import Form from './Form'; import { Form, FormValues } from './Form';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle'; import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import { EncryptionData } from '../../../initialState'; import { EncryptionData } from '../../../initialState';
interface EncryptionProps { type Props = {
setTlsConfig: (...args: unknown[]) => unknown;
validateTlsConfig: (...args: unknown[]) => unknown;
encryption: EncryptionData; encryption: EncryptionData;
t: (...args: unknown[]) => string; setTlsConfig: (values: EncryptionData) => void;
} validateTlsConfig: (values: EncryptionData) => void;
};
class Encryption extends Component<EncryptionProps> { export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
componentDidMount() { const { t } = useTranslation();
const { validateTlsConfig, encryption } = this.props;
useEffect(() => {
if (encryption.enabled) { if (encryption.enabled) {
validateTlsConfig(encryption); validateTlsConfig(encryption);
} }
} }, [encryption, validateTlsConfig]);
handleFormSubmit = (values: any) => { const getInitialValues = useCallback((data: any): FormValues => {
const submitValues = this.getSubmitValues(values);
this.props.setTlsConfig(submitValues);
};
handleFormChange = debounce((values) => {
const submitValues = this.getSubmitValues(values);
if (submitValues.enabled) {
this.props.validateTlsConfig(submitValues);
}
}, DEBOUNCE_TIMEOUT);
getInitialValues = (data: any) => {
const { certificate_chain, private_key, private_key_saved } = data; const { certificate_chain, private_key, private_key_saved } = data;
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH; const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH; const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
@@ -53,9 +34,9 @@ class Encryption extends Component<EncryptionProps> {
certificate_source, certificate_source,
key_source, key_source,
}; };
}; }, []);
getSubmitValues = (values: any) => { const getSubmitValues = useCallback((values: any) => {
const { certificate_source, key_source, private_key_saved, ...config } = values; const { certificate_source, key_source, private_key_saved, ...config } = values;
if (certificate_source === ENCRYPTION_SOURCE.PATH) { if (certificate_source === ENCRYPTION_SOURCE.PATH) {
@@ -76,63 +57,50 @@ class Encryption extends Component<EncryptionProps> {
} }
return config; return config;
}; }, []);
render() { const handleFormSubmit = useCallback(
const { encryption, t } = this.props; (values: any) => {
const { const submitValues = getSubmitValues(values);
enabled, setTlsConfig(submitValues);
server_name, },
force_https, [getSubmitValues, setTlsConfig],
port_https, );
port_dns_over_tls,
port_dns_over_quic,
certificate_chain,
private_key,
certificate_path,
private_key_path,
private_key_saved,
serve_plain_dns,
} = encryption;
const initialValues = this.getInitialValues({ const handleChange = useCallback(
enabled, debounce((values) => {
server_name, const submitValues = getSubmitValues(values);
force_https,
port_https,
port_dns_over_tls,
port_dns_over_quic,
certificate_chain,
private_key,
certificate_path,
private_key_path,
private_key_saved,
serve_plain_dns,
});
return ( if (submitValues.enabled) {
<div className="encryption"> validateTlsConfig(submitValues);
<PageTitle title={t('encryption_settings')} /> }
}, DEBOUNCE_TIMEOUT),
[getSubmitValues, validateTlsConfig],
);
{encryption.processing && <Loading />} const initialValues = getInitialValues(encryption);
{!encryption.processing && (
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
bodyType="card-body box-body--settings">
<Form
initialValues={initialValues}
onSubmit={this.handleFormSubmit}
onChange={this.handleFormChange}
setTlsConfig={this.props.setTlsConfig}
validateTlsConfig={this.props.validateTlsConfig}
{...this.props.encryption}
/>
</Card>
)}
</div>
);
}
}
export default withTranslation()(Encryption); return (
<div className="encryption">
<PageTitle title={t('encryption_settings')} />
{encryption.processing ? (
<Loading />
) : (
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
bodyType="card-body box-body--settings">
<Form
initialValues={initialValues}
onSubmit={handleFormSubmit}
onChange={handleChange}
setTlsConfig={setTlsConfig}
validateTlsConfig={validateTlsConfig}
{...encryption}
/>
</Card>
)}
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import React, { forwardRef, ReactNode } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import './checkbox.css'; import './checkbox.css';
@@ -10,26 +10,35 @@ type Props = {
name?: string; name?: string;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
error?: string;
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
}; };
export const Checkbox = ({ title, subtitle, value, name, disabled, className = 'checkbox--form', onChange }: Props) => ( export const Checkbox = forwardRef<HTMLInputElement, Props>(
<label className={clsx('checkbox', className)}> ({ title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange }, ref) => (
<span className="checkbox__marker" /> <>
<input <label className={clsx('checkbox', className)}>
name={name} <span className="checkbox__marker" />
type="checkbox" <input
className="checkbox__input" name={name}
disabled={disabled} type="checkbox"
checked={value} className="checkbox__input"
onChange={(e) => onChange(e.target.checked)} disabled={disabled}
/> checked={value}
<span className="checkbox__label"> onChange={(e) => onChange(e.target.checked)}
<span className="checkbox__label-text checkbox__label-text--long"> ref={ref}
<span className="checkbox__label-title">{title}</span> />
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{title}</span>
{subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>} {subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>}
</span> </span>
</span> </span>
</label> </label>
{error && <div className="form__message form__message--error">{error}</div>}
</>
),
); );
Checkbox.displayName = 'Checkbox';

View File

@@ -0,0 +1,29 @@
import React, { ComponentProps, forwardRef, ReactNode } from 'react';
import clsx from 'clsx';
interface Props extends ComponentProps<'input'> {
label?: string;
leftAddon?: ReactNode;
rightAddon?: ReactNode;
error?: string;
}
export const Input = forwardRef<HTMLInputElement, Props>(
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
{label}
</label>
)}
<div>
{leftAddon && <div>{leftAddon}</div>}
<input className={clsx('form-control', className)} ref={ref} {...rest} />
{rightAddon && <div>{rightAddon}</div>}
</div>
{error && <div className="form__message form__message--error">{error}</div>}
</div>
),
);
Input.displayName = 'Input';

View File

@@ -0,0 +1,49 @@
import React, { forwardRef } from 'react';
type Props<T> = {
name: string;
value: T;
onChange: (e: T) => void;
options: { label: string; desc?: string; value: T }[];
disabled?: boolean;
error?: string;
};
export const Radio = forwardRef<HTMLInputElement, Props<string | boolean | number | undefined>>(
({ disabled, onChange, value, options, name, error, ...rest }, ref) => {
const getId = (label: string) => (name ? `${label}_${name}` : label);
return (
<div>
{options.map((o) => {
const checked = value === o.value;
return (
<label
key={`${getId(o.label)}`}
htmlFor={getId(o.label)}
className="custom-control custom-radio">
<input
id={getId(o.label)}
type="radio"
className="custom-control-input"
onChange={() => onChange(o.value)}
checked={checked}
disabled={disabled}
ref={ref}
{...rest}
/>
<span className="custom-control-label">{o.label}</span>
{o.desc && <span className="checkbox__label-subtitle">{o.desc}</span>}
</label>
);
})}
{!disabled && error && <span className="form__message form__message--error">{error}</span>}
</div>
);
},
);
Radio.displayName = 'Radio';

View File

@@ -0,0 +1,32 @@
import React, { ComponentProps, forwardRef } from 'react';
import clsx from 'clsx';
interface Props extends ComponentProps<'textarea'> {
className?: string;
label?: string;
error?: string;
}
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
({ name, label, className, error, onClick, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
{label}
</label>
)}
<textarea
onClick={onClick}
className={clsx(
'form-control form-control--textarea form-control--textarea-small font-monospace',
className,
)}
ref={ref}
{...rest}
/>
{error && <div className="form__message form__message--error">{error}</div>}
</div>
),
);
Textarea.displayName = 'Textarea';

View File

@@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption'; import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
import Encryption from '../components/Settings/Encryption'; import { Encryption } from '../components/Settings/Encryption';
const mapStateToProps = (state: any) => { const mapStateToProps = (state: any) => {
const { encryption } = state; const { encryption } = state;

View File

@@ -34,7 +34,7 @@ export const validateRequiredValue = (value: any) => {
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) { if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {
return undefined; return undefined;
} }
return 'form_error_required'; return i18next.t('form_error_required');
}; };
/** /**
@@ -50,7 +50,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
const { range_end, range_start } = allValues.v4; const { range_end, range_start } = allValues.v4;
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) { if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
return 'greater_range_start_error'; return i18next.t('greater_range_start_error');
} }
return undefined; return undefined;
@@ -62,7 +62,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
*/ */
export const validateIpv4 = (value: any) => { export const validateIpv4 = (value: any) => {
if (value && !R_IPV4.test(value)) { if (value && !R_IPV4.test(value)) {
return 'form_error_ip4_format'; return i18next.t('form_error_ip4_format');
} }
return undefined; return undefined;
}; };
@@ -107,16 +107,16 @@ export const validateNotInRange = (value: any, allValues: any) => {
*/ */
export const validateGatewaySubnetMask = (_: any, allValues: any) => { export const validateGatewaySubnetMask = (_: any, allValues: any) => {
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) { if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
return 'gateway_or_subnet_invalid'; return i18next.t('gateway_or_subnet_invalid');
} }
const { subnet_mask, gateway_ip } = allValues.v4; const { subnet_mask, gateway_ip } = allValues.v4;
if (validateIpv4(gateway_ip)) { if (validateIpv4(gateway_ip)) {
return 'gateway_or_subnet_invalid'; return i18next.t('gateway_or_subnet_invalid');
} }
return parseSubnetMask(subnet_mask) ? undefined : 'gateway_or_subnet_invalid'; return parseSubnetMask(subnet_mask) ? undefined : i18next.t('gateway_or_subnet_invalid');
}; };
/** /**
@@ -138,7 +138,7 @@ export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
const subnetPrefix = parseSubnetMask(subnet_mask); const subnetPrefix = parseSubnetMask(subnet_mask);
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) { if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
return 'subnet_error'; return i18next.t('subnet_error');
} }
return undefined; return undefined;
@@ -164,7 +164,7 @@ export const validateClientId = (value: any) => {
R_CLIENT_ID.test(formattedValue) R_CLIENT_ID.test(formattedValue)
) )
) { ) {
return 'form_error_client_id_format'; return i18next.t('form_error_client_id_format');
} }
return undefined; return undefined;
}; };
@@ -205,7 +205,7 @@ export const validateServerName = (value: any) => {
*/ */
export const validateIpv6 = (value: any) => { export const validateIpv6 = (value: any) => {
if (value && !R_IPV6.test(value)) { if (value && !R_IPV6.test(value)) {
return 'form_error_ip6_format'; return i18next.t('form_error_ip6_format');
} }
return undefined; return undefined;
}; };
@@ -216,7 +216,7 @@ export const validateIpv6 = (value: any) => {
*/ */
export const validateIp = (value: any) => { export const validateIp = (value: any) => {
if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) { if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {
return 'form_error_ip_format'; return i18next.t('form_error_ip_format');
} }
return undefined; return undefined;
}; };
@@ -227,7 +227,7 @@ export const validateIp = (value: any) => {
*/ */
export const validateMac = (value: any) => { export const validateMac = (value: any) => {
if (value && !R_MAC.test(value)) { if (value && !R_MAC.test(value)) {
return 'form_error_mac_format'; return i18next.t('form_error_mac_format');
} }
return undefined; return undefined;
}; };
@@ -249,7 +249,7 @@ export const validatePort = (value: any) => {
*/ */
export const validateInstallPort = (value: any) => { export const validateInstallPort = (value: any) => {
if (value < 1 || value > MAX_PORT) { if (value < 1 || value > MAX_PORT) {
return 'form_error_port'; return i18next.t('form_error_port');
} }
return undefined; return undefined;
}; };
@@ -263,7 +263,7 @@ export const validatePortTLS = (value: any) => {
return undefined; return undefined;
} }
if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) { if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
return 'form_error_port_range'; return i18next.t('form_error_port_range');
} }
return undefined; return undefined;
}; };
@@ -313,7 +313,7 @@ export const validateAnswer = (value: any) => {
*/ */
export const validatePath = (value: any) => { export const validatePath = (value: any) => {
if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) { if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {
return 'form_error_url_or_path_format'; return i18next.t('form_error_url_or_path_format');
} }
return undefined; return undefined;
}; };
@@ -401,7 +401,7 @@ export const validatePlainDns = (value: any, allValues: any) => {
const { enabled } = allValues; const { enabled } = allValues;
if (!enabled && !value) { if (!enabled && !value) {
return 'encryption_plain_dns_error'; return i18next.t('encryption_plain_dns_error');
} }
return undefined; return undefined;