diff --git a/package.json b/package.json index e56bdaed..79020d5f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "dependencies": { "@bufbuild/protobuf": "^1.10.0", "@emeraldpay/hashicon-react": "^0.5.2", - "@meshtastic/js": "2.3.7-0", + "@meshtastic/js": "2.3.7-1", + "@noble/curves": "^1.5.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", @@ -48,7 +49,7 @@ "crypto-random-string": "^5.0.0", "immer": "^10.1.1", "lucide-react": "^0.363.0", - "mapbox-gl": "npm:empty-npm-package@^1.0.0", + "mapbox-gl": "^3.6.0", "maplibre-gl": "4.1.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -63,7 +64,7 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.2", - "@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240613143006-244927bc441a.1", + "@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1", "@types/chrome": "^0.0.263", "@types/node": "^20.14.9", "@types/react": "^18.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 938ea82e..58131bb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,11 @@ importers: specifier: ^0.5.2 version: 0.5.2 '@meshtastic/js': - specifier: 2.3.7-0 - version: 2.3.7-0 + specifier: 2.3.7-1 + version: 2.3.7-1 + '@noble/curves': + specifier: ^1.5.0 + version: 1.5.0 '@radix-ui/react-accordion': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -90,8 +93,8 @@ importers: specifier: ^0.363.0 version: 0.363.0(react@18.3.1) mapbox-gl: - specifier: npm:empty-npm-package@^1.0.0 - version: empty-npm-package@1.0.0 + specifier: ^3.6.0 + version: 3.6.0 maplibre-gl: specifier: 4.1.2 version: 4.1.2 @@ -106,7 +109,7 @@ importers: version: 7.52.0(react@18.3.1) react-map-gl: specifier: 7.1.7 - version: 7.1.7(empty-npm-package@1.0.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 7.1.7(mapbox-gl@3.6.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-qrcode-logo: specifier: ^2.10.0 version: 2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -130,8 +133,8 @@ importers: specifier: ^1.8.2 version: 1.8.2 '@buf/meshtastic_protobufs.bufbuild_es': - specifier: 1.10.0-20240613143006-244927bc441a.1 - version: 1.10.0-20240613143006-244927bc441a.1(@bufbuild/protobuf@1.10.0) + specifier: 1.10.0-20240906232734-3da561588c55.1 + version: 1.10.0-20240906232734-3da561588c55.1(@bufbuild/protobuf@1.10.0) '@types/chrome': specifier: ^0.0.263 version: 0.0.263 @@ -354,8 +357,8 @@ packages: cpu: [x64] os: [win32] - '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240613143006-244927bc441a.1': - resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240613143006-244927bc441a.1.tgz} + '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240906232734-3da561588c55.1': + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240906232734-3da561588c55.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.10.0 @@ -557,6 +560,9 @@ packages: resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} engines: {node: '>= 0.6'} + '@mapbox/mapbox-gl-supported@3.0.0': + resolution: {integrity: sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==} + '@mapbox/point-geometry@0.1.0': resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} @@ -581,8 +587,15 @@ packages: resolution: {integrity: sha512-eSiQ3E5LUSxAOY9ABXGyfNhout2iEa6mUxKeaQ9nJ8NL1NuaQYU7zKqzx/LEYcXe1neT4uYAgM1wYZj3fTSXtA==} hasBin: true - '@meshtastic/js@2.3.7-0': - resolution: {integrity: sha512-XTNyUXj3SWQ91XqwgrTZT7rTQsiI3d8noRaxnpxRw6Ck7WtjjPF0ygnPA8eQ6kastyUkgpXzcjtD9a6Qz6n+WQ==} + '@meshtastic/js@2.3.7-1': + resolution: {integrity: sha512-pv+Xk6HkKrScCrQp31k5QOUYozabXn6NhXN7c7Cc9ysG94U1wGtfueRbEbFxXCHO3JshNz0CdE1FcSMnrLMjsQ==} + + '@noble/curves@1.5.0': + resolution: {integrity: sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1823,6 +1836,9 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + cheap-ruler@4.0.0: + resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1908,6 +1924,9 @@ packages: resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} engines: {node: '>=14.16'} + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1975,6 +1994,9 @@ packages: earcut@2.2.4: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + earcut@3.0.0: + resolution: {integrity: sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1987,9 +2009,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - empty-npm-package@1.0.0: - resolution: {integrity: sha512-q4Mq/+XO7UNDdMiPpR/LIBIW1Zl4V0Z6UT9aKGqIAnBCtCb3lvZJM1KbDbdzdC8fKflwflModfjR29Nt0EpcwA==} - end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -2032,6 +2051,9 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2074,6 +2096,9 @@ packages: geojson-vt@3.2.1: resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} + geojson-vt@4.0.2: + resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2121,6 +2146,9 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + gzipper@7.2.0: resolution: {integrity: sha512-qwYQr7GWBXIm9Cdzud+tyM/s9N+QFzGDZoF9YR8RYJbDKOYowzjMDPEinFtm78EQeeYMC/FJW2FXY0bHkyUgsA==} engines: {node: '>=14'} @@ -2361,6 +2389,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 + mapbox-gl@3.6.0: + resolution: {integrity: sha512-xjYHHIJDh6haYcKY+/9jh1eywwYfIOWCgT5Fowj4JriZexx/oOtg2S7BQDMZtpFyg9IN4VLCysmUWxY0pFNRWA==} + maplibre-gl@4.1.2: resolution: {integrity: sha512-98T+3BesL4w/N39q/rgs9q6HzHLG6pgbS9UaTqg6fMISfzy2WGKokjK205ENFDDmEljj54/LTfdXgqg2XfYU4A==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} @@ -2575,6 +2606,9 @@ packages: quickselect@2.0.0: resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + quotemeta@0.0.0: resolution: {integrity: sha512-1XGObUh7RN5b58vKuAsrlfqT+Rc4vmw8N4pP9gFCq1GFlTdV0Ex/D2Ro1Drvrqj++HPi3ig0Np17XPslELeMRA==} @@ -2748,6 +2782,10 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + serialize-to-js@3.1.2: + resolution: {integrity: sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==} + engines: {node: '>=4.0.0'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2907,6 +2945,9 @@ packages: tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -2936,6 +2977,9 @@ packages: turf-jsts@1.2.3: resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==} + tweakpane@4.0.4: + resolution: {integrity: sha512-RkWD54zDlEbnN01wQPk0ANHGbdCvlJx/E8A1QxhTfCbX+ROWos1Ws2MnhOm39aUGMOh+36TjUwpDmLfmwTr1Fg==} + type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -3306,7 +3350,7 @@ snapshots: '@biomejs/cli-win32-x64@1.8.2': optional: true - '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240613143006-244927bc441a.1(@bufbuild/protobuf@1.10.0)': + '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240906232734-3da561588c55.1(@bufbuild/protobuf@1.10.0)': dependencies: '@bufbuild/protobuf': 1.10.0 @@ -3445,6 +3489,8 @@ snapshots: '@mapbox/jsonlint-lines-primitives@2.0.2': {} + '@mapbox/mapbox-gl-supported@3.0.0': {} + '@mapbox/point-geometry@0.1.0': {} '@mapbox/tiny-sdf@2.0.6': {} @@ -3477,7 +3523,7 @@ snapshots: sort-object: 3.0.3 tinyqueue: 2.0.3 - '@meshtastic/js@2.3.7-0': + '@meshtastic/js@2.3.7-1': dependencies: crc: 4.3.2 ste-simple-events: 3.0.11 @@ -3485,6 +3531,12 @@ snapshots: transitivePeerDependencies: - buffer + '@noble/curves@1.5.0': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/hashes@1.4.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5221,6 +5273,8 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + cheap-ruler@4.0.0: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5308,6 +5362,8 @@ snapshots: dependencies: type-fest: 2.19.0 + csscolorparser@1.0.3: {} + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -5387,6 +5443,8 @@ snapshots: earcut@2.2.4: {} + earcut@3.0.0: {} + eastasianwidth@0.2.0: {} electron-to-chromium@1.4.812: {} @@ -5395,8 +5453,6 @@ snapshots: emoji-regex@9.2.2: {} - empty-npm-package@1.0.0: {} - end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -5470,6 +5526,8 @@ snapshots: dependencies: reusify: 1.0.4 + fflate@0.8.2: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5512,6 +5570,8 @@ snapshots: geojson-vt@3.2.1: {} + geojson-vt@4.0.2: {} + get-caller-file@2.0.5: {} get-intrinsic@1.2.4: @@ -5559,6 +5619,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + grid-index@1.1.0: {} + gzipper@7.2.0: dependencies: '@gfx/zopfli': 1.0.15 @@ -5754,6 +5816,36 @@ snapshots: dependencies: react: 18.3.1 + mapbox-gl@3.6.0: + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 3.0.0 + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 2.0.6 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + '@types/geojson': 7946.0.14 + '@types/mapbox__vector-tile': 1.3.4 + cheap-ruler: 4.0.0 + csscolorparser: 1.0.3 + earcut: 3.0.0 + fflate: 0.8.2 + geojson-vt: 4.0.2 + gl-matrix: 3.4.3 + grid-index: 1.1.0 + kdbush: 4.0.2 + murmurhash-js: 1.0.0 + pbf: 3.2.1 + potpack: 2.0.0 + quickselect: 3.0.0 + rw: 1.3.3 + serialize-to-js: 3.1.2 + supercluster: 8.0.1 + tinyqueue: 3.0.0 + tweakpane: 4.0.4 + vt-pbf: 3.1.3 + maplibre-gl@4.1.2: dependencies: '@mapbox/geojson-rewind': 0.5.2 @@ -5963,6 +6055,8 @@ snapshots: quickselect@2.0.0: {} + quickselect@3.0.0: {} + quotemeta@0.0.0: {} rbush@2.0.2: @@ -5985,14 +6079,14 @@ snapshots: react-is@16.13.1: {} - react-map-gl@7.1.7(empty-npm-package@1.0.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-map-gl@7.1.7(mapbox-gl@3.6.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@maplibre/maplibre-gl-style-spec': 19.3.3 '@types/mapbox-gl': 3.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - mapbox-gl: empty-npm-package@1.0.0 + mapbox-gl: 3.6.0 maplibre-gl: 4.1.2 react-qrcode-logo@2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -6153,6 +6247,8 @@ snapshots: semver@6.3.1: {} + serialize-to-js@3.1.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6358,6 +6454,8 @@ snapshots: tinyqueue@2.0.3: {} + tinyqueue@3.0.0: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -6380,6 +6478,8 @@ snapshots: turf-jsts@1.2.3: {} + tweakpane@4.0.4: {} + type-fest@2.19.0: {} typescript@5.5.2: {} diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 07f456e1..2062b740 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -200,10 +200,17 @@ export const CommandPalette = (): JSX.Element => { }, }, { - label: "Factory Reset", + label: "Factory Reset Device", icon: FactoryIcon, action() { - connection?.factoryReset(); + connection?.factoryResetDevice(); + }, + }, + { + label: "Factory Reset Config", + icon: FactoryIcon, + action() { + connection?.factoryResetConfig(); }, }, ], diff --git a/src/components/Dialog/PkiRegenerateDialog.tsx b/src/components/Dialog/PkiRegenerateDialog.tsx new file mode 100644 index 00000000..3edc221a --- /dev/null +++ b/src/components/Dialog/PkiRegenerateDialog.tsx @@ -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 ( + + + + Regenerate Key pair? + + Are you sure you want to regenerate key pair? + + + + + + + + ); +}; diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 61c4c0e7..f957e7f0 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -23,6 +23,7 @@ interface DisabledBy { export interface BaseFormBuilderProps { name: Path; + disabled?: boolean; disabledBy?: DisabledBy[]; label: string; description?: string; @@ -62,11 +63,16 @@ export function DynamicForm({ defaultValues: defaultValues, }); - const isDisabled = (disabledBy?: DisabledBy[]): boolean => { + const isDisabled = ( + disabledBy?: DisabledBy[], + disabled?: boolean, + ): boolean => { + if (disabled) return true; if (!disabledBy) return false; return disabledBy.some((field) => { const value = getValues(field.fieldName); + if (value === "always") return true; if (typeof value === "boolean") return field.invert ? value : !value; if (typeof value === "number") return field.invert @@ -109,7 +115,7 @@ export function DynamicForm({ ))} diff --git a/src/components/Form/FormInput.tsx b/src/components/Form/FormInput.tsx index fced1b00..13f77260 100644 --- a/src/components/Form/FormInput.tsx +++ b/src/components/Form/FormInput.tsx @@ -4,11 +4,14 @@ import type { } from "@components/Form/DynamicForm.js"; import { Input } from "@components/UI/Input.js"; import type { LucideIcon } from "lucide-react"; +import type { ChangeEventHandler } from "react"; import { Controller, type FieldValues } from "react-hook-form"; export interface InputFieldProps extends BaseFormBuilderProps { type: "text" | "number" | "password"; + inputChange?: ChangeEventHandler; properties?: { + value?: string; prefix?: string; suffix?: string; step?: number; @@ -33,13 +36,14 @@ export function GenericInput({ type={field.type} step={field.properties?.step} value={field.type === "number" ? Number.parseFloat(value) : value} - onChange={(e) => + onChange={(e) => { + if (field.inputChange) field.inputChange(e); onChange( field.type === "number" ? Number.parseFloat(e.target.value) : e.target.value, - ) - } + ); + }} {...field.properties} {...rest} disabled={disabled} diff --git a/src/components/Form/FormPasswordGenerator.tsx b/src/components/Form/FormPasswordGenerator.tsx index d015d3af..cf05f806 100644 --- a/src/components/Form/FormPasswordGenerator.tsx +++ b/src/components/Form/FormPasswordGenerator.tsx @@ -8,6 +8,8 @@ import { Controller, type FieldValues } from "react-hook-form"; export interface PasswordGeneratorProps extends BaseFormBuilderProps { type: "passwordGenerator"; + hide?: boolean; + bits?: { text: string; value: string; key: string }[]; devicePSKBitCount: number; inputChange: ChangeEventHandler; selectChange: (event: string) => void; @@ -17,6 +19,7 @@ export interface PasswordGeneratorProps extends BaseFormBuilderProps { export function PasswordGenerator({ control, field, + disabled, }: GenericFormElementProps>) { return ( ({ control={control} render={({ field: { value, ...rest } }) => ( ({ buttonText="Generate" {...field.properties} {...rest} + disabled={disabled} /> )} /> diff --git a/src/components/PageComponents/Config/Device.tsx b/src/components/PageComponents/Config/Device.tsx index c0f3bfc4..ce2b5ce1 100644 --- a/src/components/PageComponents/Config/Device.tsx +++ b/src/components/PageComponents/Config/Device.tsx @@ -36,19 +36,6 @@ export const Device = (): JSX.Element => { formatEnumName: true, }, }, - { - type: "toggle", - name: "serialEnabled", - label: "Serial Output Enabled", - description: "Enable the device's serial console", - }, - { - type: "toggle", - name: "debugLogEnabled", - label: "Enabled Debug Log", - description: - "Output debugging information to the device's serial port (auto disables when serial client is connected)", - }, { type: "number", name: "buttonGpio", @@ -86,12 +73,6 @@ export const Device = (): JSX.Element => { label: "Double Tap as Button Press", description: "Treat double tap as button press", }, - { - type: "toggle", - name: "isManaged", - label: "Managed", - description: "Is this device managed by a mesh administator", - }, { type: "toggle", name: "disableTripleClick", diff --git a/src/components/PageComponents/Config/LoRa.tsx b/src/components/PageComponents/Config/LoRa.tsx index 8556f880..79e7b810 100644 --- a/src/components/PageComponents/Config/LoRa.tsx +++ b/src/components/PageComponents/Config/LoRa.tsx @@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => { label: "Ignore MQTT", 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", + }, ], }, { diff --git a/src/components/PageComponents/Config/Security.tsx b/src/components/PageComponents/Config/Security.tsx new file mode 100644 index 00000000..9848f491 --- /dev/null +++ b/src/components/PageComponents/Config/Security.tsx @@ -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( + fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), + ); + const [privateKeyVisible, setPrivateKeyVisible] = useState(false); + const [privateKeyBitCount, setPrivateKeyBitCount] = useState( + config.security?.privateKey.length ?? 32, + ); + const [privateKeyValidationText, setPrivateKeyValidationText] = + useState(); + const [publicKey, setPublicKey] = useState( + fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), + ); + const [adminKey, setAdminKey] = useState( + fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)), + ); + const [adminKeyValidationText, setAdminKeyValidationText] = + useState(); + const [dialogOpen, setDialogOpen] = useState(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, + ) => 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, + ) => { + const privateKeyB64String = e.target.value; + setPrivateKey(privateKeyB64String); + validateKey( + privateKeyB64String, + privateKeyBitCount, + setPrivateKeyValidationText, + ); + + const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String)); + setPublicKey(fromByteArray(publicKey)); + }; + + const adminKeyInputChangeEvent = (e: React.ChangeEvent) => { + 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 ( + <> + + 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", + }, + ], + }, + ]} + /> + setDialogOpen(false)} + onSubmit={() => pkiRegenerate()} + /> + + ); +}; diff --git a/src/components/PageLayout.tsx b/src/components/PageLayout.tsx index 1a2e4dd7..f6698b97 100644 --- a/src/components/PageLayout.tsx +++ b/src/components/PageLayout.tsx @@ -8,6 +8,7 @@ export interface PageLayoutProps { children: React.ReactNode; actions?: { icon: LucideIcon; + iconClasses?: string; onClick: () => void; }[]; } @@ -39,7 +40,7 @@ export const PageLayout = ({ className="transition-all hover:text-accent" onClick={action.onClick} > - + ))} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 96d820bb..116b6647 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -85,7 +85,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
- {myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts + {myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
diff --git a/src/components/UI/Generator.tsx b/src/components/UI/Generator.tsx index 344e89bb..7d589be4 100644 --- a/src/components/UI/Generator.tsx +++ b/src/components/UI/Generator.tsx @@ -9,63 +9,95 @@ import { SelectTrigger, SelectValue, } from "@components/UI/Select.js"; +import type { LucideIcon } from "lucide-react"; export interface GeneratorProps extends React.BaseHTMLAttributes { + 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) => void; buttonClick: React.MouseEventHandler; + action?: { + icon: LucideIcon; + onClick: () => void; + }; + disabled?: boolean; } const Generator = React.forwardRef( ( { + 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(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 ( <>