committed by
GitHub
85 changed files with 6945 additions and 3653 deletions
@ -0,0 +1,94 @@ |
|||
name: Bug Report |
|||
description: File a bug report |
|||
title: "[Bug]: " |
|||
labels: ["bug"] |
|||
body: |
|||
- type: markdown |
|||
attributes: |
|||
value: | |
|||
Thanks for taking the time to fill out this bug report! |
|||
|
|||
- type: dropdown |
|||
id: hardware |
|||
attributes: |
|||
label: Hardware |
|||
description: What hardware are you encountering this issue on? |
|||
multiple: true |
|||
options: |
|||
- Not Applicable |
|||
- T-Beam |
|||
- T-Beam S3 |
|||
- T-Beam 0.7 |
|||
- T-Lora v1 |
|||
- T-Lora v1.3 |
|||
- T-Lora v2 1.6 |
|||
- T-Deck |
|||
- T-Echo |
|||
- T-Watch |
|||
- Rak4631 |
|||
- Rak11200 |
|||
- Rak11310 |
|||
- Heltec v1 |
|||
- Heltec v2 |
|||
- Heltec v2.1 |
|||
- Heltec V3 |
|||
- Heltec Wireless Paper |
|||
- Heltec Wireless Tracker |
|||
- Raspberry Pi Pico (W) |
|||
- Relay v1 |
|||
- Relay v2 |
|||
- DIY |
|||
- Other |
|||
validations: |
|||
required: true |
|||
|
|||
- type: dropdown |
|||
id: category |
|||
attributes: |
|||
label: Connection Type |
|||
description: How are you connecting to your device? |
|||
multiple: true |
|||
options: |
|||
- HTTP |
|||
- Bluetooth |
|||
- Serial |
|||
validations: |
|||
required: true |
|||
|
|||
- type: dropdown |
|||
id: local |
|||
attributes: |
|||
label: Local or Hosted |
|||
description: Are you using `meshtastic.local` or `client.meshtastic.org`? |
|||
multiple: true |
|||
options: |
|||
- http://meshtastic.local |
|||
- https://client.meshtastic.org |
|||
validations: |
|||
required: true |
|||
|
|||
- type: input |
|||
id: version |
|||
attributes: |
|||
label: Firmware Version |
|||
description: This can be found on the device's screen or via one of the apps. |
|||
placeholder: x.x.x.yyyyyyy |
|||
validations: |
|||
required: true |
|||
|
|||
- type: textarea |
|||
id: body |
|||
attributes: |
|||
label: Description |
|||
description: Please provide details on what steps you performed for this to happen. |
|||
validations: |
|||
required: true |
|||
|
|||
- type: textarea |
|||
id: logs |
|||
attributes: |
|||
label: Relevant console output |
|||
description: If you have any log output to help in diagnosing your bug, please provide it here. |
|||
render: Shell |
|||
validations: |
|||
required: false |
|||
@ -0,0 +1,17 @@ |
|||
name: Feature Request |
|||
description: Request a new feature |
|||
title: "[Feature Request]: " |
|||
labels: ["enhancement"] |
|||
body: |
|||
- type: markdown |
|||
attributes: |
|||
value: | |
|||
Thanks for your request this will not gurantee that we will implement it, but it will be reviewed. |
|||
|
|||
- type: textarea |
|||
id: body |
|||
attributes: |
|||
label: Description |
|||
description: Please provide details about your enhancement. |
|||
validations: |
|||
required: true |
|||
@ -0,0 +1,61 @@ |
|||
name: 'Release' |
|||
|
|||
on: |
|||
release: |
|||
types: [released] |
|||
|
|||
permissions: |
|||
contents: write |
|||
packages: write |
|||
|
|||
jobs: |
|||
build-and-package: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v4 |
|||
- uses: pnpm/action-setup@v4 |
|||
with: |
|||
version: latest |
|||
|
|||
- name: Install Dependencies |
|||
run: pnpm install |
|||
|
|||
- name: Build Package |
|||
run: pnpm build |
|||
|
|||
- name: Package Output |
|||
run: pnpm package |
|||
|
|||
- name: Archive compressed build |
|||
uses: actions/upload-artifact@v4 |
|||
with: |
|||
name: build |
|||
path: dist/build.tar |
|||
|
|||
- name: Set up QEMU |
|||
uses: docker/setup-qemu-action@v3 |
|||
|
|||
- name: Buildah Build |
|||
id: build-container |
|||
uses: redhat-actions/buildah-build@v2 |
|||
with: |
|||
containerfiles: | |
|||
./Containerfile |
|||
image: ${{github.event.repository.full_name}} |
|||
tags: latest ${{ github.sha }} |
|||
oci: true |
|||
platforms: linux/amd64, linux/arm64 |
|||
|
|||
- name: Push To Registry |
|||
id: push-to-registry |
|||
uses: redhat-actions/push-to-registry@v2 |
|||
with: |
|||
image: ${{ steps.build-container.outputs.image }} |
|||
tags: ${{ steps.build-container.outputs.tags }} |
|||
registry: ghcr.io |
|||
username: ${{ github.actor }} |
|||
password: ${{ secrets.GITHUB_TOKEN }} |
|||
|
|||
- name: Print image url |
|||
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}" |
|||
@ -1,4 +1,7 @@ |
|||
{ |
|||
"editor.defaultFormatter": "biomejs.biome", |
|||
"editor.codeActionsOnSave": { |
|||
"quickfix.biome": "explicit" |
|||
}, |
|||
"editor.formatOnSave": true |
|||
} |
|||
File diff suppressed because it is too large
@ -1,6 +1,6 @@ |
|||
module.exports = { |
|||
plugins: { |
|||
tailwindcss: {}, |
|||
autoprefixer: {} |
|||
} |
|||
autoprefixer: {}, |
|||
}, |
|||
}; |
|||
|
|||
@ -0,0 +1,39 @@ |
|||
import { Button } from "@components/UI/Button.js"; |
|||
import { |
|||
Dialog, |
|||
DialogContent, |
|||
DialogDescription, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.js"; |
|||
|
|||
export interface PkiRegenerateDialogProps { |
|||
open: boolean; |
|||
onOpenChange: () => void; |
|||
onSubmit: () => void; |
|||
} |
|||
|
|||
export const PkiRegenerateDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
onSubmit, |
|||
}: PkiRegenerateDialogProps): JSX.Element => { |
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent> |
|||
<DialogHeader> |
|||
<DialogTitle>Regenerate Key pair?</DialogTitle> |
|||
<DialogDescription> |
|||
Are you sure you want to regenerate key pair? |
|||
</DialogDescription> |
|||
</DialogHeader> |
|||
<DialogFooter> |
|||
<Button variant="destructive" onClick={() => onSubmit()}> |
|||
Regenerate |
|||
</Button> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,54 @@ |
|||
import { useAppStore } from "@app/core/stores/appStore"; |
|||
import { useDevice } from "@app/core/stores/deviceStore.js"; |
|||
import { Button } from "@components/UI/Button.js"; |
|||
import { |
|||
Dialog, |
|||
DialogContent, |
|||
DialogDescription, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.js"; |
|||
import { Label } from "@components/UI/Label.js"; |
|||
|
|||
export interface RemoveNodeDialogProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
export const RemoveNodeDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
}: RemoveNodeDialogProps): JSX.Element => { |
|||
const { connection, nodes, removeNode } = useDevice(); |
|||
const { nodeNumToBeRemoved } = useAppStore(); |
|||
|
|||
const onSubmit = () => { |
|||
connection?.removeNodeByNum(nodeNumToBeRemoved); |
|||
removeNode(nodeNumToBeRemoved); |
|||
onOpenChange(false); |
|||
}; |
|||
|
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent> |
|||
<DialogHeader> |
|||
<DialogTitle>Remove Node?</DialogTitle> |
|||
<DialogDescription> |
|||
Are you sure you want to remove this Node? |
|||
</DialogDescription> |
|||
</DialogHeader> |
|||
<div className="gap-4"> |
|||
<form onSubmit={onSubmit}> |
|||
<Label>{nodes.get(nodeNumToBeRemoved)?.user?.longName}</Label> |
|||
</form> |
|||
</div> |
|||
<DialogFooter> |
|||
<Button variant="destructive" onClick={() => onSubmit()}> |
|||
Remove |
|||
</Button> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,46 @@ |
|||
import type { |
|||
BaseFormBuilderProps, |
|||
GenericFormElementProps, |
|||
} from "@components/Form/DynamicForm.js"; |
|||
import { Generator } from "@components/UI/Generator.js"; |
|||
import type { ChangeEventHandler, MouseEventHandler } from "react"; |
|||
import { Controller, type FieldValues } from "react-hook-form"; |
|||
|
|||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> { |
|||
type: "passwordGenerator"; |
|||
hide?: boolean; |
|||
bits?: { text: string; value: string; key: string }[]; |
|||
devicePSKBitCount: number; |
|||
inputChange: ChangeEventHandler; |
|||
selectChange: (event: string) => void; |
|||
buttonClick: MouseEventHandler; |
|||
} |
|||
|
|||
export function PasswordGenerator<T extends FieldValues>({ |
|||
control, |
|||
field, |
|||
disabled, |
|||
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) { |
|||
return ( |
|||
<Controller |
|||
name={field.name} |
|||
control={control} |
|||
render={({ field: { value, ...rest } }) => ( |
|||
<Generator |
|||
hide={field.hide} |
|||
devicePSKBitCount={field.devicePSKBitCount} |
|||
bits={field.bits} |
|||
inputChange={field.inputChange} |
|||
selectChange={field.selectChange} |
|||
buttonClick={field.buttonClick} |
|||
value={value} |
|||
variant={field.validationText ? "invalid" : "default"} |
|||
buttonText="Generate" |
|||
{...field.properties} |
|||
{...rest} |
|||
disabled={disabled} |
|||
/> |
|||
)} |
|||
/> |
|||
); |
|||
} |
|||
@ -0,0 +1,237 @@ |
|||
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog"; |
|||
import { DynamicForm } from "@app/components/Form/DynamicForm.js"; |
|||
import { |
|||
getX25519PrivateKey, |
|||
getX25519PublicKey, |
|||
} from "@app/core/utils/x25519"; |
|||
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<string>( |
|||
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), |
|||
); |
|||
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false); |
|||
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>( |
|||
config.security?.privateKey.length ?? 32, |
|||
); |
|||
const [privateKeyValidationText, setPrivateKeyValidationText] = |
|||
useState<string>(); |
|||
const [publicKey, setPublicKey] = useState<string>( |
|||
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), |
|||
); |
|||
const [adminKey, setAdminKey] = useState<string>( |
|||
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)), |
|||
); |
|||
const [adminKeyValidationText, setAdminKeyValidationText] = |
|||
useState<string>(); |
|||
const [dialogOpen, setDialogOpen] = useState<boolean>(false); |
|||
|
|||
const onSubmit = (data: SecurityValidation) => { |
|||
if (privateKeyValidationText || adminKeyValidationText) return; |
|||
|
|||
setWorkingConfig( |
|||
new Protobuf.Config.Config({ |
|||
payloadVariant: { |
|||
case: "security", |
|||
value: { |
|||
...data, |
|||
adminKey: [toByteArray(adminKey)], |
|||
privateKey: toByteArray(privateKey), |
|||
publicKey: toByteArray(publicKey), |
|||
}, |
|||
}, |
|||
}), |
|||
); |
|||
}; |
|||
|
|||
const validateKey = ( |
|||
input: string, |
|||
count: number, |
|||
setValidationText: ( |
|||
value: React.SetStateAction<string | undefined>, |
|||
) => void, |
|||
) => { |
|||
try { |
|||
if (input.length % 4 !== 0 || toByteArray(input).length !== count) { |
|||
setValidationText(`Please enter a valid ${count * 8} bit PSK.`); |
|||
} else { |
|||
setValidationText(undefined); |
|||
} |
|||
} catch (e) { |
|||
console.error(e); |
|||
setValidationText(`Please enter a valid ${count * 8} bit PSK.`); |
|||
} |
|||
}; |
|||
|
|||
const privateKeyClickEvent = () => { |
|||
setDialogOpen(true); |
|||
}; |
|||
|
|||
const pkiRegenerate = () => { |
|||
const privateKey = getX25519PrivateKey(); |
|||
const publicKey = getX25519PublicKey(privateKey); |
|||
|
|||
setPrivateKey(fromByteArray(privateKey)); |
|||
setPublicKey(fromByteArray(publicKey)); |
|||
validateKey( |
|||
fromByteArray(privateKey), |
|||
privateKeyBitCount, |
|||
setPrivateKeyValidationText, |
|||
); |
|||
|
|||
setDialogOpen(false); |
|||
}; |
|||
|
|||
const privateKeyInputChangeEvent = ( |
|||
e: React.ChangeEvent<HTMLInputElement>, |
|||
) => { |
|||
const privateKeyB64String = e.target.value; |
|||
setPrivateKey(privateKeyB64String); |
|||
validateKey( |
|||
privateKeyB64String, |
|||
privateKeyBitCount, |
|||
setPrivateKeyValidationText, |
|||
); |
|||
|
|||
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String)); |
|||
setPublicKey(fromByteArray(publicKey)); |
|||
}; |
|||
|
|||
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => { |
|||
const psk = e.currentTarget?.value; |
|||
setAdminKey(psk); |
|||
validateKey(psk, privateKeyBitCount, setAdminKeyValidationText); |
|||
}; |
|||
|
|||
const privateKeySelectChangeEvent = (e: string) => { |
|||
const count = Number.parseInt(e); |
|||
setPrivateKeyBitCount(count); |
|||
validateKey(privateKey, count, setPrivateKeyValidationText); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<DynamicForm<SecurityValidation> |
|||
onSubmit={onSubmit} |
|||
submitType="onChange" |
|||
defaultValues={{ |
|||
...config.security, |
|||
...{ |
|||
adminKey: adminKey, |
|||
privateKey: privateKey, |
|||
publicKey: publicKey, |
|||
adminChannelEnabled: config.security?.adminChannelEnabled ?? false, |
|||
isManaged: config.security?.isManaged ?? false, |
|||
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false, |
|||
serialEnabled: config.security?.serialEnabled ?? false, |
|||
}, |
|||
}} |
|||
fieldGroups={[ |
|||
{ |
|||
label: "Security Settings", |
|||
description: "Settings for the Security configuration", |
|||
fields: [ |
|||
{ |
|||
type: "passwordGenerator", |
|||
name: "privateKey", |
|||
label: "Private Key", |
|||
description: "Used to create a shared key with a remote device", |
|||
bits: [{ text: "256 bit", value: "32", key: "bit256" }], |
|||
validationText: privateKeyValidationText, |
|||
devicePSKBitCount: privateKeyBitCount, |
|||
inputChange: privateKeyInputChangeEvent, |
|||
selectChange: privateKeySelectChangeEvent, |
|||
hide: !privateKeyVisible, |
|||
buttonClick: privateKeyClickEvent, |
|||
properties: { |
|||
value: privateKey, |
|||
action: { |
|||
icon: privateKeyVisible ? EyeOff : Eye, |
|||
onClick: () => setPrivateKeyVisible(!privateKeyVisible), |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
type: "text", |
|||
name: "publicKey", |
|||
label: "Public Key", |
|||
disabled: true, |
|||
description: |
|||
"Sent out to other nodes on the mesh to allow them to compute a shared secret key", |
|||
properties: { |
|||
value: publicKey, |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
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: "text", |
|||
name: "adminKey", |
|||
label: "Admin Key", |
|||
description: |
|||
"The public key authorized to send admin messages to this node", |
|||
validationText: adminKeyValidationText, |
|||
inputChange: adminKeyInputChangeEvent, |
|||
disabledBy: [ |
|||
{ fieldName: "adminChannelEnabled", invert: true }, |
|||
], |
|||
properties: { |
|||
value: adminKey, |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
label: "Logging Settings", |
|||
description: "Settings for Logging", |
|||
fields: [ |
|||
{ |
|||
type: "toggle", |
|||
name: "debugLogApiEnabled", |
|||
label: "Enable Debug Log API", |
|||
description: |
|||
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", |
|||
}, |
|||
{ |
|||
type: "toggle", |
|||
name: "serialEnabled", |
|||
label: "Serial Output Enabled", |
|||
description: "Serial Console over the Stream API", |
|||
}, |
|||
], |
|||
}, |
|||
]} |
|||
/> |
|||
<PkiRegenerateDialog |
|||
open={dialogOpen} |
|||
onOpenChange={() => setDialogOpen(false)} |
|||
onSubmit={() => pkiRegenerate()} |
|||
/> |
|||
</> |
|||
); |
|||
}; |
|||
@ -0,0 +1,36 @@ |
|||
import { useDevice } from "@app/core/stores/deviceStore.js"; |
|||
import type { Protobuf } from "@meshtastic/js"; |
|||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|||
|
|||
export interface TraceRouteProps { |
|||
from?: Protobuf.Mesh.NodeInfo; |
|||
to?: Protobuf.Mesh.NodeInfo; |
|||
route: Array<number>; |
|||
} |
|||
|
|||
export const TraceRoute = ({ |
|||
from, |
|||
to, |
|||
route, |
|||
}: TraceRouteProps): JSX.Element => { |
|||
const { nodes } = useDevice(); |
|||
|
|||
return route.length === 0 ? ( |
|||
<div className="ml-5 flex"> |
|||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary"> |
|||
{to?.user?.longName}↔{from?.user?.longName} |
|||
</span> |
|||
</div> |
|||
) : ( |
|||
<div className="ml-5 flex"> |
|||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary"> |
|||
{to?.user?.longName}↔ |
|||
{route.map((hop) => { |
|||
const node = nodes.get(hop); |
|||
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`; |
|||
})} |
|||
{from?.user?.longName} |
|||
</span> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,37 @@ |
|||
import React from "react"; |
|||
|
|||
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {} |
|||
|
|||
const Footer = React.forwardRef<HTMLElement, FooterProps>( |
|||
({ className, ...props }, ref) => { |
|||
return ( |
|||
<footer |
|||
className={`flex flex- justify-center p-2 ${className}`} |
|||
style={{ |
|||
backgroundColor: "var(--backgroundPrimary)", |
|||
color: "var(--textPrimary)", |
|||
}} |
|||
> |
|||
<p> |
|||
<a |
|||
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss" |
|||
className="hover:underline" |
|||
style={{ color: "var(--link)" }} |
|||
> |
|||
Powered by ▲ Vercel |
|||
</a>{" "} |
|||
| Meshtastic® is a registered trademark of Meshtastic LLC. |{" "} |
|||
<a |
|||
href="https://meshtastic.org/docs/legal" |
|||
className="hover:underline" |
|||
style={{ color: "var(--link)" }} |
|||
> |
|||
Legal Information |
|||
</a> |
|||
</p> |
|||
</footer> |
|||
); |
|||
}, |
|||
); |
|||
|
|||
export default Footer; |
|||
@ -0,0 +1,111 @@ |
|||
import * as React from "react"; |
|||
|
|||
import { Button } from "@components/UI/Button.js"; |
|||
import { Input } from "@components/UI/Input.js"; |
|||
import { |
|||
Select, |
|||
SelectContent, |
|||
SelectItem, |
|||
SelectTrigger, |
|||
SelectValue, |
|||
} from "@components/UI/Select.js"; |
|||
import type { LucideIcon } from "lucide-react"; |
|||
|
|||
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> { |
|||
hide?: boolean; |
|||
devicePSKBitCount?: number; |
|||
value: string; |
|||
variant: "default" | "invalid"; |
|||
buttonText?: string; |
|||
bits?: { text: string; value: string; key: string }[]; |
|||
selectChange: (event: string) => void; |
|||
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void; |
|||
buttonClick: React.MouseEventHandler<HTMLButtonElement>; |
|||
action?: { |
|||
icon: LucideIcon; |
|||
onClick: () => void; |
|||
}; |
|||
disabled?: boolean; |
|||
} |
|||
|
|||
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>( |
|||
( |
|||
{ |
|||
hide = true, |
|||
devicePSKBitCount, |
|||
variant, |
|||
value, |
|||
buttonText, |
|||
bits = [ |
|||
{ text: "256 bit", value: "32", key: "bit256" }, |
|||
{ text: "128 bit", value: "16", key: "bit128" }, |
|||
{ text: "8 bit", value: "1", key: "bit8" }, |
|||
], |
|||
selectChange, |
|||
inputChange, |
|||
buttonClick, |
|||
action, |
|||
disabled, |
|||
...props |
|||
}, |
|||
ref, |
|||
) => { |
|||
const inputRef = React.useRef<HTMLInputElement>(null); |
|||
|
|||
// Invokes onChange event on the input element when the value changes from the parent component
|
|||
React.useEffect(() => { |
|||
if (!inputRef.current) return; |
|||
const setValue = Object.getOwnPropertyDescriptor( |
|||
HTMLInputElement.prototype, |
|||
"value", |
|||
)?.set; |
|||
|
|||
if (!setValue) return; |
|||
inputRef.current.value = ""; |
|||
setValue.call(inputRef.current, value); |
|||
inputRef.current.dispatchEvent(new Event("input", { bubbles: true })); |
|||
}, [value]); |
|||
return ( |
|||
<> |
|||
<Input |
|||
type={hide ? "password" : "text"} |
|||
id="pskInput" |
|||
variant={variant} |
|||
value={value} |
|||
onChange={inputChange} |
|||
action={action} |
|||
disabled={disabled} |
|||
ref={inputRef} |
|||
/> |
|||
<Select |
|||
value={devicePSKBitCount?.toString()} |
|||
onValueChange={(e) => selectChange(e)} |
|||
disabled={disabled} |
|||
> |
|||
<SelectTrigger className="!max-w-max"> |
|||
<SelectValue /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{bits.map(({ text, value, key }) => ( |
|||
<SelectItem key={key} value={value}> |
|||
{text} |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
<Button |
|||
type="button" |
|||
variant="success" |
|||
onClick={buttonClick} |
|||
disabled={disabled} |
|||
{...props} |
|||
> |
|||
{buttonText} |
|||
</Button> |
|||
</> |
|||
); |
|||
}, |
|||
); |
|||
Generator.displayName = "Button"; |
|||
|
|||
export { Generator }; |
|||
@ -0,0 +1,14 @@ |
|||
export function convertIntToIpAddress(int: number): string { |
|||
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff}`; |
|||
} |
|||
|
|||
export function convertIpAddressToInt(ip: string): number | null { |
|||
return ( |
|||
ip |
|||
.split(".") |
|||
.reverse() |
|||
.reduce((ipnum, octet) => { |
|||
return (ipnum << 8) + Number.parseInt(octet); |
|||
}, 0) >>> 0 |
|||
); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
import { x25519 } from "@noble/curves/ed25519"; |
|||
|
|||
export function getX25519PrivateKey(): Uint8Array { |
|||
const key = x25519.utils.randomPrivateKey(); |
|||
|
|||
key[0] &= 248; |
|||
key[31] &= 127; |
|||
key[31] |= 64; |
|||
|
|||
return key; |
|||
} |
|||
|
|||
export function getX25519PublicKey(privateKey: Uint8Array): Uint8Array { |
|||
return x25519.getPublicKey(privateKey); |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -1,5 +1 @@ |
|||
{ |
|||
"github": { |
|||
"silent": true |
|||
} |
|||
} |
|||
{ "github": { "silent": true } } |
|||
|
|||
Loading…
Reference in new issue