Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
921db10d91 | ||
|
|
bf4f593e3a | ||
|
|
1e061a1e19 | ||
|
|
9b9f537e2c | ||
|
|
985cce0b0d | ||
|
|
3fe38eb506 | ||
|
|
51081d3052 | ||
|
|
c08f6d16bb | ||
|
|
62ad4c49f8 | ||
|
|
3b0a1e6108 | ||
|
|
c2f2205626 | ||
|
|
87c729d694 | ||
|
|
8e4f60edf3 | ||
|
|
8811eee9f5 | ||
|
|
2af93f1acd | ||
|
|
78a35544c7 | ||
|
|
3ad2d650b0 | ||
|
|
2e12b27566 | ||
|
|
989fad7e17 | ||
|
|
f7a2e5f76b | ||
|
|
2b81fc47e2 | ||
|
|
b522113cd7 | ||
|
|
fce642c24e | ||
|
|
bf425a8ec7 | ||
|
|
a7d0d36086 | ||
|
|
5e72510bef | ||
|
|
88efdc4758 | ||
|
|
41acc4d25d | ||
|
|
d10c010b9a | ||
|
|
80ab44c8db | ||
|
|
7895df2d9f | ||
|
|
c780437355 | ||
|
|
9d4aa05316 | ||
|
|
354d04592b | ||
|
|
9bea6870bb | ||
|
|
d0bd02980d | ||
|
|
7d1135b9dc | ||
|
|
f66332b3e3 | ||
|
|
05a6b6293e | ||
|
|
b9a8a2ba6c | ||
|
|
c16070f02b | ||
|
|
3e6653a98f | ||
|
|
59126ca939 | ||
|
|
4bde402a53 | ||
|
|
4cf91272de | ||
|
|
c711c39aa9 | ||
|
|
c4342f1a2b | ||
|
|
afc45588fa | ||
|
|
22cd5aa88d | ||
|
|
8c4c8a760e | ||
|
|
0911df6b0d | ||
|
|
06c20fa950 | ||
|
|
22d900a831 | ||
|
|
c6cc5e5e6f | ||
|
|
f0d8db1c87 | ||
|
|
8c37be4af3 | ||
|
|
076dae80b7 | ||
|
|
7d4001ea9d | ||
|
|
6c1f140ad1 | ||
|
|
4c4be2e18f |
@@ -1,6 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: push
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -21,42 +24,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Package Output
|
|
||||||
run: pnpm package
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
|
||||||
with:
|
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
automatic_release_tag: "latest"
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
./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 }}"
|
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Compress build
|
||||||
|
run: pnpm package
|
||||||
|
|
||||||
|
- name: Archive compressed build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist/build.tar
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
+5
-3
@@ -23,7 +23,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^1.10.0",
|
"@bufbuild/protobuf": "^1.10.0",
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@meshtastic/js": "2.3.7-1",
|
"@meshtastic/js": "2.3.7-5",
|
||||||
|
"@noble/curves": "^1.5.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.0",
|
"@radix-ui/react-checkbox": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "^3.6.0",
|
||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -59,11 +60,12 @@
|
|||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"timeago-react": "^3.0.6",
|
"timeago-react": "^3.0.6",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"zustand": "4.5.2"
|
"zustand": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.8.2",
|
"@biomejs/biome": "^1.8.2",
|
||||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1",
|
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
|
||||||
"@types/chrome": "^0.0.263",
|
"@types/chrome": "^0.0.263",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|||||||
Generated
+788
-20
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import { DeviceSelector } from "@components/DeviceSelector.js";
|
|||||||
import { DialogManager } from "@components/Dialog/DialogManager.js";
|
import { DialogManager } from "@components/Dialog/DialogManager.js";
|
||||||
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js";
|
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js";
|
||||||
import { Toaster } from "@components/Toaster.js";
|
import { Toaster } from "@components/Toaster.js";
|
||||||
|
import Footer from "@components/UI/Footer.js";
|
||||||
import { ThemeController } from "@components/generic/ThemeController.js";
|
import { ThemeController } from "@components/generic/ThemeController.js";
|
||||||
import { useAppStore } from "@core/stores/appStore.js";
|
import { useAppStore } from "@core/stores/appStore.js";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||||
@@ -40,7 +41,11 @@ export const App = (): JSX.Element => {
|
|||||||
<PageRouter />
|
<PageRouter />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,11 +4,14 @@ import type {
|
|||||||
} from "@components/Form/DynamicForm.js";
|
} from "@components/Form/DynamicForm.js";
|
||||||
import { Input } from "@components/UI/Input.js";
|
import { Input } from "@components/UI/Input.js";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import type { ChangeEventHandler } from "react";
|
||||||
import { Controller, type FieldValues } from "react-hook-form";
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "text" | "number" | "password";
|
type: "text" | "number" | "password";
|
||||||
|
inputChange?: ChangeEventHandler;
|
||||||
properties?: {
|
properties?: {
|
||||||
|
value?: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
step?: number;
|
step?: number;
|
||||||
@@ -33,13 +36,14 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
type={field.type}
|
type={field.type}
|
||||||
step={field.properties?.step}
|
step={field.properties?.step}
|
||||||
value={field.type === "number" ? Number.parseFloat(value) : value}
|
value={field.type === "number" ? Number.parseFloat(value) : value}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
if (field.inputChange) field.inputChange(e);
|
||||||
onChange(
|
onChange(
|
||||||
field.type === "number"
|
field.type === "number"
|
||||||
? Number.parseFloat(e.target.value)
|
? Number.parseFloat(e.target.value)
|
||||||
: e.target.value,
|
: e.target.value,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
{...field.properties}
|
{...field.properties}
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Controller, type FieldValues } from "react-hook-form";
|
|||||||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "passwordGenerator";
|
type: "passwordGenerator";
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
devicePSKBitCount: number;
|
devicePSKBitCount: number;
|
||||||
inputChange: ChangeEventHandler;
|
inputChange: ChangeEventHandler;
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
@@ -28,6 +29,7 @@ export function PasswordGenerator<T extends FieldValues>({
|
|||||||
<Generator
|
<Generator
|
||||||
hide={field.hide}
|
hide={field.hide}
|
||||||
devicePSKBitCount={field.devicePSKBitCount}
|
devicePSKBitCount={field.devicePSKBitCount}
|
||||||
|
bits={field.bits}
|
||||||
inputChange={field.inputChange}
|
inputChange={field.inputChange}
|
||||||
selectChange={field.selectChange}
|
selectChange={field.selectChange}
|
||||||
buttonClick={field.buttonClick}
|
buttonClick={field.buttonClick}
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
|
|||||||
label: "Ignore MQTT",
|
label: "Ignore MQTT",
|
||||||
description: "Don't forward MQTT messages over the mesh",
|
description: "Don't forward MQTT messages over the mesh",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "configOkToMqtt",
|
||||||
|
label: "OK to MQTT",
|
||||||
|
description:
|
||||||
|
"When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
||||||
import { DynamicForm } from "@app/components/Form/DynamicForm.js";
|
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 type { SecurityValidation } from "@app/validation/config/security.js";
|
||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -15,7 +19,7 @@ export const Security = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
|
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
|
||||||
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
|
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
|
||||||
config.security?.privateKey.length ?? 16,
|
config.security?.privateKey.length ?? 32,
|
||||||
);
|
);
|
||||||
const [privateKeyValidationText, setPrivateKeyValidationText] =
|
const [privateKeyValidationText, setPrivateKeyValidationText] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
@@ -23,14 +27,11 @@ export const Security = (): JSX.Element => {
|
|||||||
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
||||||
);
|
);
|
||||||
const [adminKey, setAdminKey] = useState<string>(
|
const [adminKey, setAdminKey] = useState<string>(
|
||||||
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
|
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
|
||||||
);
|
|
||||||
const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false);
|
|
||||||
const [adminKeyBitCount, setAdminKeyBitCount] = useState<number>(
|
|
||||||
config.security?.adminKey.length ?? 16,
|
|
||||||
);
|
);
|
||||||
const [adminKeyValidationText, setAdminKeyValidationText] =
|
const [adminKeyValidationText, setAdminKeyValidationText] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const onSubmit = (data: SecurityValidation) => {
|
const onSubmit = (data: SecurityValidation) => {
|
||||||
if (privateKeyValidationText || adminKeyValidationText) return;
|
if (privateKeyValidationText || adminKeyValidationText) return;
|
||||||
@@ -41,7 +42,7 @@ export const Security = (): JSX.Element => {
|
|||||||
case: "security",
|
case: "security",
|
||||||
value: {
|
value: {
|
||||||
...data,
|
...data,
|
||||||
adminKey: toByteArray(adminKey),
|
adminKey: [toByteArray(adminKey)],
|
||||||
privateKey: toByteArray(privateKey),
|
privateKey: toByteArray(privateKey),
|
||||||
publicKey: toByteArray(publicKey),
|
publicKey: toByteArray(publicKey),
|
||||||
},
|
},
|
||||||
@@ -50,73 +51,86 @@ export const Security = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickEvent = (
|
const validateKey = (
|
||||||
setKey: (value: React.SetStateAction<string>) => void,
|
|
||||||
bitCount: number,
|
|
||||||
setValidationText: (
|
|
||||||
value: React.SetStateAction<string | undefined>,
|
|
||||||
) => void,
|
|
||||||
) => {
|
|
||||||
setKey(
|
|
||||||
btoa(
|
|
||||||
cryptoRandomString({
|
|
||||||
length: bitCount ?? 0,
|
|
||||||
type: "alphanumeric",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setValidationText(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePass = (
|
|
||||||
input: string,
|
input: string,
|
||||||
count: number,
|
count: number,
|
||||||
setValidationText: (
|
setValidationText: (
|
||||||
value: React.SetStateAction<string | undefined>,
|
value: React.SetStateAction<string | undefined>,
|
||||||
) => void,
|
) => void,
|
||||||
) => {
|
) => {
|
||||||
|
try {
|
||||||
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
||||||
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
} else {
|
} else {
|
||||||
setValidationText(undefined);
|
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 = (
|
const privateKeyInputChangeEvent = (
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
const psk = e.currentTarget?.value;
|
const privateKeyB64String = e.target.value;
|
||||||
setPrivateKey(psk);
|
setPrivateKey(privateKeyB64String);
|
||||||
validatePass(psk, privateKeyBitCount, setPrivateKeyValidationText);
|
validateKey(
|
||||||
|
privateKeyB64String,
|
||||||
|
privateKeyBitCount,
|
||||||
|
setPrivateKeyValidationText,
|
||||||
|
);
|
||||||
|
|
||||||
|
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
|
||||||
|
setPublicKey(fromByteArray(publicKey));
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const psk = e.currentTarget?.value;
|
const psk = e.currentTarget?.value;
|
||||||
setAdminKey(psk);
|
setAdminKey(psk);
|
||||||
validatePass(psk, privateKeyBitCount, setAdminKeyValidationText);
|
validateKey(psk, privateKeyBitCount, setAdminKeyValidationText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const privateKeySelectChangeEvent = (e: string) => {
|
const privateKeySelectChangeEvent = (e: string) => {
|
||||||
const count = Number.parseInt(e);
|
const count = Number.parseInt(e);
|
||||||
setPrivateKeyBitCount(count);
|
setPrivateKeyBitCount(count);
|
||||||
validatePass(privateKey, count, setPrivateKeyValidationText);
|
validateKey(privateKey, count, setPrivateKeyValidationText);
|
||||||
};
|
|
||||||
|
|
||||||
const adminKeySelectChangeEvent = (e: string) => {
|
|
||||||
const count = Number.parseInt(e);
|
|
||||||
setAdminKeyBitCount(count);
|
|
||||||
validatePass(privateKey, count, setAdminKeyValidationText);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<DynamicForm<SecurityValidation>
|
<DynamicForm<SecurityValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
submitType="onChange"
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
...config.security,
|
...config.security,
|
||||||
...{
|
...{
|
||||||
adminKey: adminKey,
|
adminKey: adminKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
|
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||||
|
isManaged: config.security?.isManaged ?? false,
|
||||||
|
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||||
|
serialEnabled: config.security?.serialEnabled ?? false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fieldGroups={[
|
fieldGroups={[
|
||||||
@@ -129,23 +143,13 @@ export const Security = (): JSX.Element => {
|
|||||||
name: "privateKey",
|
name: "privateKey",
|
||||||
label: "Private Key",
|
label: "Private Key",
|
||||||
description: "Used to create a shared key with a remote device",
|
description: "Used to create a shared key with a remote device",
|
||||||
|
bits: [{ text: "256 bit", value: "32", key: "bit256" }],
|
||||||
validationText: privateKeyValidationText,
|
validationText: privateKeyValidationText,
|
||||||
devicePSKBitCount: privateKeyBitCount,
|
devicePSKBitCount: privateKeyBitCount,
|
||||||
inputChange: privateKeyInputChangeEvent,
|
inputChange: privateKeyInputChangeEvent,
|
||||||
selectChange: privateKeySelectChangeEvent,
|
selectChange: privateKeySelectChangeEvent,
|
||||||
hide: !privateKeyVisible,
|
hide: !privateKeyVisible,
|
||||||
buttonClick: () =>
|
buttonClick: privateKeyClickEvent,
|
||||||
clickEvent(
|
|
||||||
setPrivateKey,
|
|
||||||
privateKeyBitCount,
|
|
||||||
setPrivateKeyValidationText,
|
|
||||||
),
|
|
||||||
disabledBy: [
|
|
||||||
{
|
|
||||||
fieldName: "adminChannelEnabled",
|
|
||||||
invert: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
properties: {
|
properties: {
|
||||||
value: privateKey,
|
value: privateKey,
|
||||||
action: {
|
action: {
|
||||||
@@ -161,6 +165,9 @@ export const Security = (): JSX.Element => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
description:
|
description:
|
||||||
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
|
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
|
||||||
|
properties: {
|
||||||
|
value: publicKey,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -183,29 +190,18 @@ export const Security = (): JSX.Element => {
|
|||||||
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "passwordGenerator",
|
type: "text",
|
||||||
name: "adminKey",
|
name: "adminKey",
|
||||||
label: "Admin Key",
|
label: "Admin Key",
|
||||||
description:
|
description:
|
||||||
"The public key authorized to send admin messages to this node",
|
"The public key authorized to send admin messages to this node",
|
||||||
validationText: adminKeyValidationText,
|
validationText: adminKeyValidationText,
|
||||||
devicePSKBitCount: adminKeyBitCount,
|
|
||||||
inputChange: adminKeyInputChangeEvent,
|
inputChange: adminKeyInputChangeEvent,
|
||||||
selectChange: adminKeySelectChangeEvent,
|
disabledBy: [
|
||||||
hide: !adminKeyVisible,
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
buttonClick: () =>
|
],
|
||||||
clickEvent(
|
|
||||||
setAdminKey,
|
|
||||||
adminKeyBitCount,
|
|
||||||
setAdminKeyValidationText,
|
|
||||||
),
|
|
||||||
disabledBy: [{ fieldName: "adminChannelEnabled" }],
|
|
||||||
properties: {
|
properties: {
|
||||||
value: adminKey,
|
value: adminKey,
|
||||||
action: {
|
|
||||||
icon: adminKeyVisible ? EyeOff : Eye,
|
|
||||||
onClick: () => setAdminKeyVisible(!adminKeyVisible),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -214,17 +210,12 @@ export const Security = (): JSX.Element => {
|
|||||||
label: "Logging Settings",
|
label: "Logging Settings",
|
||||||
description: "Settings for Logging",
|
description: "Settings for Logging",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "bluetoothLoggingEnabled",
|
|
||||||
label: "Allow Bluetooth Logging",
|
|
||||||
description: "Enables device (serial style logs) over Bluetooth",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "debugLogApiEnabled",
|
name: "debugLogApiEnabled",
|
||||||
label: "Enable Debug Log API",
|
label: "Enable Debug Log API",
|
||||||
description: "Output live debug logging over serial",
|
description:
|
||||||
|
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
@@ -236,5 +227,11 @@ export const Security = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<PkiRegenerateDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={() => setDialogOpen(false)}
|
||||||
|
onSubmit={() => pkiRegenerate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ChannelChat = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="pl-3 pr-3 pt-3 pb-1">
|
||||||
<MessageInput to={to} channel={channel} />
|
<MessageInput to={to} channel={channel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
description: "How often to send Power data over the mesh",
|
description: "How often to send Power data over the mesh",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "toggle",
|
||||||
name: "powerScreenEnabled",
|
name: "powerScreenEnabled",
|
||||||
label: "Power Screen Enabled",
|
label: "Power Screen Enabled",
|
||||||
description: "Enable the Power Telemetry Screen",
|
description: "Enable the Power Telemetry Screen",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from "@app/core/utils/cn.js";
|
import { cn } from "@app/core/utils/cn.js";
|
||||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||||
|
import Footer from "./UI/Footer";
|
||||||
|
|
||||||
export interface PageLayoutProps {
|
export interface PageLayoutProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -7,6 +8,7 @@ export interface PageLayoutProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions?: {
|
actions?: {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
iconClasses?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -18,6 +20,7 @@ export const PageLayout = ({
|
|||||||
children,
|
children,
|
||||||
}: PageLayoutProps): JSX.Element => {
|
}: PageLayoutProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="relative flex h-full w-full flex-col">
|
<div className="relative flex h-full w-full flex-col">
|
||||||
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
|
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +40,7 @@ export const PageLayout = ({
|
|||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon />
|
<action.icon className={action.iconClasses} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,11 +50,13 @@ export const PageLayout = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-col overflow-y-auto",
|
"flex h-full w-full flex-col overflow-y-auto",
|
||||||
!noPadding && "p-3",
|
!noPadding && "pl-3 pr-3 ",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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;
|
||||||
@@ -17,6 +17,7 @@ export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
|||||||
value: string;
|
value: string;
|
||||||
variant: "default" | "invalid";
|
variant: "default" | "invalid";
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
@@ -35,6 +36,11 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
|||||||
variant,
|
variant,
|
||||||
value,
|
value,
|
||||||
buttonText,
|
buttonText,
|
||||||
|
bits = [
|
||||||
|
{ text: "256 bit", value: "32", key: "bit256" },
|
||||||
|
{ text: "128 bit", value: "16", key: "bit128" },
|
||||||
|
{ text: "8 bit", value: "1", key: "bit8" },
|
||||||
|
],
|
||||||
selectChange,
|
selectChange,
|
||||||
inputChange,
|
inputChange,
|
||||||
buttonClick,
|
buttonClick,
|
||||||
@@ -44,6 +50,21 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -54,30 +75,29 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
|||||||
onChange={inputChange}
|
onChange={inputChange}
|
||||||
action={action}
|
action={action}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={devicePSKBitCount?.toString()}
|
value={devicePSKBitCount?.toString()}
|
||||||
onValueChange={(e) => selectChange(e)}
|
onValueChange={(e) => selectChange(e)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="!max-w-max">
|
<SelectTrigger className="!max-w-max">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem key="bit256" value="32">
|
{bits.map(({ text, value, key }) => (
|
||||||
256 bit
|
<SelectItem key={key} value={value}>
|
||||||
</SelectItem>
|
{text}
|
||||||
<SelectItem key="bit128" value="16">
|
|
||||||
128 bit
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="bit8" value="1">
|
|
||||||
8 bit
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="success"
|
variant="success"
|
||||||
onClick={buttonClick}
|
onClick={buttonClick}
|
||||||
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface InputProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, variant, prefix, suffix, action, ...props }, ref) => {
|
({ className, value, variant, prefix, suffix, action, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
@@ -45,6 +45,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className,
|
className,
|
||||||
inputVariants({ variant }),
|
inputVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
|
value={value}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 dark:bg-slate-800",
|
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 mt-2 dark:bg-slate-800",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ export const Dashboard = () => {
|
|||||||
const devices = useMemo(() => getDevices(), [getDevices]);
|
const devices = useMemo(() => getDevices(), [getDevices]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -95,5 +96,6 @@ export const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-1
@@ -8,7 +8,7 @@ import { useDevice } from "@core/stores/deviceStore.js";
|
|||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, Types } from "@meshtastic/js";
|
||||||
import { getChannelName } from "@pages/Channels.js";
|
import { getChannelName } from "@pages/Channels.js";
|
||||||
import { HashIcon, WaypointsIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MessagesPage = (): JSX.Element => {
|
export const MessagesPage = (): JSX.Element => {
|
||||||
@@ -67,6 +67,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
<div className="flex flex-col flex-grow">
|
||||||
<PageLayout
|
<PageLayout
|
||||||
label={`Messages: ${
|
label={`Messages: ${
|
||||||
chatType === "broadcast" && currentChannel
|
chatType === "broadcast" && currentChannel
|
||||||
@@ -78,6 +79,23 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
actions={
|
actions={
|
||||||
chatType === "direct"
|
chatType === "direct"
|
||||||
? [
|
? [
|
||||||
|
{
|
||||||
|
icon: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? LockIcon
|
||||||
|
: LockOpenIcon,
|
||||||
|
iconClasses: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "text-green-600"
|
||||||
|
: "text-yellow-300",
|
||||||
|
async onClick() {
|
||||||
|
const targetNode = nodes.get(activeChat)?.num;
|
||||||
|
if (targetNode === undefined) return;
|
||||||
|
toast({
|
||||||
|
title: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "Chat is using PKI encryption."
|
||||||
|
: "Chat is using PSK encryption.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: WaypointsIcon,
|
icon: WaypointsIcon,
|
||||||
async onClick() {
|
async onClick() {
|
||||||
@@ -121,6 +139,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import Footer from "@app/components/UI/Footer";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
import { useAppStore } from "@app/core/stores/appStore";
|
||||||
import { Sidebar } from "@components/Sidebar.js";
|
import { Sidebar } from "@components/Sidebar.js";
|
||||||
import { Button } from "@components/UI/Button.js";
|
import { Button } from "@components/UI/Button.js";
|
||||||
@@ -7,7 +8,7 @@ import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="w-full overflow-y-auto">
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="overflow-y-auto h-full">
|
||||||
<Table
|
<Table
|
||||||
headings={[
|
headings={[
|
||||||
{ title: "", type: "blank", sortable: false },
|
{ title: "", type: "blank", sortable: false },
|
||||||
@@ -36,6 +38,7 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{ title: "MAC Address", type: "normal", sortable: true },
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
{ title: "Last Heard", type: "normal", sortable: true },
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
{ title: "SNR", type: "normal", sortable: true },
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
{ title: "Connection", type: "normal", sortable: true },
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
{ title: "Remove", type: "normal", sortable: false },
|
{ title: "Remove", type: "normal", sortable: false },
|
||||||
]}
|
]}
|
||||||
@@ -71,6 +74,13 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||||
{(node.snr + 10) * 5}raw
|
{(node.snr + 10) * 5}raw
|
||||||
</Mono>,
|
</Mono>,
|
||||||
|
<Mono key="pki">
|
||||||
|
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||||
|
<LockIcon className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<LockOpenIcon className="text-yellow-300" />
|
||||||
|
)}
|
||||||
|
</Mono>,
|
||||||
<Mono key="hops">
|
<Mono key="hops">
|
||||||
{node.lastHeard !== 0
|
{node.lastHeard !== 0
|
||||||
? node.viaMqtt === false && node.hopsAway === 0
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
@@ -95,6 +105,8 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,4 +60,7 @@ export class LoRaValidation
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
ignoreMqtt: boolean;
|
ignoreMqtt: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
configOkToMqtt: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
import { visualizer } from "rollup-plugin-visualizer";
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import EnvironmentPlugin from "vite-plugin-environment";
|
import EnvironmentPlugin from "vite-plugin-environment";
|
||||||
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
|
|
||||||
let hash = "";
|
let hash = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user