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.defaultFormatter": "biomejs.biome", |
||||
|
"editor.codeActionsOnSave": { |
||||
|
"quickfix.biome": "explicit" |
||||
|
}, |
||||
"editor.formatOnSave": true |
"editor.formatOnSave": true |
||||
} |
} |
||||
File diff suppressed because it is too large
@ -1,6 +1,6 @@ |
|||||
module.exports = { |
module.exports = { |
||||
plugins: { |
plugins: { |
||||
tailwindcss: {}, |
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