mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-07-28 22:29:09 +00:00
Pull request 2452: ADG-10295 add encryption settings page
Squashed commit of the following: commite841e3a0b4Author: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 26 15:49:58 2025 +0300 fix todo commitcf251e209bAuthor: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 26 15:48:31 2025 +0300 fix lint issue commitc47e8216ecMerge:f754d7cd90e3692f9aAuthor: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 26 15:38:36 2025 +0300 Merge branch 'ADG-10291' into ADG-10295 commitf754d7cd97Merge:d56ea7d8afebcc6814Author: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 22 18:26:15 2025 +0300 Merge branch 'ADG-10291' into ADG-10295 commitd56ea7d8a6Author: Ildar Kamalov <ik@adguard.com> Date: Thu Aug 21 16:34:47 2025 +0300 fix form validation commita5fee78963Author: Ildar Kamalov <ik@adguard.com> Date: Wed Aug 20 17:59:53 2025 +0300 fix form commita2da460426Merge:cff3f4588ebfa5230aAuthor: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 15 17:30:28 2025 +0300 Merge branch 'ADG-10291' into ADG-10295 commitcff3f45883Author: Ildar Kamalov <ik@adguard.com> Date: Wed Aug 13 19:28:46 2025 +0300 ADG-10295 add encryption settings page commit3f6d759b7fAuthor: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 12 16:40:31 2025 +0300 fix placeholder commit507631ab71Author: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 12 16:15:57 2025 +0300 use batch commite495a5585cAuthor: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 12 16:13:13 2025 +0300 review fix commit2ba671b408Author: Ildar Kamalov <ik@adguard.com> Date: Tue Aug 12 10:45:00 2025 +0300 fix commit6f696238a0Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 18:38:28 2025 +0300 fix language change commit8206f3f885Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 14:54:58 2025 +0300 fix custom radio commit0920a51c4aAuthor: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 14:30:26 2025 +0300 fix commitc7ca1e8eafAuthor: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 14:25:16 2025 +0300 fix ids commit7ecd6426d9Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 13:56:29 2025 +0300 fix constant import commit246f1277b8Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 13:46:11 2025 +0300 fix version commit0224f567e6Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 13:34:29 2025 +0300 fix version commit75c9caffbdAuthor: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 13:14:01 2025 +0300 lint fix commitd0cf6ad5abAuthor: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 12:40:49 2025 +0300 lint fix commit87eb68e519Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 12:06:35 2025 +0300 use npm commit7df7d60ec9Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 11:23:00 2025 +0300 rm unused commitc285ea978aAuthor: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 10:42:39 2025 +0300 fix commitd00f1e12b3Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 11 09:43:35 2025 +0300 fix ... and 2 more commits
This commit is contained in:
2
Makefile
2
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
|
||||
|
||||
@@ -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}}</0> 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</0>.",
|
||||
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
|
||||
@@ -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 <a>letsencrypt.org</a> 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 <strong>%value%</strong>",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -99,7 +99,9 @@
|
||||
}
|
||||
|
||||
.inputLabel {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import cn from 'clsx';
|
||||
|
||||
import s from './Input.module.pcss';
|
||||
|
||||
type Props = ComponentProps<'input'> & {
|
||||
type Props = Omit<ComponentProps<'input'>, 'size'> & {
|
||||
label?: ReactNode;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
|
||||
@@ -33,5 +33,9 @@
|
||||
}
|
||||
|
||||
.input:disabled + .handler .icon {
|
||||
color: var(--disabled-gray-icons);
|
||||
}
|
||||
|
||||
.input:disabled:checked + .handler .icon {
|
||||
color: var(--disabled-main-button);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
35
client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
Normal file
35
client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
Normal file
@@ -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 (
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
menu={
|
||||
<div
|
||||
className={cn(theme.dropdown.menu, s.menu, {
|
||||
[s.menu_large]: menuSize === 'large',
|
||||
})}>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
className={s.dropdown}
|
||||
position="bottomLeft"
|
||||
noIcon>
|
||||
<div className={s.trigger}>
|
||||
<Icon icon="faq" className={s.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/common/ui/FaqTooltip/index.ts
Normal file
1
client_v2/src/common/ui/FaqTooltip/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { FaqTooltip } from './FaqTooltip';
|
||||
19
client_v2/src/common/ui/FaqTooltip/styles.module.pcss
Normal file
19
client_v2/src/common/ui/FaqTooltip/styles.module.pcss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx
Normal file
50
client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx
Normal file
@@ -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<T> = { text: string; value: T };
|
||||
|
||||
type Props<T> = {
|
||||
title: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
value: T;
|
||||
options: Option<T>[];
|
||||
onChange: (value: T) => void;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const RadioGroup = <T extends number | string | boolean>({
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
className,
|
||||
children,
|
||||
name,
|
||||
}: Props<T>) => {
|
||||
return (
|
||||
<div className={cn(s.switch, className)}>
|
||||
<div className={s.row}>
|
||||
<div className={s.text}>
|
||||
<div className={cn(s.title, theme.text.t2, theme.text.semibold)}>{title}</div>
|
||||
{description && <div className={cn(s.desc, theme.text.t3)}>{description}</div>}
|
||||
</div>
|
||||
<div className={s.input} />
|
||||
</div>
|
||||
|
||||
<div className={s.content}>
|
||||
<Radio<T> disabled={disabled} value={value} options={options} handleChange={onChange} name={name} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx
Normal file
46
client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx
Normal file
@@ -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<HTMLInputElement>) => void;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const SwitchGroup = ({ title, description, id, className, checked, onChange, disabled, children }: Props) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (disabled || !inputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(s.switch, className)}>
|
||||
<div className={s.row} onClick={handleRowClick}>
|
||||
<div className={s.text}>
|
||||
<div className={cn(theme.text.t2, theme.text.semibold, s.title)}>{title}</div>
|
||||
{description && <div className={cn(theme.text.t3, s.desc)}>{description}</div>}
|
||||
</div>
|
||||
<div className={s.input} onClick={(e) => e.stopPropagation()}>
|
||||
<Switch id={id} checked={checked} onChange={onChange} disabled={disabled} ref={inputRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children && <div className={s.content}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
client_v2/src/common/ui/SettingsGroup/index.ts
Normal file
2
client_v2/src/common/ui/SettingsGroup/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { RadioGroup } from './RadioGroup';
|
||||
export { SwitchGroup } from './SwitchGroup';
|
||||
40
client_v2/src/common/ui/SettingsGroup/styles.module.pcss
Normal file
40
client_v2/src/common/ui/SettingsGroup/styles.module.pcss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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 = () => {
|
||||
|
||||
636
client_v2/src/components/Encryption/Form.tsx
Normal file
636
client_v2/src/components/Encryption/Form.tsx
Normal file
@@ -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<EncryptionFormValues | null>(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<EncryptionFormValues>({
|
||||
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 <ValidationStatus type={isWarning ? 'warning' : 'error'} message={warning_validation} />;
|
||||
}
|
||||
|
||||
if (!certificateChain && !certificatePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CertificateStatus
|
||||
validChain={valid_chain}
|
||||
validCert={valid_cert}
|
||||
subject={subject}
|
||||
issuer={issuer}
|
||||
notAfter={not_after}
|
||||
dnsNames={dns_names}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const isDisabled = isSavingDisabled();
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="enabled"
|
||||
title={intl.getMessage('encryption_encrypted_dns')}
|
||||
description={intl.getMessage('encryption_encrypted_dns_desc')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}>
|
||||
<div className={s.group}>
|
||||
<div>
|
||||
<Controller
|
||||
name="server_name"
|
||||
control={control}
|
||||
rules={{ validate: validateServerName }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_server')}
|
||||
|
||||
<FaqTooltip
|
||||
text={
|
||||
<>
|
||||
<div className={s.tooltipText}>
|
||||
{intl.getMessage('encryption_server_tooltip_1')}
|
||||
</div>
|
||||
<div className={s.tooltipText}>
|
||||
{intl.getMessage('encryption_server_tooltip_2')}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
placeholder={intl.getMessage('encryption_server_enter')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="port_https"
|
||||
control={control}
|
||||
rules={{ validate: { validatePort, validateIsSafePort } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_https')}
|
||||
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('encryption_https_tooltip')}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="port_dns_over_tls"
|
||||
control={control}
|
||||
rules={{ validate: validatePortTLS }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_dot')}
|
||||
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('encryption_dot_tooltip')}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="port_dns_over_quic"
|
||||
control={control}
|
||||
rules={{ validate: validatePortQuic }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
label={
|
||||
<>
|
||||
{intl.getMessage('encryption_doq')}
|
||||
|
||||
<FaqTooltip
|
||||
text={intl.getMessage('encryption_doq_tooltip')}
|
||||
menuSize="large"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="serve_plain_dns"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => validatePlainDns(value, getValues()),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="serve_plain_dns"
|
||||
title={intl.getMessage('encryption_plain_dns')}
|
||||
description={intl.getMessage('encryption_plain_dns_desc')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="force_https"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SwitchGroup
|
||||
id="force_https"
|
||||
title={intl.getMessage('encryption_force_redirect')}
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('encryption_certificates')}
|
||||
</h2>
|
||||
|
||||
<p className={cn(s.description, theme.text.t2)}>
|
||||
{intl.getMessage('encryption_certificates_desc', {
|
||||
a: (text: string) => (
|
||||
<a href="https://letsencrypt.org/" target="_blank" rel="noreferrer" className={theme.link.link}>
|
||||
{text}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div className={theme.form.group}>
|
||||
<Controller
|
||||
name="certificate_source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio
|
||||
value={field.value}
|
||||
handleChange={field.onChange}
|
||||
name={field.name}
|
||||
options={certificateSourceOptions}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="certificate_chain"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={intl.getMessage('encryption_certificates_input')}
|
||||
disabled={!isEnabled}
|
||||
errorMessage={fieldState.error?.message}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur();
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="certificate_path"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={intl.getMessage('encryption_certificate_path')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur();
|
||||
}}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderCertificateStatus()}
|
||||
</div>
|
||||
|
||||
<h2 className={cn(theme.layout.subtitle, theme.title.h5, theme.title.h4_tablet)}>
|
||||
{intl.getMessage('encryption_key')}
|
||||
</h2>
|
||||
|
||||
<div className={theme.form.group}>
|
||||
<Controller
|
||||
name="key_source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio
|
||||
value={field.value}
|
||||
handleChange={field.onChange}
|
||||
name={field.name}
|
||||
options={keySourceOptions}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="private_key_saved"
|
||||
control={control}
|
||||
render={({ field: { value, onChange, name } }) => (
|
||||
<Checkbox
|
||||
name={name}
|
||||
disabled={!isEnabled || privateKeySource !== ENCRYPTION_SOURCE.CONTENT}
|
||||
checked={value}
|
||||
onChange={({ target: { checked } }) => {
|
||||
if (checked) {
|
||||
setValue('private_key', '');
|
||||
}
|
||||
onChange(checked);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
className={s.useSavedKey}>
|
||||
{intl.getMessage('use_saved_key')}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={theme.form.input}>
|
||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="private_key"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={intl.getMessage('encryption_key_input')}
|
||||
disabled={!isEnabled || privateKeySaved}
|
||||
errorMessage={fieldState.error?.message}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur();
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="private_key_path"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={intl.getMessage('encryption_private_key_path')}
|
||||
errorMessage={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur();
|
||||
}}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(privateKey || privateKeyPath) && <KeyStatus validKey={valid_key} keyType={key_type} />}
|
||||
</div>
|
||||
|
||||
<div className={theme.form.buttonGroup}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={isDisabled}
|
||||
className={theme.form.button}>
|
||||
{intl.getMessage('save')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary-danger"
|
||||
size="small"
|
||||
disabled={isSubmitting || processingConfig}
|
||||
onClick={handleResetOpen}
|
||||
className={theme.form.button}>
|
||||
{intl.getMessage('reset')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{openConfirmReset && (
|
||||
<ConfirmDialog
|
||||
onClose={handleResetClose}
|
||||
onConfirm={handleReset}
|
||||
buttonText={intl.getMessage('reset')}
|
||||
cancelText={intl.getMessage('cancel')}
|
||||
title={intl.getMessage('encryption_confirm_clear')}
|
||||
text={intl.getMessage('encryption_confirm_clear_desc')}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
|
||||
{openPlainDnsDisable && (
|
||||
<ConfirmDialog
|
||||
onClose={handlePlainDnsDisableClose}
|
||||
onConfirm={handlePlainDnsDisableConfirm}
|
||||
buttonText={intl.getMessage('disable')}
|
||||
cancelText={intl.getMessage('cancel')}
|
||||
title={intl.getMessage('encryption_disable_plain_dns')}
|
||||
text={intl.getMessage('encryption_disable_plain_dns_desc')}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
import { EMPTY_DATE } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
validChain: boolean;
|
||||
validCert: boolean;
|
||||
subject?: string;
|
||||
issuer?: string;
|
||||
notAfter?: string;
|
||||
dnsNames?: string[];
|
||||
};
|
||||
|
||||
export const CertificateStatus = ({ validChain, validCert, subject, issuer, notAfter, dnsNames }: Props) => (
|
||||
<StatusBlock
|
||||
variant={validChain ? 'success' : 'error'}
|
||||
title={validChain ? intl.getMessage('encryption_chain_valid') : intl.getMessage('encryption_chain_invalid')}
|
||||
>
|
||||
{validCert && (subject || issuer || notAfter || dnsNames) && (
|
||||
<ul className={s.statusList}>
|
||||
{subject && <li>{intl.getMessage('encryption_subject', { value: subject })}</li>}
|
||||
{issuer && <li>{intl.getMessage('encryption_issuer', { value: issuer })}</li>}
|
||||
{notAfter && notAfter !== EMPTY_DATE && (
|
||||
<li>{intl.getMessage('encryption_expire', { value: format(notAfter, 'YYYY-MM-DD HH:mm:ss') })}</li>
|
||||
)}
|
||||
{dnsNames && <li>{intl.getMessage('encryption_hostnames', { value: dnsNames.join(', ') })}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</StatusBlock>
|
||||
);
|
||||
19
client_v2/src/components/Encryption/Status/KeyStatus.tsx
Normal file
19
client_v2/src/components/Encryption/Status/KeyStatus.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
validKey: boolean;
|
||||
keyType?: string;
|
||||
};
|
||||
|
||||
export const KeyStatus = ({ validKey, keyType }: Props) => (
|
||||
<StatusBlock
|
||||
variant={validKey ? 'success' : 'error'}
|
||||
title={validKey ? intl.getMessage('encryption_key_valid') : intl.getMessage('encryption_key_invalid')}
|
||||
>
|
||||
{keyType && <div className={s.statusText}>{intl.getMessage('encryption_key_type', { value: keyType })}</div>}
|
||||
</StatusBlock>
|
||||
);
|
||||
28
client_v2/src/components/Encryption/Status/StatusBlock.tsx
Normal file
28
client_v2/src/components/Encryption/Status/StatusBlock.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import cn from 'clsx';
|
||||
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
export type StatusVariant = 'success' | 'error' | 'warning';
|
||||
|
||||
type Props = {
|
||||
variant: StatusVariant;
|
||||
title: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const StatusBlock = ({ variant, title, children }: Props) => (
|
||||
<div className={cn(s.status, theme.text.t3)}>
|
||||
<div
|
||||
className={cn(s.statusTitle, {
|
||||
[s.statusTitle_success]: variant === 'success',
|
||||
[s.statusTitle_error]: variant === 'error',
|
||||
[s.statusTitle_warning]: variant === 'warning',
|
||||
})}>
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { StatusBlock } from './StatusBlock';
|
||||
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
type Props = {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const ValidationStatus = ({ type, message }: Props) => (
|
||||
<StatusBlock variant={type} title={intl.getMessage('encryption_certificate_has_issues')}>
|
||||
<div className={s.statusText}>{message}</div>
|
||||
</StatusBlock>
|
||||
);
|
||||
3
client_v2/src/components/Encryption/Status/index.ts
Normal file
3
client_v2/src/components/Encryption/Status/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CertificateStatus } from './CertificateStatus';
|
||||
export { KeyStatus } from './KeyStatus';
|
||||
export { ValidationStatus } from './ValidationStatus';
|
||||
@@ -0,0 +1,26 @@
|
||||
.status {
|
||||
color: var(--default-main-text);
|
||||
}
|
||||
|
||||
.statusTitle {
|
||||
&_success {
|
||||
color: var(--default-link);
|
||||
}
|
||||
|
||||
&_warning {
|
||||
color: var(--default-attention-link);
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: var(--default-error-link);
|
||||
}
|
||||
}
|
||||
|
||||
.statusList {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
115
client_v2/src/components/Encryption/index.tsx
Normal file
115
client_v2/src/components/Encryption/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from 'panel/helpers/constants';
|
||||
import { RootState } from 'panel/initialState';
|
||||
import { PageLoader } from 'panel/common/ui/Loader';
|
||||
import { setTlsConfig, validateTlsConfig } from 'panel/actions/encryption';
|
||||
import theme from 'panel/lib/theme';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
import { EncryptionFormValues, Form } from './Form';
|
||||
import s from './styles.module.pcss';
|
||||
|
||||
export const Encryption = () => {
|
||||
const dispatch = useDispatch();
|
||||
const encryption = useSelector((state: RootState) => state.encryption);
|
||||
|
||||
const initialValues = useMemo((): EncryptionFormValues => {
|
||||
const {
|
||||
enabled,
|
||||
serve_plain_dns,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
} = encryption;
|
||||
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;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
serve_plain_dns,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
port_dns_over_quic,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
certificate_path,
|
||||
private_key_path,
|
||||
private_key_saved,
|
||||
certificate_source,
|
||||
key_source,
|
||||
};
|
||||
}, [encryption]);
|
||||
|
||||
const getSubmitValues = useCallback((values: any) => {
|
||||
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
||||
|
||||
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
||||
config.certificate_chain = '';
|
||||
} else {
|
||||
config.certificate_path = '';
|
||||
}
|
||||
|
||||
if (key_source === ENCRYPTION_SOURCE.PATH) {
|
||||
config.private_key = '';
|
||||
} else {
|
||||
config.private_key_path = '';
|
||||
|
||||
if (private_key_saved) {
|
||||
config.private_key = '';
|
||||
config.private_key_saved = private_key_saved;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(values: any) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
dispatch(setTlsConfig(submitValues));
|
||||
},
|
||||
[getSubmitValues, setTlsConfig],
|
||||
);
|
||||
|
||||
const validateConfig = useCallback((values) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
|
||||
if (submitValues.enabled) {
|
||||
dispatch(validateTlsConfig(submitValues));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedConfigValidation = useMemo(() => debounce(validateConfig, DEBOUNCE_TIMEOUT), [validateConfig]);
|
||||
|
||||
return (
|
||||
<div className={theme.layout.container}>
|
||||
<h1 className={cn(theme.layout.title, theme.title.h4, theme.title.h3_tablet, s.title)}>
|
||||
{intl.getMessage('encryption_title')}
|
||||
</h1>
|
||||
|
||||
{encryption.processing ? (
|
||||
<PageLoader />
|
||||
) : (
|
||||
<Form
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
debouncedConfigValidation={debouncedConfigValidation}
|
||||
encryption={encryption}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
client_v2/src/components/Encryption/styles.module.pcss
Normal file
22
client_v2/src/components/Encryption/styles.module.pcss
Normal file
@@ -0,0 +1,22 @@
|
||||
.title {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.useSavedKey {
|
||||
padding: 8px 0 8px 32px;
|
||||
}
|
||||
|
||||
.tooltipText:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { RoutePath } from 'panel/components/Routes/Paths';
|
||||
import { Link } from 'panel/common/ui/Link';
|
||||
|
||||
import { setFiltersConfig } from 'panel/actions/filtering';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
|
||||
@@ -5,12 +5,11 @@ import cn from 'clsx';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import intl from 'panel/common/intl';
|
||||
import { trimLinesAndRemoveEmpty } from 'panel/helpers/helpers';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
|
||||
import s from './styles.module.pcss';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
@@ -38,30 +37,25 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
|
||||
{...field}
|
||||
id={textareaId}
|
||||
label={
|
||||
<div className={s.label}>
|
||||
<>
|
||||
{intl.getMessage('settings_domain_names')}
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
menu={
|
||||
<div className={cn(theme.dropdown.menu, s.dropdownMenu)}>
|
||||
|
||||
<FaqTooltip
|
||||
text={
|
||||
<>
|
||||
<div className={s.dropdownTitle}>
|
||||
{intl.getMessage('settings_tooltip_domain_names')}
|
||||
</div>
|
||||
<div className={s.dropdownText}>
|
||||
<div>
|
||||
<strong>{intl.getMessage('settings_tooltip_examples')}</strong>
|
||||
<div>example.com</div>
|
||||
<div>*.example.com</div>
|
||||
<div>||example.com^</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
className={s.dropdown}
|
||||
position="bottomLeft"
|
||||
noIcon>
|
||||
<div className={s.dropdownTrigger}>
|
||||
<Icon icon="faq" className={s.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
/>
|
||||
|
||||
<a
|
||||
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home"
|
||||
target="_blank"
|
||||
@@ -69,7 +63,7 @@ export const IgnoredDomains = ({ control, processing, ignoreEnabled, setValue, s
|
||||
className={cn(s.link, theme.link.link, theme.link.noDecoration)}>
|
||||
{intl.getMessage('settings_rule_syntax')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
placeholder={`example.com\n*.example.com\n||example.com^`}
|
||||
size="large"
|
||||
|
||||
@@ -9,20 +9,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
cursor: pointer;
|
||||
color: var(--default-gray-icons);
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
padding: 16px;
|
||||
max-width: 240px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTitle {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Button } from 'panel/common/ui/Button';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { QUERY_LOG_INTERVALS_DAYS, RETENTION_CUSTOM } from 'panel/helpers/constants';
|
||||
|
||||
import { RadioGroup } from '../SettingsGroup/RadioGroup';
|
||||
import { SwitchGroup } from '../SettingsGroup';
|
||||
import { RadioGroup, SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
@@ -11,12 +11,12 @@ import { initSettings, toggleSetting } from 'panel/actions';
|
||||
import { getStatsConfig } from 'panel/actions/stats';
|
||||
import { getLogsConfig } from 'panel/actions/queryLogs';
|
||||
import { getFilteringStatus } from 'panel/actions/filtering';
|
||||
import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { StatsConfig } from './StatsConfig/StatsConfig';
|
||||
import { LogsConfig } from './LogsConfig';
|
||||
import { FiltersConfig } from './FiltersConfig';
|
||||
import { getSafeSearchProviderTitle } from './helpers';
|
||||
import { SwitchGroup } from './SettingsGroup';
|
||||
|
||||
const SETTINGS = {
|
||||
safebrowsing: {
|
||||
@@ -48,9 +48,8 @@ export const Settings = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSettingToggle =
|
||||
(key: keyof typeof SETTINGS) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(toggleSetting(key, !e.target.checked));
|
||||
const handleSettingToggle = (key: keyof typeof SETTINGS) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(toggleSetting(key, !e.target.checked));
|
||||
|
||||
const renderSettings = (settingsList?: SettingsData['settingsList']) =>
|
||||
settingsList
|
||||
@@ -82,20 +81,18 @@ export const Settings = () => {
|
||||
|
||||
type SafeSearchConfigShape = Record<string, boolean> & { enabled: boolean };
|
||||
|
||||
const onSafeSearchEnabledChange =
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = { ...safesearch, enabled: e.target.checked } as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
const onSafeSearchEnabledChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = { ...safesearch, enabled: e.target.checked } as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
|
||||
const onProviderChange =
|
||||
(searchKey: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = {
|
||||
...safesearch,
|
||||
[searchKey]: e.target.checked,
|
||||
} as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
const onProviderChange = (searchKey: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const payload = {
|
||||
...safesearch,
|
||||
[searchKey]: e.target.checked,
|
||||
} as SafeSearchConfigShape;
|
||||
dispatch(toggleSetting('safesearch', payload));
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchGroup
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button } from 'panel/common/ui/Button';
|
||||
import intl from 'panel/common/intl';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { RadioGroup, SwitchGroup } from 'panel/common/ui/SettingsGroup';
|
||||
|
||||
import { getIntervalTitle, getDefaultInterval } from '../helpers';
|
||||
import { STATS_INTERVALS_DAYS, RETENTION_CUSTOM } from '../../../helpers/constants';
|
||||
import { RadioGroup, SwitchGroup } from '../SettingsGroup';
|
||||
import { IgnoredDomains } from '../IgnoredDomains';
|
||||
import { RetentionCustomInput } from '../RetentionCustomInput';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import {
|
||||
MAX_PORT,
|
||||
R_CIDR,
|
||||
@@ -34,7 +33,7 @@ export const validateRequiredValue = (value: any) => {
|
||||
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {
|
||||
return undefined;
|
||||
}
|
||||
return i18next.t('form_error_required');
|
||||
return intl.getMessage('form_error_required');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,7 +49,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
||||
const { range_end, range_start } = allValues.v4;
|
||||
|
||||
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
|
||||
return i18next.t('greater_range_start_error');
|
||||
return intl.getMessage('greater_range_start_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -62,7 +61,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
||||
*/
|
||||
export const validateIpv4 = (value: any) => {
|
||||
if (value && !R_IPV4.test(value)) {
|
||||
return i18next.t('form_error_ip4_format');
|
||||
return intl.getMessage('form_error_ip4_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -91,7 +90,7 @@ export const validateNotInRange = (value: any, allValues: any) => {
|
||||
const isBelowMax = range_end && ip4ToInt(value) <= ip4ToInt(range_end);
|
||||
|
||||
if (isAboveMin && isBelowMax) {
|
||||
return i18next.t('out_of_range_error', {
|
||||
return intl.getMessage('out_of_range_error', {
|
||||
start: range_start,
|
||||
end: range_end,
|
||||
});
|
||||
@@ -107,16 +106,16 @@ export const validateNotInRange = (value: any, allValues: any) => {
|
||||
*/
|
||||
export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
||||
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
|
||||
return i18next.t('gateway_or_subnet_invalid');
|
||||
return intl.getMessage('gateway_or_subnet_invalid');
|
||||
}
|
||||
|
||||
const { subnet_mask, gateway_ip } = allValues.v4;
|
||||
|
||||
if (validateIpv4(gateway_ip)) {
|
||||
return i18next.t('gateway_or_subnet_invalid');
|
||||
return intl.getMessage('gateway_or_subnet_invalid');
|
||||
}
|
||||
|
||||
return parseSubnetMask(subnet_mask) ? undefined : i18next.t('gateway_or_subnet_invalid');
|
||||
return parseSubnetMask(subnet_mask) ? undefined : intl.getMessage('gateway_or_subnet_invalid');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,7 +137,7 @@ export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
||||
const subnetPrefix = parseSubnetMask(subnet_mask);
|
||||
|
||||
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
|
||||
return i18next.t('subnet_error');
|
||||
return intl.getMessage('subnet_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -164,7 +163,7 @@ export const validateClientId = (value: string) => {
|
||||
R_CLIENT_ID.test(formattedValue)
|
||||
)
|
||||
) {
|
||||
return i18next.t('form_error_client_id_format');
|
||||
return intl.getMessage('form_error_client_id_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -179,7 +178,7 @@ export const validateConfigClientId = (value: any) => {
|
||||
}
|
||||
const formattedValue = value.trim();
|
||||
if (formattedValue && !R_CLIENT_ID.test(formattedValue)) {
|
||||
return i18next.t('form_error_client_id_format');
|
||||
return intl.getMessage('form_error_client_id_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -194,7 +193,7 @@ export const validateServerName = (value: any) => {
|
||||
}
|
||||
const formattedValue = value ? value.trim() : value;
|
||||
if (formattedValue && !R_DOMAIN.test(formattedValue)) {
|
||||
return i18next.t('form_error_server_name');
|
||||
return intl.getMessage('form_error_server_name');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -205,7 +204,7 @@ export const validateServerName = (value: any) => {
|
||||
*/
|
||||
export const validateIpv6 = (value: any) => {
|
||||
if (value && !R_IPV6.test(value)) {
|
||||
return i18next.t('form_error_ip6_format');
|
||||
return intl.getMessage('form_error_ip6_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -216,7 +215,7 @@ export const validateIpv6 = (value: any) => {
|
||||
*/
|
||||
export const validateIp = (value: any) => {
|
||||
if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {
|
||||
return i18next.t('form_error_ip_format');
|
||||
return intl.getMessage('form_error_ip_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -227,7 +226,7 @@ export const validateIp = (value: any) => {
|
||||
*/
|
||||
export const validateMac = (value: any) => {
|
||||
if (value && !R_MAC.test(value)) {
|
||||
return i18next.t('form_error_mac_format');
|
||||
return intl.getMessage('form_error_mac_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -238,7 +237,7 @@ export const validateMac = (value: any) => {
|
||||
*/
|
||||
export const validatePort = (value: any) => {
|
||||
if ((value || value === 0) && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
||||
return i18next.t('form_error_port_range');
|
||||
return intl.getMessage('form_error_port_range');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -249,7 +248,7 @@ export const validatePort = (value: any) => {
|
||||
*/
|
||||
export const validateInstallPort = (value: any) => {
|
||||
if (value < 1 || value > MAX_PORT) {
|
||||
return i18next.t('form_error_port');
|
||||
return intl.getMessage('form_error_port');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -263,7 +262,7 @@ export const validatePortTLS = (value: any) => {
|
||||
return undefined;
|
||||
}
|
||||
if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
||||
return i18next.t('form_error_port_range');
|
||||
return intl.getMessage('form_error_port_range');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -280,7 +279,7 @@ export const validatePortQuic = validatePortTLS;
|
||||
*/
|
||||
export const validateIsSafePort = (value: any) => {
|
||||
if (UNSAFE_PORTS.includes(value)) {
|
||||
return i18next.t('form_error_port_unsafe');
|
||||
return intl.getMessage('form_error_port_unsafe');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -291,7 +290,7 @@ export const validateIsSafePort = (value: any) => {
|
||||
*/
|
||||
export const validateDomain = (value: any) => {
|
||||
if (value && !R_HOST.test(value)) {
|
||||
return i18next.t('form_error_domain_format');
|
||||
return intl.getMessage('form_error_domain_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -302,7 +301,7 @@ export const validateDomain = (value: any) => {
|
||||
*/
|
||||
export const validateAnswer = (value: any) => {
|
||||
if (value && !R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value)) {
|
||||
return i18next.t('form_error_answer_format');
|
||||
return intl.getMessage('form_error_answer_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -313,7 +312,7 @@ export const validateAnswer = (value: any) => {
|
||||
*/
|
||||
export const validatePath = (value: any) => {
|
||||
if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {
|
||||
return i18next.t('form_error_url_or_path_format');
|
||||
return intl.getMessage('form_error_url_or_path_format');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -324,7 +323,7 @@ export const validatePath = (value: any) => {
|
||||
*/
|
||||
export const validateIpv4InCidr = (valueIp: any, allValues: any) => {
|
||||
if (!isIpInCidr(valueIp, allValues.cidr)) {
|
||||
return i18next.t('form_error_subnet', { ip: valueIp, cidr: allValues.cidr });
|
||||
return intl.getMessage('form_error_subnet', { ip: valueIp, cidr: allValues.cidr });
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -349,7 +348,7 @@ export const validatePasswordLength = (value: any) => {
|
||||
const length = utf8StringLength(value);
|
||||
if (length < MIN_PASSWORD_LENGTH || length > MAX_PASSWORD_LENGTH) {
|
||||
// TODO: Make the i18n clearer with regards to bytes vs. characters.
|
||||
return i18next.t('form_error_password_length', {
|
||||
return intl.getMessage('form_error_password_length', {
|
||||
min: MIN_PASSWORD_LENGTH,
|
||||
max: MAX_PASSWORD_LENGTH,
|
||||
});
|
||||
@@ -365,7 +364,7 @@ export const validatePasswordLength = (value: any) => {
|
||||
*/
|
||||
export const validateIpGateway = (value: any, allValues: any) => {
|
||||
if (value === allValues.gatewayIp) {
|
||||
return i18next.t('form_error_gateway_ip');
|
||||
return intl.getMessage('form_error_gateway_ip');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -376,7 +375,7 @@ export const validateIpGateway = (value: any, allValues: any) => {
|
||||
*/
|
||||
export const validateIPv4Subnet = (value: any) => {
|
||||
if (!R_IPV4_SUBNET.test(value)) {
|
||||
return i18next.t('rate_limit_subnet_len_ipv4_error');
|
||||
return intl.getMessage('rate_limit_subnet_len_ipv4_error');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -387,7 +386,7 @@ export const validateIPv4Subnet = (value: any) => {
|
||||
*/
|
||||
export const validateIPv6Subnet = (value: any) => {
|
||||
if (!R_IPV6_SUBNET.test(value)) {
|
||||
return i18next.t('rate_limit_subnet_len_ipv6_error');
|
||||
return intl.getMessage('rate_limit_subnet_len_ipv6_error');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -401,7 +400,7 @@ export const validatePlainDns = (value: any, allValues: any) => {
|
||||
const { enabled } = allValues;
|
||||
|
||||
if (!enabled && !value) {
|
||||
return i18next.t('encryption_plain_dns_error');
|
||||
return intl.getMessage('encryption_plain_dns_error');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
.buttonGroup {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px 16px;
|
||||
padding: 16px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
@@ -28,3 +29,8 @@
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ set -e -f -u -x
|
||||
# for this database is https://github.com/AdguardTeam/companiesdb.
|
||||
#
|
||||
trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json'
|
||||
# TODO: Update output path to './client_v2/src/helpers/trackers/trackers.json' for new frontend migration
|
||||
# TODO(ik): Update output path to './client_v2/src/helpers/trackers/trackers.json' for new frontend migration
|
||||
output='./client/src/helpers/trackers/trackers.json'
|
||||
readonly trackers_url output
|
||||
|
||||
|
||||
@@ -25,13 +25,12 @@ import (
|
||||
"github.com/AdguardTeam/golibs/osutil"
|
||||
)
|
||||
|
||||
// TODO(ik): Update localesDir and srcDir to "./client_v2/src/" for new frontend migration
|
||||
const (
|
||||
twoskyConfFile = "./.twosky.json"
|
||||
// TODO: Update localesDir to "./client_v2/src/__locales" for new frontend migration
|
||||
localesDir = "./client/src/__locales"
|
||||
defaultBaseFile = "en.json"
|
||||
defaultProjectID = "home"
|
||||
// TODO: Update srcDir to "./client_v2/src" for new frontend migration
|
||||
srcDir = "./client/src"
|
||||
twoskyURI = "https://twosky.int.agrd.dev/api/v1"
|
||||
|
||||
@@ -79,8 +78,7 @@ func main() {
|
||||
usage("")
|
||||
}
|
||||
|
||||
// TODO: Support multiple projects in .twosky.json - add PROJECT_ID env var to select between 'home' and 'home_v2'
|
||||
// TODO: Modify readTwoskyConfig() to return all configs and add selectProject() function
|
||||
// TODO(ik): Support multiple projects in .twosky.json - add PROJECT_ID env var to select between 'home' and 'home_v2'
|
||||
conf := errors.Must(readTwoskyConfig())
|
||||
|
||||
var cli *twoskyClient
|
||||
@@ -110,7 +108,7 @@ func main() {
|
||||
// usage prints usage. If addStr is not empty print addStr and exit with code
|
||||
// 1, otherwise exit with code 0.
|
||||
func usage(addStr string) {
|
||||
// TODO: Add PROJECT_ID environment variable documentation when multi-project support is implemented
|
||||
// TODO(ik): Add PROJECT_ID environment variable documentation when multi-project support is implemented
|
||||
const usageStr = `Usage: go run main.go <command> [<args>]
|
||||
Commands:
|
||||
help
|
||||
@@ -166,7 +164,7 @@ func readTwoskyConfig() (t *twoskyConfig, err error) {
|
||||
return nil, fmt.Errorf("%q is empty", twoskyConfFile)
|
||||
}
|
||||
|
||||
// TODO: Currently only uses first project - modify to support project selection
|
||||
// TODO(ik): Currently only uses first project - modify to support project selection
|
||||
conf := tsc[0]
|
||||
|
||||
for _, lang := range conf.Languages {
|
||||
|
||||
@@ -95,7 +95,7 @@ func main() {
|
||||
|
||||
errors.Check(enc.Encode(aghFlt))
|
||||
|
||||
// TODO: Update output path to "client_v2/src/helpers/filters/filters.ts" for new frontend migration
|
||||
// TODO(ik): Update output path to "client_v2/src/helpers/filters/filters.ts" for new frontend migration
|
||||
err = maybe.WriteFile("client/src/helpers/filters/filters.ts", buf.Bytes(), 0o644)
|
||||
errors.Check(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user