diff --git a/deno.json b/deno.json index 2ab367be..b5ed748e 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,10 @@ { "imports": { + "@meshtastic/core": "jsr:@meshtastic/core@^2.6.2", + "@meshtastic/js": "jsr:@meshtastic/js@^2.3.4", + "@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1", + "@meshtastic/transport-web-bluetooth": "jsr:@meshtastic/transport-web-bluetooth@^0.1.1", + "@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1", "@app/": "./src/", "@pages/": "./src/pages/", "@components/": "./src/components/", diff --git a/deno.lock b/deno.lock index 46190061..7d73615b 100644 --- a/deno.lock +++ b/deno.lock @@ -1,11 +1,15 @@ { "version": "4", "specifiers": { + "jsr:@meshtastic/core@^2.6.0": "2.6.2", + "jsr:@meshtastic/core@^2.6.2": "2.6.2", + "jsr:@meshtastic/js@^2.3.4": "2.3.4", + "jsr:@meshtastic/protobufs@^2.3.12": "2.6.2", + "jsr:@meshtastic/protobufs@^2.6.2": "2.6.2", + "jsr:@meshtastic/transport-http@~0.2.1": "0.2.1", + "jsr:@meshtastic/transport-web-bluetooth@~0.1.1": "0.1.1", + "jsr:@meshtastic/transport-web-serial@~0.2.1": "0.2.1", "npm:@bufbuild/protobuf@^2.2.3": "2.2.3", - "npm:@jsr/meshtastic__core@2.6.0-0": "2.6.0-0", - "npm:@jsr/meshtastic__js@2.6.0-0": "2.6.0-0", - "npm:@jsr/meshtastic__transport-http@*": "0.2.1", - "npm:@jsr/meshtastic__transport-web-serial@*": "0.2.1", "npm:@noble/curves@^1.8.1": "1.8.1", "npm:@radix-ui/react-accordion@^1.2.3": "1.2.3_@types+react@19.0.10_@types+react-dom@19.0.4__@types+react@19.0.10_react@19.0.0_react-dom@19.0.0__react@19.0.0", "npm:@radix-ui/react-checkbox@^1.1.4": "1.1.4_@types+react@19.0.10_@types+react-dom@19.0.4__@types+react@19.0.10_react@19.0.0_react-dom@19.0.0__react@19.0.0", @@ -73,8 +77,48 @@ "npm:vite@*": "6.2.0_@types+node@22.13.8", "npm:vite@^6.2.0": "6.2.0_@types+node@22.13.8", "npm:vitest@^3.0.7": "3.0.8_@types+node@22.13.8_happy-dom@17.2.2_vite@6.2.0__@types+node@22.13.8_@vitest+browser@3.0.8__playwright@1.50.1__vitest@3.0.8___@types+node@22.13.8___happy-dom@17.2.2___@vitest+browser@3.0.8____playwright@1.50.1____vitest@3.0.8____msw@2.7.3_____typescript@5.8.2_____@types+node@22.13.8____vite@6.2.0_____@types+node@22.13.8____typescript@5.8.2____@types+node@22.13.8____happy-dom@17.2.2___playwright@1.50.1___vite@6.2.0____@types+node@22.13.8___typescript@5.8.2__vitest@3.0.8__typescript@5.8.2__msw@2.7.3___typescript@5.8.2___@types+node@22.13.8__vite@6.2.0___@types+node@22.13.8__@types+node@22.13.8_playwright@1.50.1_typescript@5.8.2", + "npm:zod@^3.24.2": "3.24.2", "npm:zustand@5.0.3": "5.0.3_@types+react@19.0.10_immer@10.1.1_react@19.0.0" }, + "jsr": { + "@meshtastic/core@2.6.2": { + "integrity": "5c948bbbfad280c5eb093c62edc84773f76509487b333066ec4a349f40dcacf2", + "dependencies": [ + "jsr:@meshtastic/protobufs@^2.6.2", + "npm:@bufbuild/protobuf", + "npm:crc", + "npm:ste-simple-events", + "npm:tslog@^4.9.3" + ] + }, + "@meshtastic/js@2.3.4": { + "integrity": "7a81a36fb7ef1b7b68a3989c02d50f687114ac56bcd7f0452a31ef560ac99719", + "dependencies": [ + "jsr:@meshtastic/protobufs@^2.3.12", + "npm:crc", + "npm:ste-simple-events", + "npm:tslog@^4.9.2" + ] + }, + "@meshtastic/protobufs@2.6.2": { + "integrity": "55e9b98fc22ea0d28e6a7979e4ff0a5f2c94513c1bc93e67522636a89925ad69", + "dependencies": [ + "npm:@bufbuild/protobuf" + ] + }, + "@meshtastic/transport-http@0.2.1": { + "integrity": "4d086ee6d5665c3490736737c4354eb3049edf792b1d195b30a3254cb535a7d6" + }, + "@meshtastic/transport-web-bluetooth@0.1.1": { + "integrity": "f7676b98e2049ad0bca508e34054730b22cf2648019921989f11297441fe958d" + }, + "@meshtastic/transport-web-serial@0.2.1": { + "integrity": "d09fa8ac278b105c8f2b3a72af9cf8a5676baac6f4e9111c6773ff6217e2d5be", + "dependencies": [ + "jsr:@meshtastic/core@^2.6.0" + ] + } + }, "npm": { "@adobe/css-tools@4.4.2": { "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==" @@ -1082,54 +1126,6 @@ "@jridgewell/sourcemap-codec" ] }, - "@jsr/meshtastic__core@2.6.0": { - "integrity": "sha512-+Ik6gzZnfi5sW+WC06bRayA6KGF2NI+zi3bqKbvA8mGDNSOPgsFhA4VZ79DKY4bSflTW170MRIUeyYo0IWQQuw==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__core@2.6.0-0": { - "integrity": "sha512-Ks71sRagbBipotznULpsJZ1EMcQIqCEJQx6mf628dmCNVf2YECi2zi/i/5zErp1hGPgfbDvCz9oPogvsd/7fMA==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__js@2.6.0-0": { - "integrity": "sha512-+xpZpxK6oUIVOuEs7C+LyxRr2druvc7UNNNTK9Rl8ioXj63Jz1uQXlYe2Gj0xjnRAiSQLR7QVaPef21BR/YTxA==", - "dependencies": [ - "@bufbuild/protobuf", - "@jsr/meshtastic__protobufs", - "crc", - "ste-simple-events", - "tslog" - ] - }, - "@jsr/meshtastic__protobufs@2.6.0": { - "integrity": "sha512-CGlgBdzAuQCZuGPrnzP8zU+EcLlmyYeeMbqFHuJ834cYfArWXDjDh1UYaPo2rI03LTjqa3MeWpfqDlzBR8kIMg==", - "dependencies": [ - "@bufbuild/protobuf" - ] - }, - "@jsr/meshtastic__transport-http@0.2.1": { - "integrity": "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg==", - "dependencies": [ - "@jsr/meshtastic__core@2.6.0" - ] - }, - "@jsr/meshtastic__transport-web-serial@0.2.1": { - "integrity": "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q==", - "dependencies": [ - "@jsr/meshtastic__core@2.6.0" - ] - }, "@mapbox/geojson-rewind@0.5.2": { "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", "dependencies": [ @@ -6869,6 +6865,9 @@ "yoctocolors-cjs@2.1.2": { "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==" }, + "zod@3.24.2": { + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" + }, "zone.js@0.8.29": { "integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ==" }, @@ -6882,13 +6881,16 @@ } }, "workspace": { + "dependencies": [ + "jsr:@meshtastic/core@^2.6.2", + "jsr:@meshtastic/js@^2.3.4", + "jsr:@meshtastic/transport-http@~0.2.1", + "jsr:@meshtastic/transport-web-bluetooth@~0.1.1", + "jsr:@meshtastic/transport-web-serial@~0.2.1" + ], "packageJson": { "dependencies": [ "npm:@bufbuild/protobuf@^2.2.3", - "npm:@jsr/meshtastic__core@2.6.0-0", - "npm:@jsr/meshtastic__js@2.6.0-0", - "npm:@jsr/meshtastic__transport-http@*", - "npm:@jsr/meshtastic__transport-web-serial@*", "npm:@noble/curves@^1.8.1", "npm:@radix-ui/react-accordion@^1.2.3", "npm:@radix-ui/react-checkbox@^1.1.4", @@ -6950,6 +6952,7 @@ "npm:vite-plugin-pwa@~0.21.1", "npm:vite@^6.2.0", "npm:vitest@^3.0.7", + "npm:zod@^3.24.2", "npm:zustand@5.0.3" ] } diff --git a/package.json b/package.json index 51b99b39..f161c710 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,6 @@ "homepage": "https://meshtastic.org", "dependencies": { "@bufbuild/protobuf": "^2.2.3", - "@meshtastic/core": "npm:@jsr/meshtastic__core@2.6.0-0", - "@meshtastic/js": "npm:@jsr/meshtastic__js@2.6.0-0", - "@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http", - "@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial", "@noble/curves": "^1.8.1", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-checkbox": "^1.1.4", @@ -73,6 +69,7 @@ "react-qrcode-logo": "^3.0.0", "rfc4648": "^1.5.4", "vite-plugin-node-polyfills": "^0.23.0", + "zod": "^3.24.2", "zustand": "5.0.3" }, "devDependencies": { diff --git a/src/components/Form/FormSelect.tsx b/src/components/Form/FormSelect.tsx index 037b651c..42be6c11 100644 --- a/src/components/Form/FormSelect.tsx +++ b/src/components/Form/FormSelect.tsx @@ -10,13 +10,14 @@ import { SelectValue, } from "@components/UI/Select.tsx"; import { useController, type FieldValues } from "react-hook-form"; -import { computeHeadingLevel } from "@core/utils/test.tsx"; export interface SelectFieldProps extends BaseFormBuilderProps { type: "select"; selectChange?: (e: string, name: string) => void; validate?: (newValue: string) => Promise; + defaultValue?: string; properties: BaseFormBuilderProps["properties"] & { + defaultValue?: T; enumValue: { [s: string]: string | number; }; @@ -70,7 +71,6 @@ export function SelectInput({ onChange(Number.parseInt(newValue)); }; - return ( { +// const mockData = { role: e.target.value }; +// onSubmit(mockData); +// }} +// > +// {Object.entries(Protobuf.Config.Config_DeviceConfig_Role).map(([key, value]) => ( +// +// ))} +// +// +// +// ); +// }) +// })); + +// describe('Network component', () => { +// const setWorkingConfigMock = vi.fn(); +// const mockDeviceConfig = { +// role: "CLIENT", +// buttonGpio: 0, +// buzzerGpio: 0, +// rebroadcastMode: "ALL", +// nodeInfoBroadcastSecs: 300, +// doubleTapAsButtonPress: false, +// disableTripleClick: false, +// ledHeartbeatDisabled: false, +// }; + +// beforeEach(() => { +// vi.resetAllMocks(); + +// (useDevice as any).mockReturnValue({ +// config: { +// device: mockDeviceConfig +// }, +// setWorkingConfig: setWorkingConfigMock +// }); + +// }); + +// afterEach(() => { +// vi.clearAllMocks(); +// }); + +// it('should render the Network form', () => { +// render(); +// expect(screen.getByTestId('dynamic-form')).toBeInTheDocument(); +// }); + +// it('should call setWorkingConfig when form is submitted', async () => { +// render(); + +// fireEvent.click(screen.getByTestId('submit-button')); + +// await waitFor(() => { +// expect(setWorkingConfigMock).toHaveBeenCalledWith( +// expect.objectContaining({ +// payloadVariant: { +// case: "device", +// value: expect.objectContaining({ role: "CLIENT" }) +// } +// }) +// ); +// }); +// }); + + +// it('should create config with proper structure', async () => { +// render(); + +// // Simulate form submission +// fireEvent.click(screen.getByTestId('submit-button')); + +// await waitFor(() => { +// expect(setWorkingConfigMock).toHaveBeenCalledWith( +// expect.objectContaining({ +// payloadVariant: { +// case: "network", +// value: expect.any(Object) +// } +// }) +// ); +// }); +// }); +// }); + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Network } from '@components/PageComponents/Config/Network/index.tsx'; +import { useDevice } from "@core/stores/deviceStore.ts"; +import { Protobuf } from "@meshtastic/core"; + +vi.mock('@core/stores/deviceStore', () => ({ + useDevice: vi.fn() +})); + +vi.mock('@components/Form/DynamicForm', async () => { + const React = await import('react'); + const { useState } = React; + + return { + DynamicForm: ({ onSubmit, defaultValues }: any) => { + const [wifiEnabled, setWifiEnabled] = useState(defaultValues.wifiEnabled ?? false); + const [ssid, setSsid] = useState(defaultValues.wifiSsid ?? ''); + const [psk, setPsk] = useState(defaultValues.wifiPsk ?? ''); + + return ( +
{ + e.preventDefault(); + onSubmit({ + ...defaultValues, + wifiEnabled, + wifiSsid: ssid, + wifiPsk: psk, + }); + }} + data-testid="dynamic-form" + > + setWifiEnabled(e.target.checked)} + /> + setSsid(e.target.value)} + disabled={!wifiEnabled} + /> + setPsk(e.target.value)} + disabled={!wifiEnabled} + /> + +
+ ); + }, + }; +}); +; + +describe('Network component', () => { + const setWorkingConfigMock = vi.fn(); + const mockNetworkConfig = { + wifiEnabled: false, + wifiSsid: '', + wifiPsk: '', + ntpServer: '', + ethEnabled: false, + addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, + ipv4Config: { + ip: 0, + gateway: 0, + subnet: 0, + dns: 0, + }, + enabledProtocols: + Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, + rsyslogServer: '', + }; + + beforeEach(() => { + vi.resetAllMocks(); + + (useDevice as any).mockReturnValue({ + config: { + network: mockNetworkConfig + }, + setWorkingConfig: setWorkingConfigMock + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the Network form', () => { + render(); + expect(screen.getByTestId('dynamic-form')).toBeInTheDocument(); + }); + + it('should disable SSID and PSK fields when wifi is off', () => { + render(); + expect(screen.getByLabelText("SSID")).toBeDisabled(); + expect(screen.getByLabelText("PSK")).toBeDisabled(); + }); + + it('should enable SSID and PSK when wifi is toggled on', async () => { + render(); + const toggle = screen.getByLabelText("WiFi Enabled"); + screen.debug() + + fireEvent.click(toggle); // turns wifiEnabled = true + + await waitFor(() => { + expect(screen.getByLabelText("SSID")).not.toBeDisabled(); + expect(screen.getByLabelText("PSK")).not.toBeDisabled(); + }); + }); + + it('should call setWorkingConfig with the right structure on submit', async () => { + render(); + + fireEvent.click(screen.getByTestId("submit-button")); + + await waitFor(() => { + expect(setWorkingConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + payloadVariant: { + case: "network", + value: expect.objectContaining({ + wifiEnabled: false, + wifiSsid: '', + wifiPsk: '', + ntpServer: '', + ethEnabled: false, + rsyslogServer: '', + }) + } + }) + ); + }); + }); + + it('should submit valid data after enabling wifi and entering SSID and PSK', async () => { + render(); + fireEvent.click(screen.getByLabelText("WiFi Enabled")); + + fireEvent.change(screen.getByLabelText("SSID"), { + target: { value: "MySSID" } + }); + + fireEvent.change(screen.getByLabelText("PSK"), { + target: { value: "MySecretPSK" } + }); + + fireEvent.click(screen.getByTestId("submit-button")); + + await waitFor(() => { + expect(setWorkingConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + payloadVariant: { + case: "network", + value: expect.objectContaining({ + wifiEnabled: true, + wifiSsid: "MySSID", + wifiPsk: "MySecretPSK" + }) + } + }) + ); + }); + }); +}); diff --git a/src/components/PageComponents/Config/Network.tsx b/src/components/PageComponents/Config/Network/index.tsx similarity index 81% rename from src/components/PageComponents/Config/Network.tsx rename to src/components/PageComponents/Config/Network/index.tsx index 1c991cb4..cae121d6 100644 --- a/src/components/PageComponents/Config/Network.tsx +++ b/src/components/PageComponents/Config/Network/index.tsx @@ -1,4 +1,4 @@ -import type { NetworkValidation } from "@app/validation/config/network.tsx"; +import { NetworkValidationSchema, type NetworkValidation } from "@app/validation/config/network.ts"; import { create } from "@bufbuild/protobuf"; import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; @@ -7,11 +7,18 @@ import { convertIpAddressToInt, } from "@core/utils/ip.ts"; import { Protobuf } from "@meshtastic/core"; +import { validateSchema } from "@app/validation/validate.ts"; export const Network = () => { const { config, setWorkingConfig } = useDevice(); const onSubmit = (data: NetworkValidation) => { + const result = validateSchema(NetworkValidationSchema, data); + + if (!result.success) { + console.error("Validation errors:", result.errors); + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -21,10 +28,10 @@ export const Network = () => { ipv4Config: create( Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, { - ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0, - gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0, - subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0, - dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0, + ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""), + gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""), + subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""), + dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""), }, ), }, @@ -48,6 +55,8 @@ export const Network = () => { ), dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0), }, + enabledProtocols: config.network?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST + }} fieldGroups={[ { @@ -165,6 +174,22 @@ export const Network = () => { }, ], }, + { + label: "UDP Config", + description: "UDP over Mesh configuration", + fields: [ + { + type: "select", + name: "enabledProtocols", + label: "Mesh via UDP", + properties: { + enumValue: + Protobuf.Config.Config_NetworkConfig_ProtocolFlags, + formatEnumName: true, + } + }, + ], + }, { label: "NTP Config", description: "NTP configuration", diff --git a/src/core/utils/ip.ts b/src/core/utils/ip.ts index 0dbc5ca5..70ac97f6 100644 --- a/src/core/utils/ip.ts +++ b/src/core/utils/ip.ts @@ -1,10 +1,12 @@ export function convertIntToIpAddress(int: number): string { - return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${ - (int >> 24) & 0xff - }`; + return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff + }`; } -export function convertIpAddressToInt(ip: string): number | null { +export function convertIpAddressToInt(ip: string): number | undefined { + if (!ip) { + return undefined; + } return ( ip .split(".") diff --git a/src/pages/Config/DeviceConfig.tsx b/src/pages/Config/DeviceConfig.tsx index 45ae5f19..b97e8640 100644 --- a/src/pages/Config/DeviceConfig.tsx +++ b/src/pages/Config/DeviceConfig.tsx @@ -2,7 +2,7 @@ import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx"; import { Device } from "../../components/PageComponents/Config/Device/index.tsx"; import { Display } from "@components/PageComponents/Config/Display.tsx"; import { LoRa } from "@components/PageComponents/Config/LoRa.tsx"; -import { Network } from "@components/PageComponents/Config/Network.tsx"; +import { Network } from "../../components/PageComponents/Config/Network/index.tsx"; import { Position } from "@components/PageComponents/Config/Position.tsx"; import { Power } from "@components/PageComponents/Config/Power.tsx"; import { Security } from "../../components/PageComponents/Config/Security/Security.tsx"; @@ -31,7 +31,6 @@ export const DeviceConfig = () => { { label: "Network", element: Network, - // disabled: !metadata.get(0)?.hasWifi, }, { label: "Display", diff --git a/src/validation/config/network.ts b/src/validation/config/network.ts index f37796cc..9268128b 100644 --- a/src/validation/config/network.ts +++ b/src/validation/config/network.ts @@ -1,61 +1,27 @@ -import type { Message } from "@bufbuild/protobuf"; +import { z } from "zod"; import { Protobuf } from "@meshtastic/core"; -import { - IsBoolean, - IsEnum, - IsIP, - IsOptional, - IsString, - Length, -} from "class-validator"; -export class NetworkValidation - implements - Omit { - @IsBoolean() - wifiEnabled: boolean; +const AddressModeEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_AddressMode); +const ProtocolFlagsEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_ProtocolFlags); - @Length(1, 33) - @IsOptional({}) - wifiSsid: string; +export const NetworkValidationIpV4ConfigSchema = z.object({ + ip: z.string().ip(), + gateway: z.string().ip(), + subnet: z.string().ip(), + dns: z.string().ip(), +}); - @Length(8, 64) - @IsOptional() - wifiPsk: string; +export const NetworkValidationSchema = z.object({ + wifiEnabled: z.boolean(), + wifiSsid: z.string().min(0).max(33).optional(), + wifiPsk: z.string().min(0).max(64).optional(), + ntpServer: z.string().min(2).max(30), + ethEnabled: z.boolean(), + addressMode: AddressModeEnum, + ipv4Config: NetworkValidationIpV4ConfigSchema.optional(), + enabledProtocols: ProtocolFlagsEnum, + rsyslogServer: z.string(), +}); - @Length(2, 30) - ntpServer: string; +export type NetworkValidation = z.infer; - @IsBoolean() - ethEnabled: boolean; - - @IsEnum(Protobuf.Config.Config_NetworkConfig_AddressMode) - addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode; - - ipv4Config: NetworkValidationIpV4Config; - - @IsString() - rsyslogServer: string; -} - -export class NetworkValidationIpV4Config implements - Omit< - Protobuf.Config.Config_NetworkConfig_IpV4Config, - keyof Message | "ip" | "gateway" | "subnet" | "dns" - > { - @IsIP() - @IsOptional() - ip: string; - - @IsIP() - @IsOptional() - gateway: string; - - @IsIP() - @IsOptional() - subnet: string; - - @IsIP() - @IsOptional() - dns: string; -} diff --git a/src/validation/validate.ts b/src/validation/validate.ts new file mode 100644 index 00000000..0969a1de --- /dev/null +++ b/src/validation/validate.ts @@ -0,0 +1,13 @@ +import { ZodError, ZodSchema } from "zod"; + +export function validateSchema( + schema: ZodSchema, + data: unknown +): { success: true; data: T } | { success: false; errors: ZodError["issues"] } { + const result = schema.safeParse(data); + if (result.success) { + return { success: true, data: result.data }; + } else { + return { success: false, errors: result.error.issues }; + } +} \ No newline at end of file