Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { type SubmitHandler, useForm } from 'react-hook-form';
import type { Subject } from 'rxjs';
import { z } from 'zod';

import { useI18nContext } from '../../../../../i18n/i18n-react';
import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput';
Expand All @@ -19,20 +18,23 @@ import type { ToggleOption } from '../../../../../shared/defguard-ui/components/
import useApi from '../../../../../shared/hooks/useApi';
import { useToaster } from '../../../../../shared/hooks/useToaster';
import type { GetAvailableLocationIpResponse } from '../../../../../shared/types';
import { validateWireguardPublicKey } from '../../../../../shared/validators';
import {
type AddStandaloneDeviceFormFields,
WGConfigGenChoice,
} from '../../AddStandaloneDeviceModal/types';
import { StandaloneDeviceModalFormMode } from '../types';
import {
type StandaloneDeviceFormFields,
standaloneDeviceFormSchema,
} from './formSchema';

type Props = {
onSubmit: (formValues: AddStandaloneDeviceFormFields) => Promise<void>;
mode: StandaloneDeviceModalFormMode;
onLoadingChange: (value: boolean) => void;
locationOptions: SelectOption<number>[];
submitSubject: Subject<void>;
defaults: AddStandaloneDeviceFormFields;
defaults: StandaloneDeviceFormFields;
reservedNames: string[];
initialIpRecommendation: GetAvailableLocationIpResponse;
};
Expand All @@ -57,7 +59,6 @@ export const StandaloneDeviceModalForm = ({
// auto assign upon location change is happening
const [ipIsLoading, setIpIsLoading] = useState(false);
const localLL = LL.modals.addStandaloneDevice.form;
const errors = LL.form.error;
const labels = localLL.labels;
const submitRef = useRef<HTMLInputElement | null>(null);
const toaster = useToaster();
Expand Down Expand Up @@ -99,46 +100,12 @@ export const StandaloneDeviceModalForm = ({

const schema = useMemo(
() =>
z
.object({
name: z
.string()
.min(1, LL.form.error.required())
.refine((value) => {
if (mode === StandaloneDeviceModalFormMode.EDIT) {
const filtered = reservedNames.filter((n) => n !== defaults.name.trim());
return !filtered.includes(value.trim());
}
return !reservedNames.includes(value.trim());
}, LL.form.error.reservedName()),
location_id: z.number(),
description: z.string().optional(),
modifiableIpParts: z.array(z.string().min(1, LL.form.error.required())),
generationChoice: z.nativeEnum(WGConfigGenChoice),
wireguard_pubkey: z.string().optional(),
})
.superRefine((vals, ctx) => {
if (mode === StandaloneDeviceModalFormMode.CREATE_MANUAL) {
if (vals.generationChoice === WGConfigGenChoice.MANUAL) {
const result = validateWireguardPublicKey({
requiredError: errors.required(),
maxError: errors.maximumLengthOf({ length: 44 }),
minError: errors.minimumLengthOf({ length: 44 }),
validKeyError: errors.invalid(),
}).safeParse(vals.wireguard_pubkey);
if (!result.success) {
result.error.errors.forEach((e) => {
ctx.addIssue({
path: ['wireguard_pubkey'],
message: e.message,
code: 'custom',
});
});
}
}
}
}),
[LL.form.error, defaults.name, errors, mode, reservedNames],
standaloneDeviceFormSchema(LL, {
mode,
reservedNames,
originalName: defaults.name,
}),
[mode, reservedNames, defaults.name, LL],
);

const {
Expand All @@ -163,9 +130,7 @@ export const StandaloneDeviceModalForm = ({
const formIpsSet = new Set<string>(formIps);
return Array.from(formIpsSet.difference(initialIpsSet));
}
const submitHandler: SubmitHandler<AddStandaloneDeviceFormFields> = async (
formValues,
) => {
const submitHandler: SubmitHandler<StandaloneDeviceFormFields> = async (formValues) => {
const values = formValues;
const { modifiableIpParts: modifiableIpPart } = values;
values.description = values.description?.trim();
Expand All @@ -181,29 +146,40 @@ export const StandaloneDeviceModalForm = ({
await onSubmit(values);
return;
}
try {
const response = await validateLocationIp({
ips: newIps(values.modifiableIpParts),
location: values.location_id,
});
const { available, valid } = response;
if (available && valid) {
await onSubmit(values);
} else {
const ips = newIps(values.modifiableIpParts);
let validationErrors = false;
let index = 0;
for (const newIp of ips) {
try {
const response = await validateLocationIp({
ips: [newIp],
location: values.location_id,
});
const { available, valid } = response;
if (!available) {
setError('modifiableIpParts', {
validationErrors = true;
setError(`modifiableIpParts.${index}`, {
message: LL.form.error.reservedIp(),
});
}
if (!valid) {
setError('modifiableIpParts', {
validationErrors = true;
setError(`modifiableIpParts.${index}`, {
message: LL.form.error.invalidIp(),
});
}
} catch (_) {
validationErrors = true;
} finally {
index++;
}
}
if (!validationErrors) {
try {
await onSubmit(values);
} catch (_) {
toaster.error(LL.messages.error());
}
} catch (e) {
toaster.error(LL.messages.error());
console.error(e);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import z from 'zod';
import type { TranslationFunctions } from '../../../../../i18n/i18n-types';
import { isPresent } from '../../../../../shared/defguard-ui/utils/isPresent';
import { validateWireguardPublicKey } from '../../../../../shared/validators';
import { WGConfigGenChoice } from '../../AddStandaloneDeviceModal/types';
import { StandaloneDeviceModalFormMode } from '../types';

type SchemaProps = {
mode: StandaloneDeviceModalFormMode;
reservedNames: string[];
originalName?: string;
};

export const standaloneDeviceFormSchema = (
LL: TranslationFunctions,
{ mode, reservedNames, originalName }: SchemaProps,
) => {
const errors = LL.form.error;

return z
.object({
name: z
.string()
.min(1, LL.form.error.required())
.refine((value) => {
if (mode === StandaloneDeviceModalFormMode.EDIT && isPresent(originalName)) {
const filtered = reservedNames.filter((n) => n !== originalName.trim());
return !filtered.includes(value.trim());
}
return !reservedNames.includes(value.trim());
}, LL.form.error.reservedName()),
location_id: z.number(),
description: z.string().optional(),
modifiableIpParts: z.array(z.string().min(1, LL.form.error.required())),
generationChoice: z.nativeEnum(WGConfigGenChoice),
wireguard_pubkey: z.string().optional(),
})
.superRefine((vals, ctx) => {
if (mode === StandaloneDeviceModalFormMode.CREATE_MANUAL) {
if (vals.generationChoice === WGConfigGenChoice.MANUAL) {
const result = validateWireguardPublicKey({
requiredError: errors.required(),
maxError: errors.maximumLengthOf({ length: 44 }),
minError: errors.minimumLengthOf({ length: 44 }),
validKeyError: errors.invalid(),
}).safeParse(vals.wireguard_pubkey);
if (!result.success) {
result.error.errors.forEach((e) => {
ctx.addIssue({
path: ['wireguard_pubkey'],
message: e.message,
code: 'custom',
});
});
}
}
}
});
};

export type StandaloneDeviceFormFields = z.infer<
ReturnType<typeof standaloneDeviceFormSchema>
>;