diff --git a/package.json b/package.json index e56bdaed..b65359d9 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.2", - "@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240613143006-244927bc441a.1", + "@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1", "@types/chrome": "^0.0.263", "@types/node": "^20.14.9", "@types/react": "^18.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 938ea82e..62578557 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,8 +130,8 @@ importers: specifier: ^1.8.2 version: 1.8.2 '@buf/meshtastic_protobufs.bufbuild_es': - specifier: 1.10.0-20240613143006-244927bc441a.1 - version: 1.10.0-20240613143006-244927bc441a.1(@bufbuild/protobuf@1.10.0) + specifier: 1.10.0-20240820152623-fac6975bbc78.1 + version: 1.10.0-20240820152623-fac6975bbc78.1(@bufbuild/protobuf@1.10.0) '@types/chrome': specifier: ^0.0.263 version: 0.0.263 @@ -354,8 +354,8 @@ packages: cpu: [x64] os: [win32] - '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240613143006-244927bc441a.1': - resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240613143006-244927bc441a.1.tgz} + '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240820152623-fac6975bbc78.1': + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240820152623-fac6975bbc78.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -3306,7 +3306,7 @@ snapshots: '@biomejs/cli-win32-x64@1.8.2': optional: true - '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240613143006-244927bc441a.1(@bufbuild/protobuf@1.10.0)': + '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240820152623-fac6975bbc78.1(@bufbuild/protobuf@1.10.0)': dependencies: '@bufbuild/protobuf': 1.10.0 diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 61c4c0e7..58bf241e 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -16,7 +16,7 @@ import { } from "react-hook-form"; interface DisabledBy { - fieldName: Path; + fieldName: Path | "always"; selector?: number; invert?: boolean; } @@ -66,7 +66,9 @@ export function DynamicForm({ if (!disabledBy) return false; return disabledBy.some((field) => { + if (field.fieldName === "always") return true; const value = getValues(field.fieldName); + if (value === "always") return true; if (typeof value === "boolean") return field.invert ? value : !value; if (typeof value === "number") return field.invert diff --git a/src/components/PageComponents/Config/Device.tsx b/src/components/PageComponents/Config/Device.tsx index c0f3bfc4..ce2b5ce1 100644 --- a/src/components/PageComponents/Config/Device.tsx +++ b/src/components/PageComponents/Config/Device.tsx @@ -36,19 +36,6 @@ export const Device = (): JSX.Element => { formatEnumName: true, }, }, - { - type: "toggle", - name: "serialEnabled", - label: "Serial Output Enabled", - description: "Enable the device's serial console", - }, - { - type: "toggle", - name: "debugLogEnabled", - label: "Enabled Debug Log", - description: - "Output debugging information to the device's serial port (auto disables when serial client is connected)", - }, { type: "number", name: "buttonGpio", @@ -86,12 +73,6 @@ export const Device = (): JSX.Element => { label: "Double Tap as Button Press", description: "Treat double tap as button press", }, - { - type: "toggle", - name: "isManaged", - label: "Managed", - description: "Is this device managed by a mesh administator", - }, { type: "toggle", name: "disableTripleClick", diff --git a/src/components/PageComponents/Config/Security.tsx b/src/components/PageComponents/Config/Security.tsx new file mode 100644 index 00000000..c9e1175c --- /dev/null +++ b/src/components/PageComponents/Config/Security.tsx @@ -0,0 +1,142 @@ +import { DynamicForm } from "@app/components/Form/DynamicForm.js"; +import type { SecurityValidation } from "@app/validation/config/security.js"; +import { useDevice } from "@core/stores/deviceStore.js"; +import { Protobuf } from "@meshtastic/js"; +import { fromByteArray, toByteArray } from "base64-js"; +import { Eye, EyeOff } from "lucide-react"; +import { useState } from "react"; + +export const Security = (): JSX.Element => { + const { config, nodes, hardware, setWorkingConfig } = useDevice(); + + const [privateKey, setPrivateKey] = useState( + fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), + ); + const [privateKeyVisible, setPrivateKeyVisible] = useState(false); + const [publicKey, setPublicKey] = useState( + fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), + ); + const [adminKey, setAdminKey] = useState( + fromByteArray(config.security?.adminKey ?? new Uint8Array(0)), + ); + const [adminKeyVisible, setAdminKeyVisible] = useState(false); + + const onSubmit = (data: SecurityValidation) => { + setWorkingConfig( + new Protobuf.Config.Config({ + payloadVariant: { + case: "security", + value: { + ...data, + adminKey: toByteArray(adminKey), + privateKey: toByteArray(privateKey), + publicKey: toByteArray(publicKey), + }, + }, + }), + ); + }; + return ( + + onSubmit={onSubmit} + defaultValues={{ + ...config.security, + adminKey: adminKey, + privateKey: privateKey, + publicKey: publicKey, + }} + fieldGroups={[ + { + label: "Security Settings", + description: "Settings for the Security configuration", + fields: [ + { + type: privateKeyVisible ? "text" : "password", + name: "privateKey", + label: "Private Key", + description: "Used to create a shared key with a remote device", + disabledBy: [ + { + fieldName: "adminChannelEnabled", + invert: true, + }, + ], + properties: { + action: { + icon: privateKeyVisible ? EyeOff : Eye, + onClick: () => setPrivateKeyVisible(!privateKeyVisible), + }, + }, + }, + { + type: "text", + name: "publicKey", + label: "Public Key", + description: + "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + disabledBy: [{ fieldName: "always" }], + }, + ], + }, + { + label: "Admin Settings", + description: "Settings for Admin ", + fields: [ + { + type: "toggle", + name: "adminChannelEnabled", + label: "Allow Legacy Admin", + description: + "Allow incoming device control over the insecure legacy admin channel", + }, + { + type: "toggle", + name: "isManaged", + label: "Managed", + description: + 'If true, device is considered to be "managed" by a mesh administrator via admin messages', + }, + { + type: adminKeyVisible ? "text" : "password", + name: "adminKey", + label: "Admin Key", + disabledBy: [{ fieldName: "adminChannelEnabled" }], + properties: { + action: { + icon: adminKeyVisible ? EyeOff : Eye, + onClick: () => setAdminKeyVisible(!adminKeyVisible), + }, + }, + description: + "The public key authorized to send admin messages to this node", + }, + ], + }, + { + label: "Logging Settings", + description: "Settings for Logging", + fields: [ + { + type: "toggle", + name: "bluetoothLoggingEnabled", + label: "Allow Bluetooth Logging", + description: "Enables device (serial style logs) over Bluetooth", + }, + { + type: "toggle", + name: "debugLogApiEnabled", + label: "Enable Debug Log API", + description: "Output live debug logging over serial", + }, + { + type: "toggle", + name: "serialEnabled", + label: "Serial Output Enabled", + description: "Serial Console over the Stream API", + }, + ], + }, + ]} + /> + ); +}; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 96d820bb..116b6647 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -85,7 +85,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
- {myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts + {myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index 29ac6ee0..a716a85b 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -191,6 +191,9 @@ export const useDeviceStore = create((set, get) => ({ device.config.bluetooth = config.payloadVariant.value; break; } + case "security": { + device.config.security = config.payloadVariant.value; + } } } }), diff --git a/src/pages/Config/DeviceConfig.tsx b/src/pages/Config/DeviceConfig.tsx index 66b8559d..45973e60 100644 --- a/src/pages/Config/DeviceConfig.tsx +++ b/src/pages/Config/DeviceConfig.tsx @@ -5,6 +5,7 @@ import { LoRa } from "@components/PageComponents/Config/LoRa.js"; import { Network } from "@components/PageComponents/Config/Network.js"; import { Position } from "@components/PageComponents/Config/Position.js"; import { Power } from "@components/PageComponents/Config/Power.js"; +import { Security } from "@components/PageComponents/Config/Security.js"; import { Tabs, TabsContent, @@ -47,6 +48,10 @@ export const DeviceConfig = (): JSX.Element => { label: "Bluetooth", element: Bluetooth, }, + { + label: "Security", + element: Security, + }, ]; return ( diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx index 8c86480e..53df0cd4 100644 --- a/src/pages/Map.tsx +++ b/src/pages/Map.tsx @@ -144,8 +144,8 @@ export const MapPage = (): JSX.Element => { {waypoints.map((wp) => (
@@ -163,8 +163,8 @@ export const MapPage = (): JSX.Element => { return ( { diff --git a/src/validation/config/bluetooth.ts b/src/validation/config/bluetooth.ts index 65a0d0be..efaca6e9 100644 --- a/src/validation/config/bluetooth.ts +++ b/src/validation/config/bluetooth.ts @@ -3,7 +3,11 @@ import { Protobuf } from "@meshtastic/js"; import { IsBoolean, IsEnum, IsInt } from "class-validator"; export class BluetoothValidation - implements Omit + implements + Omit< + Protobuf.Config.Config_BluetoothConfig, + keyof Message | "deviceLoggingEnabled" + > { @IsBoolean() enabled: boolean; diff --git a/src/validation/config/lora.ts b/src/validation/config/lora.ts index 291a42d0..f5ef49d6 100644 --- a/src/validation/config/lora.ts +++ b/src/validation/config/lora.ts @@ -3,7 +3,8 @@ import { Protobuf } from "@meshtastic/js"; import { IsArray, IsBoolean, IsEnum, IsInt, Max, Min } from "class-validator"; export class LoRaValidation - implements Omit + implements + Omit { @IsBoolean() usePreset: boolean; diff --git a/src/validation/config/power.ts b/src/validation/config/power.ts index 3f3d097a..67bd1cd0 100644 --- a/src/validation/config/power.ts +++ b/src/validation/config/power.ts @@ -3,7 +3,8 @@ import type { Protobuf } from "@meshtastic/js"; import { IsBoolean, IsInt, IsNumber, Max, Min } from "class-validator"; export class PowerValidation - implements Omit + implements + Omit { @IsBoolean() isPowerSaving: boolean; diff --git a/src/validation/config/security.ts b/src/validation/config/security.ts new file mode 100644 index 00000000..d1a35808 --- /dev/null +++ b/src/validation/config/security.ts @@ -0,0 +1,35 @@ +import type { Message } from "@bufbuild/protobuf"; +import type { Protobuf } from "@meshtastic/js"; +import { IsBoolean, IsString } from "class-validator"; + +export class SecurityValidation + implements + Omit< + Protobuf.Config.Config_SecurityConfig, + keyof Message | "adminKey" | "privateKey" | "publicKey" + > +{ + @IsBoolean() + adminChannelEnabled: boolean; + + @IsString() + adminKey: string; + + @IsBoolean() + bluetoothLoggingEnabled: boolean; + + @IsBoolean() + debugLogApiEnabled: boolean; + + @IsBoolean() + isManaged: boolean; + + @IsString() + privateKey: string; + + @IsString() + publicKey: string; + + @IsBoolean() + serialEnabled: boolean; +} diff --git a/src/validation/moduleConfig/storeForward.ts b/src/validation/moduleConfig/storeForward.ts index 82d9f4c2..773a2539 100644 --- a/src/validation/moduleConfig/storeForward.ts +++ b/src/validation/moduleConfig/storeForward.ts @@ -4,7 +4,10 @@ import { IsBoolean, IsInt } from "class-validator"; export class StoreForwardValidation implements - Omit + Omit< + Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig, + keyof Message | "isServer" + > { @IsBoolean() enabled: boolean;