mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Add better input validation for setup-keys, nameserver and routes (#373)
* Return the correct promise for errors * Update icon * Add better validation for routes * Add better validation for DNS * Add better validation for setup keys * Merge exit nodes to input validation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import type { ErrorResponse } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import classNames from "classnames";
|
||||
@@ -88,7 +89,7 @@ export default function Notification<T>({
|
||||
{loading ? (
|
||||
<Loader2 size={14} className={"animate-spin"} />
|
||||
) : error ? (
|
||||
<XIcon size={14} />
|
||||
<IconCircleX size={24} />
|
||||
) : (
|
||||
icon || <CheckIcon size={14} />
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,7 @@ const RoutesContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function RoutesProvider({ children }: Props) {
|
||||
const routeRequest = useApiCall<Route>("/routes");
|
||||
const routeRequest = useApiCall<Route>("/routes", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const updateRoute = async (
|
||||
|
||||
@@ -115,7 +115,7 @@ export function NameserverModalContent({
|
||||
preset,
|
||||
cell,
|
||||
}: ModalProps) {
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers");
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const isUpdate = useMemo(() => {
|
||||
@@ -233,24 +233,31 @@ export function NameserverModalContent({
|
||||
return domains.some((d) => d.name === "");
|
||||
}, [domains]);
|
||||
|
||||
const nameLengthError = useMemo(() => {
|
||||
if (name.length > 40) return "Name should be less than 40 characters";
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
const hasAnyError = useMemo(() => {
|
||||
return (
|
||||
hasNSErrors ||
|
||||
nsError ||
|
||||
domainError ||
|
||||
name == "" ||
|
||||
nameservers.length == 0 ||
|
||||
hasDomainErrors ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
nameLengthError !== "" ||
|
||||
name == ""
|
||||
);
|
||||
}, [
|
||||
nsError,
|
||||
domainError,
|
||||
name,
|
||||
nameservers,
|
||||
groups,
|
||||
hasNSErrors,
|
||||
hasDomainErrors,
|
||||
nameLengthError,
|
||||
name,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -427,6 +434,7 @@ export function NameserverModalContent({
|
||||
<Input
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
error={nameLengthError}
|
||||
placeholder={"e.g., Public DNS"}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -516,7 +524,7 @@ function NameserverInput({
|
||||
const validCIDR = cidr.isValidAddress(ip);
|
||||
if (!validCIDR) {
|
||||
onError && onError(true);
|
||||
return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
|
||||
return "Please enter a valid IP, e.g., 192.168.1.0";
|
||||
}
|
||||
onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -532,7 +540,7 @@ function NameserverInput({
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={"IP"}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
placeholder={"e.g., 172.16.0.0"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={ip}
|
||||
className={"font-mono !text-[13px]"}
|
||||
|
||||
@@ -183,17 +183,37 @@ export function RouteModalContent({
|
||||
(cidrError && cidrError.length > 1) ||
|
||||
(peerTab === "peer-group" && routingPeerGroups.length == 0) ||
|
||||
(peerTab === "routing-peer" && !routingPeer) ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
networkRange == ""
|
||||
);
|
||||
}, [cidrError, peerTab, routingPeerGroups.length, routingPeer, groups]);
|
||||
}, [
|
||||
cidrError,
|
||||
peerTab,
|
||||
routingPeerGroups.length,
|
||||
routingPeer,
|
||||
groups,
|
||||
networkRange,
|
||||
]);
|
||||
|
||||
const isNameEntered = useMemo(() => {
|
||||
return !(networkIdentifier == "");
|
||||
const networkIdentifierError = useMemo(() => {
|
||||
return (networkIdentifier?.length || 0) > 40
|
||||
? "Network Identifier must be less than 40 characters"
|
||||
: "";
|
||||
}, [networkIdentifier]);
|
||||
|
||||
const metricError = useMemo(() => {
|
||||
return parseInt(metric) < 1 || parseInt(metric) > 9999
|
||||
? "Metric must be between 1 and 9999"
|
||||
: "";
|
||||
}, [metric]);
|
||||
|
||||
const isNameEntered = useMemo(() => {
|
||||
return networkIdentifier != "" && networkIdentifierError == "";
|
||||
}, [networkIdentifier, networkIdentifierError]);
|
||||
|
||||
const canCreateOrSave = useMemo(() => {
|
||||
return isNetworkEntered && isNameEntered;
|
||||
}, [isNetworkEntered, isNameEntered]);
|
||||
return isNetworkEntered && isNameEntered && metricError == "";
|
||||
}, [isNetworkEntered, isNameEntered, metricError]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
@@ -250,7 +270,10 @@ export function RouteModalContent({
|
||||
/>
|
||||
Name & Description
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"settings"} disabled={!canCreateOrSave}>
|
||||
<TabsTrigger
|
||||
value={"settings"}
|
||||
disabled={!isNetworkEntered || !isNameEntered}
|
||||
>
|
||||
<Settings2
|
||||
size={16}
|
||||
className={
|
||||
@@ -340,6 +363,7 @@ export function RouteModalContent({
|
||||
Add a unique network identifier that is assigned to each device.
|
||||
</HelpText>
|
||||
<Input
|
||||
error={networkIdentifierError}
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
ref={nameRef}
|
||||
@@ -406,6 +430,8 @@ export function RouteModalContent({
|
||||
max={9999}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
value={metric}
|
||||
error={metricError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setMetric(e.target.value)}
|
||||
customPrefix={
|
||||
@@ -469,7 +495,7 @@ export function RouteModalContent({
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("settings")}
|
||||
disabled={!canCreateOrSave}
|
||||
disabled={!isNameEntered || !isNetworkEntered}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
|
||||
@@ -197,14 +197,21 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
.filter((p) => p != undefined) as string[];
|
||||
}, [groupedRoute]);
|
||||
|
||||
const metricError = useMemo(() => {
|
||||
return parseInt(metric.toString()) < 1 || parseInt(metric.toString()) > 9999
|
||||
? "Metric must be between 1 and 9999"
|
||||
: "";
|
||||
}, [metric]);
|
||||
|
||||
// Is button disabled
|
||||
const isDisabled = useMemo(() => {
|
||||
return (
|
||||
(peerTab === "peer-group" && routingPeerGroups.length == 0) ||
|
||||
(peerTab === "routing-peer" && !routingPeer) ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
metricError !== ""
|
||||
);
|
||||
}, [peerTab, routingPeerGroups.length, routingPeer, groups]);
|
||||
}, [peerTab, routingPeerGroups.length, routingPeer, groups, metricError]);
|
||||
|
||||
const [tab, setTab] = useState(
|
||||
cell && cell == "metric" ? "settings" : "network",
|
||||
@@ -352,6 +359,8 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
max={9999}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
value={metric}
|
||||
error={metricError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setMetric(e.target.value)}
|
||||
customPrefix={
|
||||
|
||||
@@ -126,7 +126,7 @@ type ModalProps = {
|
||||
};
|
||||
|
||||
export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys");
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
@@ -143,10 +143,18 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
return reusable ? "Unlimited" : "1";
|
||||
}, [reusable]);
|
||||
|
||||
const expiresInError = useMemo(() => {
|
||||
const expires = parseInt(expiresIn);
|
||||
if (expires < 1 || expires > 365) {
|
||||
return "Days should be between 1 and 365";
|
||||
}
|
||||
return "";
|
||||
}, [expiresIn]);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
const trimmedName = trim(name);
|
||||
return trimmedName.length === 0;
|
||||
}, [name]);
|
||||
return trimmedName.length === 0 || expiresInError.length > 0;
|
||||
}, [name, expiresInError]);
|
||||
|
||||
const submit = () => {
|
||||
if (!selectedGroups) return;
|
||||
@@ -245,6 +253,8 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
min={1}
|
||||
max={365}
|
||||
value={expiresIn}
|
||||
error={expiresInError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setExpiresIn(e.target.value)}
|
||||
customPrefix={
|
||||
|
||||
@@ -169,13 +169,13 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
return login(currentPath);
|
||||
}
|
||||
if (err.code == 401 && err.message == "token invalid") {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
if (err.code == 500 && err.message == "internal server error") {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
if (err.code > 400 && err.code <= 500) {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
|
||||
Reference in New Issue
Block a user