Zod config validation (#635)
* Zod WIP * Zod form validation * DynamicForm testing * Fix linting * Delete rasterSource.ts --------- Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
co-authored by
philon-
parent
df036d3904
commit
1cbd98ec53
@@ -1,63 +1,18 @@
|
||||
import { useAppStore } from "../../../core/stores/appStore.ts";
|
||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.ts";
|
||||
import {
|
||||
type BluetoothValidation,
|
||||
BluetoothValidationSchema,
|
||||
} from "@app/validation/config/bluetooth.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const Bluetooth = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
const {
|
||||
hasErrors,
|
||||
getErrorMessage,
|
||||
hasFieldError,
|
||||
addError,
|
||||
removeError,
|
||||
clearErrors,
|
||||
} = useAppStore();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
const [bluetoothPin, setBluetoothPin] = useState(
|
||||
config?.bluetooth?.fixedPin.toString() ?? "",
|
||||
);
|
||||
|
||||
const validateBluetoothPin = (pin: string) => {
|
||||
// if empty show error they need a pin set
|
||||
if (pin === "") {
|
||||
return addError("fixedPin", t("bluetooth.validation.pinRequired"));
|
||||
}
|
||||
|
||||
// clear any existing errors
|
||||
clearErrors();
|
||||
|
||||
// if it starts with 0 show error
|
||||
if (pin[0] === "0") {
|
||||
return addError(
|
||||
"fixedPin",
|
||||
t("bluetooth.validation.pinCannotStartWithZero"),
|
||||
);
|
||||
}
|
||||
// if it's not 6 digits show error
|
||||
if (pin.length < 6) {
|
||||
return addError("fixedPin", t("bluetooth.validation.pinMustBeSixDigits"));
|
||||
}
|
||||
|
||||
removeError("fixedPin");
|
||||
};
|
||||
|
||||
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const numericValue = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||||
setBluetoothPin(numericValue);
|
||||
validateBluetoothPin(numericValue);
|
||||
};
|
||||
|
||||
const onSubmit = (data: BluetoothValidation) => {
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkingConfig(
|
||||
create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
@@ -71,6 +26,8 @@ export const Bluetooth = () => {
|
||||
return (
|
||||
<DynamicForm<BluetoothValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={BluetoothValidationSchema}
|
||||
formId="Config_BluetoothConfig"
|
||||
defaultValues={config.bluetooth}
|
||||
fieldGroups={[
|
||||
{
|
||||
@@ -89,12 +46,6 @@ export const Bluetooth = () => {
|
||||
name: "mode",
|
||||
label: t("bluetooth.pairingMode.label"),
|
||||
description: t("bluetooth.pairingMode.description"),
|
||||
selectChange: (e) => {
|
||||
if (e !== "1") {
|
||||
setBluetoothPin("");
|
||||
removeError("fixedPin");
|
||||
}
|
||||
},
|
||||
disabledBy: [
|
||||
{
|
||||
fieldName: "enabled",
|
||||
@@ -110,24 +61,6 @@ export const Bluetooth = () => {
|
||||
name: "fixedPin",
|
||||
label: t("bluetooth.pin.label"),
|
||||
description: t("bluetooth.pin.description"),
|
||||
validationText: hasFieldError("fixedPin")
|
||||
? getErrorMessage("fixedPin")
|
||||
: "",
|
||||
inputChange: bluetoothPinChangeEvent,
|
||||
disabledBy: [
|
||||
{
|
||||
fieldName: "mode",
|
||||
selector: Protobuf.Config.Config_BluetoothConfig_PairingMode
|
||||
.FIXED_PIN,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
fieldName: "enabled",
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
value: bluetoothPin,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { DeviceValidation } from "@app/validation/config/device.ts";
|
||||
import {
|
||||
type DeviceValidation,
|
||||
DeviceValidationSchema,
|
||||
} from "@app/validation/config/device.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -24,6 +27,8 @@ export const Device = () => {
|
||||
return (
|
||||
<DynamicForm<DeviceValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={DeviceValidationSchema}
|
||||
formId="Config_DeviceConfig"
|
||||
defaultValues={config.device}
|
||||
fieldGroups={[
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { DisplayValidation } from "@app/validation/config/display.ts";
|
||||
import {
|
||||
type DisplayValidation,
|
||||
DisplayValidationSchema,
|
||||
} from "@app/validation/config/display.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -23,6 +26,8 @@ export const Display = () => {
|
||||
return (
|
||||
<DynamicForm<DisplayValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={DisplayValidationSchema}
|
||||
formId="Config_DisplayConfig"
|
||||
defaultValues={config.display}
|
||||
fieldGroups={[
|
||||
{
|
||||
@@ -91,7 +96,7 @@ export const Display = () => {
|
||||
label: t("display.oledType.label"),
|
||||
description: t("display.oledType.description"),
|
||||
properties: {
|
||||
enumValue: Protobuf.Config.Config_Displayjonfig_OledType,
|
||||
enumValue: Protobuf.Config.Config_DisplayConfig_OledType,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { LoRaValidation } from "@app/validation/config/lora.ts";
|
||||
import {
|
||||
type LoRaValidation,
|
||||
LoRaValidationSchema,
|
||||
} from "@app/validation/config/lora.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -23,6 +26,8 @@ export const LoRa = () => {
|
||||
return (
|
||||
<DynamicForm<LoRaValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={LoRaValidationSchema}
|
||||
formId="Config_LoRaConfig"
|
||||
defaultValues={config.lora}
|
||||
fieldGroups={[
|
||||
{
|
||||
|
||||
@@ -10,20 +10,12 @@ import {
|
||||
convertIpAddressToInt,
|
||||
} from "@core/utils/ip.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { validateSchema } from "@app/validation/validate.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const Network = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
const onSubmit = (data: NetworkValidation) => {
|
||||
const result = validateSchema(NetworkValidationSchema, data);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Validation errors:", result.errors);
|
||||
}
|
||||
|
||||
setWorkingConfig(
|
||||
create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
@@ -48,6 +40,8 @@ export const Network = () => {
|
||||
return (
|
||||
<DynamicForm<NetworkValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={NetworkValidationSchema}
|
||||
formId="Config_NetworkConfig"
|
||||
defaultValues={{
|
||||
...config.network,
|
||||
ipv4Config: {
|
||||
|
||||
@@ -2,7 +2,10 @@ import {
|
||||
type FlagName,
|
||||
usePositionFlags,
|
||||
} from "@core/hooks/usePositionFlags.ts";
|
||||
import type { PositionValidation } from "@app/validation/config/position.ts";
|
||||
import {
|
||||
type PositionValidation,
|
||||
PositionValidationSchema,
|
||||
} from "@app/validation/config/position.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -41,6 +44,8 @@ export const Position = () => {
|
||||
data.positionFlags = flagsValue;
|
||||
return onSubmit(data);
|
||||
}}
|
||||
validationSchema={PositionValidationSchema}
|
||||
formId="Config_PositionConfig"
|
||||
defaultValues={config.position}
|
||||
fieldGroups={[
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { PowerValidation } from "@app/validation/config/power.ts";
|
||||
import {
|
||||
type PowerValidation,
|
||||
PowerValidationSchema,
|
||||
} from "@app/validation/config/power.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
@@ -23,6 +26,8 @@ export const Power = () => {
|
||||
return (
|
||||
<DynamicForm<PowerValidation>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={PowerValidationSchema}
|
||||
formId="Config_PowerConfig"
|
||||
defaultValues={config.power}
|
||||
fieldGroups={[
|
||||
{
|
||||
|
||||
@@ -2,116 +2,46 @@ import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||
import type { SecurityValidation } from "@app/validation/config/security.ts";
|
||||
import {
|
||||
type ParsedSecurity,
|
||||
type RawSecurity,
|
||||
RawSecuritySchema,
|
||||
} from "@app/validation/config/security.ts";
|
||||
import { useState } from "react";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import { useReducer } from "react";
|
||||
import { securityReducer } from "@components/PageComponents/Config/Security/securityReducer.tsx";
|
||||
import type { SecurityConfigInit } from "./types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type KeyState = {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
privateKeyDialogOpen: boolean;
|
||||
};
|
||||
|
||||
export const Security = () => {
|
||||
const { config, setWorkingConfig, setDialogOpen } = useDevice();
|
||||
const {
|
||||
hasErrors,
|
||||
getErrorMessage,
|
||||
hasFieldError,
|
||||
addError,
|
||||
removeError,
|
||||
clearErrors,
|
||||
} = useAppStore();
|
||||
const { removeError } = useAppStore();
|
||||
const { t } = useTranslation("deviceConfig");
|
||||
|
||||
const [state, dispatch] = useReducer(securityReducer, {
|
||||
privateKey: fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
|
||||
privateKeyVisible: false,
|
||||
adminKeyVisible: [false, false, false],
|
||||
privateKeyBitCount: config.security?.privateKey?.length ?? 32,
|
||||
publicKey: fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
||||
adminKey: [
|
||||
fromByteArray(config.security?.adminKey?.at(0) ?? new Uint8Array(0)),
|
||||
fromByteArray(config.security?.adminKey?.at(1) ?? new Uint8Array(0)),
|
||||
fromByteArray(config.security?.adminKey?.at(2) ?? new Uint8Array(0)),
|
||||
],
|
||||
const [keyState, setKeyState] = useState<KeyState>(() => ({
|
||||
publicKey: fromByteArray(config?.security?.publicKey ?? new Uint8Array(0)),
|
||||
privateKey: fromByteArray(
|
||||
config?.security?.privateKey ?? new Uint8Array(0),
|
||||
),
|
||||
privateKeyDialogOpen: false,
|
||||
isManaged: config.security?.isManaged ?? false,
|
||||
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||
serialEnabled: config.security?.serialEnabled ?? false,
|
||||
});
|
||||
}));
|
||||
|
||||
const validateKey = (
|
||||
input: string,
|
||||
count: number,
|
||||
fieldName: "privateKey" | "adminKey",
|
||||
fieldIndex?: number,
|
||||
) => {
|
||||
const fieldNameKey = fieldName + (fieldIndex ?? "");
|
||||
try {
|
||||
removeError(fieldNameKey);
|
||||
if (fieldName === "privateKey" && input === "") {
|
||||
addError(fieldNameKey, t("security.validation.privateKeyRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldName === "adminKey" && input === "") {
|
||||
if (
|
||||
state.isManaged &&
|
||||
state.adminKey
|
||||
.map((v, i) => (i === fieldIndex ? input : v))
|
||||
.every((s) => s === "")
|
||||
) {
|
||||
addError("adminKey0", t("security."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.length % 4 !== 0) {
|
||||
addError(
|
||||
fieldNameKey,
|
||||
fieldName === "privateKey"
|
||||
? t("security.validation.privateKeyMustBe256BitPsk")
|
||||
: t("security.validation.adminKeyMustBe256BitPsk"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = toByteArray(input);
|
||||
if (decoded.length !== count) {
|
||||
addError(
|
||||
fieldNameKey,
|
||||
t("security.validation.enterValid256BitPsk", {
|
||||
bits: count * 8,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
addError(
|
||||
fieldNameKey,
|
||||
fieldName === "privateKey"
|
||||
? t("security.validation.invalidPrivateKeyFormat")
|
||||
: t("security.validation.invalidAdminKeyFormat"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function setSecurityPayload(overrides: SecurityConfigInit) {
|
||||
const base: SecurityConfigInit = {
|
||||
isManaged: state.isManaged,
|
||||
adminChannelEnabled: state.adminChannelEnabled,
|
||||
debugLogApiEnabled: state.debugLogApiEnabled,
|
||||
serialEnabled: state.serialEnabled,
|
||||
privateKey: overrides?.privateKey ?? toByteArray(state.privateKey),
|
||||
publicKey: overrides?.publicKey ?? toByteArray(state.publicKey),
|
||||
const onSubmit = (data: RawSecurity) => {
|
||||
const payload: ParsedSecurity = {
|
||||
...data,
|
||||
privateKey: toByteArray(keyState.privateKey),
|
||||
publicKey: toByteArray(keyState.publicKey),
|
||||
adminKey: [
|
||||
overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]),
|
||||
overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]),
|
||||
overrides?.adminKey?.[0] ?? toByteArray(state.adminKey[0]),
|
||||
toByteArray(data.adminKey.at(0) ?? ""),
|
||||
toByteArray(data.adminKey.at(1) ?? ""),
|
||||
toByteArray(data.adminKey.at(2) ?? ""),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -119,137 +49,79 @@ export const Security = () => {
|
||||
create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: "security",
|
||||
value: { ...base, ...overrides },
|
||||
value: payload,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
const pkiRegenerate = () => {
|
||||
clearErrors();
|
||||
const privateKey = getX25519PrivateKey();
|
||||
const publicKey = getX25519PublicKey(privateKey);
|
||||
|
||||
dispatch({
|
||||
type: "REGENERATE_PRIV_PUB_KEY",
|
||||
payload: {
|
||||
privateKey: fromByteArray(privateKey),
|
||||
publicKey: fromByteArray(publicKey),
|
||||
},
|
||||
});
|
||||
updatePublicKey(fromByteArray(privateKey));
|
||||
|
||||
validateKey(
|
||||
fromByteArray(privateKey),
|
||||
state.privateKeyBitCount,
|
||||
"privateKey",
|
||||
);
|
||||
setKeyState((prev) => ({
|
||||
...prev,
|
||||
privateKey: fromByteArray(privateKey),
|
||||
privateKeyDialogOpen: false,
|
||||
}));
|
||||
|
||||
if (!hasErrors()) {
|
||||
setSecurityPayload({
|
||||
removeError("privateKey");
|
||||
};
|
||||
|
||||
const updatePublicKey = (privateKey: string) => {
|
||||
try {
|
||||
const publicKey = fromByteArray(
|
||||
getX25519PublicKey(toByteArray(privateKey)),
|
||||
);
|
||||
setKeyState((prev) => ({
|
||||
...prev,
|
||||
privateKey: privateKey,
|
||||
publicKey: publicKey,
|
||||
});
|
||||
}));
|
||||
|
||||
removeError("publicKey");
|
||||
} catch (_e) {
|
||||
setKeyState((prev) => ({
|
||||
...prev,
|
||||
privateKey: privateKey,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const privateKeyInputChangeEvent = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const privateKeyB64String = e.target.value;
|
||||
dispatch({ type: "SET_PRIVATE_KEY", payload: privateKeyB64String });
|
||||
validateKey(privateKeyB64String, state.privateKeyBitCount, "privateKey");
|
||||
|
||||
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
|
||||
dispatch({ type: "SET_PUBLIC_KEY", payload: fromByteArray(publicKey) });
|
||||
|
||||
if (!hasErrors()) {
|
||||
setSecurityPayload({
|
||||
privateKey: toByteArray(privateKeyB64String),
|
||||
publicKey: publicKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const adminKeyInputChangeEvent = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
fieldIndex?: number,
|
||||
) => {
|
||||
if (fieldIndex === undefined) return;
|
||||
const psk = e.target.value;
|
||||
|
||||
const payload = [
|
||||
fieldIndex === 0 ? psk : state.adminKey[0],
|
||||
fieldIndex === 1 ? psk : state.adminKey[1],
|
||||
fieldIndex === 2 ? psk : state.adminKey[2],
|
||||
] satisfies [string, string, string];
|
||||
|
||||
dispatch({ type: "SET_ADMIN_KEY", payload: payload });
|
||||
validateKey(psk, state.privateKeyBitCount, "adminKey", fieldIndex);
|
||||
|
||||
if (!hasErrors()) {
|
||||
setSecurityPayload({
|
||||
adminKey: payload.map(toByteArray) as [
|
||||
Uint8Array,
|
||||
Uint8Array,
|
||||
Uint8Array,
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleChange = (
|
||||
field:
|
||||
| "isManaged"
|
||||
| "adminChannelEnabled"
|
||||
| "debugLogApiEnabled"
|
||||
| "serialEnabled",
|
||||
next: boolean,
|
||||
) => {
|
||||
dispatch({ type: "SET_TOGGLE", field, payload: next });
|
||||
|
||||
if (field === "isManaged" && state.adminKey.every((s) => s === "")) {
|
||||
if (next) {
|
||||
// If enabling 'managed' and no admin keys are set
|
||||
addError(
|
||||
"adminKey0",
|
||||
t("security.validation.adminKeyRequiredWhenManaged"),
|
||||
);
|
||||
} else {
|
||||
removeError("adminKey0");
|
||||
removeError("adminKey1");
|
||||
removeError("adminKey2");
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors()) {
|
||||
setSecurityPayload({
|
||||
isManaged: field === "isManaged" ? next : state.isManaged,
|
||||
adminChannelEnabled: field === "adminChannelEnabled"
|
||||
? next
|
||||
: state.adminChannelEnabled,
|
||||
debugLogApiEnabled: field === "debugLogApiEnabled"
|
||||
? next
|
||||
: state.debugLogApiEnabled,
|
||||
serialEnabled: field === "serialEnabled" ? next : state.serialEnabled,
|
||||
});
|
||||
}
|
||||
};
|
||||
const bits = [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DynamicForm<SecurityValidation>
|
||||
onSubmit={() => {}}
|
||||
submitType="onSubmit"
|
||||
<DynamicForm<RawSecurity>
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={RawSecuritySchema}
|
||||
formId="Config_SecurityConfig"
|
||||
defaultValues={{
|
||||
...config.security,
|
||||
...{
|
||||
adminKey: state.adminKey,
|
||||
privateKey: state.privateKey,
|
||||
publicKey: state.publicKey,
|
||||
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||
isManaged: config.security?.isManaged ?? false,
|
||||
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||
serialEnabled: config.security?.serialEnabled ?? false,
|
||||
privateKey: fromByteArray(
|
||||
config?.security?.privateKey ?? new Uint8Array(0),
|
||||
),
|
||||
publicKey: fromByteArray(
|
||||
config?.security?.publicKey ?? new Uint8Array(0),
|
||||
),
|
||||
adminKey: [
|
||||
fromByteArray(
|
||||
config?.security?.adminKey.at(0) ?? new Uint8Array(0),
|
||||
),
|
||||
fromByteArray(
|
||||
config?.security?.adminKey.at(1) ?? new Uint8Array(0),
|
||||
),
|
||||
fromByteArray(
|
||||
config?.security?.adminKey.at(2) ?? new Uint8Array(0),
|
||||
),
|
||||
],
|
||||
},
|
||||
}}
|
||||
fieldGroups={[
|
||||
@@ -263,28 +135,20 @@ export const Security = () => {
|
||||
name: "privateKey",
|
||||
label: t("security.privateKey.label"),
|
||||
description: t("security.privateKey.description"),
|
||||
bits: [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
],
|
||||
validationText: hasFieldError("privateKey")
|
||||
? getErrorMessage("privateKey")
|
||||
: "",
|
||||
devicePSKBitCount: state.privateKeyBitCount,
|
||||
inputChange: privateKeyInputChangeEvent,
|
||||
selectChange: () => {},
|
||||
hide: !state.privateKeyVisible,
|
||||
bits,
|
||||
devicePSKBitCount: 32,
|
||||
hide: true,
|
||||
inputChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updatePublicKey(e.target.value);
|
||||
},
|
||||
actionButtons: [
|
||||
{
|
||||
text: t("button.generate"),
|
||||
onClick: () =>
|
||||
dispatch({
|
||||
type: "SHOW_PRIVATE_KEY_DIALOG",
|
||||
payload: true,
|
||||
}),
|
||||
setKeyState((prev) => ({
|
||||
...prev,
|
||||
privateKeyDialogOpen: true,
|
||||
})),
|
||||
variant: "success",
|
||||
},
|
||||
{
|
||||
@@ -294,9 +158,10 @@ export const Security = () => {
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
value: state.privateKey,
|
||||
showCopyButton: true,
|
||||
showPasswordToggle: true,
|
||||
|
||||
value: keyState.privateKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,8 +171,8 @@ export const Security = () => {
|
||||
disabled: true,
|
||||
description: t("security.publicKey.description"),
|
||||
properties: {
|
||||
value: state.publicKey,
|
||||
showCopyButton: true,
|
||||
value: keyState.publicKey,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -322,26 +187,14 @@ export const Security = () => {
|
||||
id: "adminKey0Input",
|
||||
label: t("security.primaryAdminKey.label"),
|
||||
description: t("security.primaryAdminKey.description"),
|
||||
validationText: hasFieldError("adminKey0")
|
||||
? getErrorMessage("adminKey0")
|
||||
: "",
|
||||
inputChange: (e) => adminKeyInputChangeEvent(e, 0),
|
||||
selectChange: () => {},
|
||||
bits: [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
],
|
||||
devicePSKBitCount: state.privateKeyBitCount,
|
||||
hide: !state.adminKeyVisible[0],
|
||||
bits,
|
||||
devicePSKBitCount: 32,
|
||||
hide: true,
|
||||
actionButtons: [],
|
||||
disabledBy: [
|
||||
{ fieldName: "adminChannelEnabled", invert: true },
|
||||
],
|
||||
properties: {
|
||||
value: state.adminKey[0],
|
||||
showCopyButton: true,
|
||||
showPasswordToggle: true,
|
||||
},
|
||||
@@ -352,26 +205,14 @@ export const Security = () => {
|
||||
id: "adminKey1Input",
|
||||
label: t("security.secondaryAdminKey.label"),
|
||||
description: t("security.secondaryAdminKey.description"),
|
||||
validationText: hasFieldError("adminKey1")
|
||||
? getErrorMessage("adminKey1")
|
||||
: "",
|
||||
inputChange: (e) => adminKeyInputChangeEvent(e, 1),
|
||||
selectChange: () => {},
|
||||
bits: [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
],
|
||||
devicePSKBitCount: state.privateKeyBitCount,
|
||||
hide: !state.adminKeyVisible[1],
|
||||
bits,
|
||||
devicePSKBitCount: 32,
|
||||
hide: true,
|
||||
actionButtons: [],
|
||||
disabledBy: [
|
||||
{ fieldName: "adminChannelEnabled", invert: true },
|
||||
],
|
||||
properties: {
|
||||
value: state.adminKey[1],
|
||||
showCopyButton: true,
|
||||
showPasswordToggle: true,
|
||||
},
|
||||
@@ -382,26 +223,14 @@ export const Security = () => {
|
||||
id: "adminKey2Input",
|
||||
label: t("security.tertiaryAdminKey.label"),
|
||||
description: t("security.tertiaryAdminKey.description"),
|
||||
validationText: hasFieldError("adminKey2")
|
||||
? getErrorMessage("adminKey2")
|
||||
: "",
|
||||
inputChange: (e) => adminKeyInputChangeEvent(e, 2),
|
||||
selectChange: () => {},
|
||||
bits: [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
],
|
||||
devicePSKBitCount: state.privateKeyBitCount,
|
||||
hide: !state.adminKeyVisible[2],
|
||||
bits,
|
||||
devicePSKBitCount: 32,
|
||||
hide: true,
|
||||
actionButtons: [],
|
||||
disabledBy: [
|
||||
{ fieldName: "adminChannelEnabled", invert: true },
|
||||
],
|
||||
properties: {
|
||||
value: state.adminKey[2],
|
||||
showCopyButton: true,
|
||||
showPasswordToggle: true,
|
||||
},
|
||||
@@ -411,25 +240,12 @@ export const Security = () => {
|
||||
name: "isManaged",
|
||||
label: t("security.managed.label"),
|
||||
description: t("security.managed.description"),
|
||||
inputChange: (e: boolean) => onToggleChange("isManaged", e),
|
||||
properties: {
|
||||
checked: state.isManaged,
|
||||
},
|
||||
disabled: (hasFieldError("adminKey0") ||
|
||||
hasFieldError("adminKey1") ||
|
||||
hasFieldError("adminKey2")) &&
|
||||
!state.adminKey.every((s) => s === ""),
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "adminChannelEnabled",
|
||||
label: t("security.adminChannelEnabled.label"),
|
||||
description: t("security.adminChannelEnabled.description"),
|
||||
inputChange: (e: boolean) =>
|
||||
onToggleChange("adminChannelEnabled", e),
|
||||
properties: {
|
||||
checked: state.adminChannelEnabled,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -442,21 +258,12 @@ export const Security = () => {
|
||||
name: "debugLogApiEnabled",
|
||||
label: t("security.enableDebugLogApi.label"),
|
||||
description: t("security.enableDebugLogApi.description"),
|
||||
inputChange: (e: boolean) =>
|
||||
onToggleChange("debugLogApiEnabled", e),
|
||||
properties: {
|
||||
checked: state.debugLogApiEnabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "serialEnabled",
|
||||
label: t("security.serialOutputEnabled.label"),
|
||||
description: t("security.serialOutputEnabled.description"),
|
||||
inputChange: (e: boolean) => onToggleChange("serialEnabled", e),
|
||||
properties: {
|
||||
checked: state.serialEnabled,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -468,9 +275,12 @@ export const Security = () => {
|
||||
title: t("pkiRegenerate.title"),
|
||||
description: t("pkiRegenerate.description"),
|
||||
}}
|
||||
open={state.privateKeyDialogOpen}
|
||||
open={keyState.privateKeyDialogOpen}
|
||||
onOpenChange={() =>
|
||||
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })}
|
||||
setKeyState((prev) => ({
|
||||
...prev,
|
||||
privateKeyDialogOpen: false,
|
||||
}))}
|
||||
onSubmit={pkiRegenerate}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { SecurityAction, SecurityState } from "./types.ts";
|
||||
|
||||
export function securityReducer(
|
||||
state: SecurityState,
|
||||
action: SecurityAction,
|
||||
): SecurityState {
|
||||
switch (action.type) {
|
||||
case "SET_PRIVATE_KEY":
|
||||
return { ...state, privateKey: action.payload };
|
||||
case "SET_PRIVATE_KEY_BIT_COUNT":
|
||||
return { ...state, privateKeyBitCount: action.payload };
|
||||
case "SET_PUBLIC_KEY":
|
||||
return { ...state, publicKey: action.payload };
|
||||
case "SET_ADMIN_KEY":
|
||||
return { ...state, adminKey: action.payload };
|
||||
case "SHOW_PRIVATE_KEY_DIALOG":
|
||||
return { ...state, privateKeyDialogOpen: action.payload };
|
||||
case "REGENERATE_PRIV_PUB_KEY":
|
||||
return {
|
||||
...state,
|
||||
privateKey: action.payload.privateKey,
|
||||
publicKey: action.payload.publicKey,
|
||||
privateKeyDialogOpen: false,
|
||||
};
|
||||
case "SET_TOGGLE":
|
||||
return { ...state, [action.field]: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { type MessageInitShape } from "@bufbuild/protobuf";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
export interface SecurityState {
|
||||
privateKey: string;
|
||||
privateKeyVisible: boolean;
|
||||
adminKeyVisible: [boolean, boolean, boolean];
|
||||
privateKeyBitCount: number;
|
||||
publicKey: string;
|
||||
adminKey: [string, string, string];
|
||||
privateKeyDialogOpen: boolean;
|
||||
isManaged: boolean;
|
||||
adminChannelEnabled: boolean;
|
||||
debugLogApiEnabled: boolean;
|
||||
serialEnabled: boolean;
|
||||
}
|
||||
|
||||
export type SecurityAction =
|
||||
| { type: "SET_PRIVATE_KEY"; payload: string }
|
||||
| { type: "SET_PRIVATE_KEY_BIT_COUNT"; payload: number }
|
||||
| { type: "SET_PUBLIC_KEY"; payload: string }
|
||||
| { type: "SET_ADMIN_KEY"; payload: [string, string, string] }
|
||||
| { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean }
|
||||
| { type: "SET_TOGGLE"; payload: boolean; field: string }
|
||||
| {
|
||||
type: "REGENERATE_PRIV_PUB_KEY";
|
||||
payload: { privateKey: string; publicKey: string };
|
||||
};
|
||||
|
||||
export type SecurityConfigInit = Extract<
|
||||
MessageInitShape<
|
||||
typeof Protobuf.Config.ConfigSchema
|
||||
>["payloadVariant"],
|
||||
{ case: "security" }
|
||||
>["value"];
|
||||
Reference in New Issue
Block a user