Browse Source

Merge pull request #266 from Hunter275/issue-261-password-generator

PSK Generator
pull/278/head
Hunter Thornsberry 2 years ago
committed by GitHub
parent
commit
2b34d78a86
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      package.json
  2. 17
      pnpm-lock.yaml
  3. 9
      src/components/Form/DynamicForm.tsx
  4. 15
      src/components/Form/DynamicFormField.tsx
  5. 40
      src/components/Form/FormPasswordGenerator.tsx
  6. 7
      src/components/Form/FormWrapper.tsx
  7. 57
      src/components/PageComponents/Channel.tsx
  8. 2
      src/components/UI/Button.tsx
  9. 79
      src/components/UI/Generator.tsx
  10. 25
      src/components/UI/Input.tsx

1
package.json

@ -45,6 +45,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"crypto-random-string": "^5.0.0",
"immer": "^10.1.1",
"lucide-react": "^0.363.0",
"mapbox-gl": "npm:empty-npm-package@^1.0.0",

17
pnpm-lock.yaml

@ -80,6 +80,9 @@ importers:
cmdk:
specifier: ^1.0.0
version: 1.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
crypto-random-string:
specifier: ^5.0.0
version: 5.0.0
immer:
specifier: ^10.1.1
version: 10.1.1
@ -1901,6 +1904,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
engines: {node: '>=14.16'}
[email protected]:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@ -2929,6 +2936,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==}
[email protected]:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
[email protected]:
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
engines: {node: '>=14.17'}
@ -5293,6 +5304,10 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
[email protected]:
dependencies:
type-fest: 2.19.0
[email protected]: {}
[email protected]: {}
@ -6365,6 +6380,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}

9
src/components/Form/DynamicForm.tsx

@ -26,6 +26,7 @@ export interface BaseFormBuilderProps<T> {
disabledBy?: DisabledBy<T>[];
label: string;
description?: string;
validationText?: string;
properties?: Record<string, unknown>;
}
@ -39,11 +40,12 @@ export interface DynamicFormProps<T extends FieldValues> {
onSubmit: SubmitHandler<T>;
submitType?: "onChange" | "onSubmit";
hasSubmitButton?: boolean;
// defaultValues?: DeepPartial<T>;
defaultValues?: DefaultValues<T>;
fieldGroups: {
label: string;
description: string;
valid?: boolean;
validationText?: string;
fields: FieldProps<T>[];
}[];
}
@ -98,6 +100,11 @@ export function DynamicForm<T extends FieldValues>({
key={field.label}
label={field.label}
description={field.description}
valid={
field.validationText === undefined ||
field.validationText === ""
}
validationText={field.validationText}
>
<DynamicFormField
field={field}

15
src/components/Form/DynamicFormField.tsx

@ -2,6 +2,10 @@ import {
GenericInput,
type InputFieldProps,
} from "@components/Form/FormInput.js";
import {
PasswordGenerator,
type PasswordGeneratorProps,
} from "@components/Form/FormPasswordGenerator.js";
import {
type SelectFieldProps,
SelectInput,
@ -15,7 +19,8 @@ import type { Control, FieldValues } from "react-hook-form";
export type FieldProps<T> =
| InputFieldProps<T>
| SelectFieldProps<T>
| ToggleFieldProps<T>;
| ToggleFieldProps<T>
| PasswordGeneratorProps<T>;
export interface DynamicFormFieldProps<T extends FieldValues> {
field: FieldProps<T>;
@ -44,6 +49,14 @@ export function DynamicFormField<T extends FieldValues>({
return (
<SelectInput field={field} control={control} disabled={disabled} />
);
case "passwordGenerator":
return (
<PasswordGenerator
field={field}
control={control}
disabled={disabled}
/>
);
case "multiSelect":
return <div>tmp</div>;
}

40
src/components/Form/FormPasswordGenerator.tsx

@ -0,0 +1,40 @@
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";
devicePSKBitCount: number;
inputChange: ChangeEventHandler;
selectChange: (event: string) => void;
buttonClick: MouseEventHandler;
}
export function PasswordGenerator<T extends FieldValues>({
control,
field,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, ...rest } }) => (
<Generator
devicePSKBitCount={field.devicePSKBitCount}
inputChange={field.inputChange}
selectChange={field.selectChange}
buttonClick={field.buttonClick}
value={value}
variant={field.validationText ? "invalid" : "default"}
buttonText="Generate"
{...field.properties}
{...rest}
/>
)}
/>
);
}

7
src/components/Form/FormWrapper.tsx

@ -5,12 +5,16 @@ export interface FieldWrapperProps {
description?: string;
disabled?: boolean;
children?: React.ReactNode;
valid?: boolean;
validationText?: string;
}
export const FieldWrapper = ({
label,
description,
children,
valid,
validationText,
}: FieldWrapperProps): JSX.Element => (
<div className="pt-6 sm:pt-5">
<div role="group" aria-labelledby="label-notifications">
@ -19,6 +23,9 @@ export const FieldWrapper = ({
<div className="sm:col-span-2">
<div className="max-w-lg">
<p className="text-sm text-gray-500">{description}</p>
<p hidden={valid ?? true} className="text-sm text-red-500">
{validationText}
</p>
<div className="mt-4 space-y-4">
<div className="flex items-center">{children}</div>
</div>

57
src/components/PageComponents/Channel.tsx

@ -4,6 +4,8 @@ import { useToast } from "@core/hooks/useToast.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { useState } from "react";
export interface SettingsPanelProps {
channel: Protobuf.Channel.Channel;
@ -13,12 +15,20 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
const { config, connection, addChannel } = useDevice();
const { toast } = useToast();
const [pass, setPass] = useState<string>(
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
);
const [bitCount, setBits] = useState<number>(
channel?.settings?.psk.length ?? 16,
);
const [validationText, setValidationText] = useState<string>();
const onSubmit = (data: ChannelValidation) => {
const channel = new Protobuf.Channel.Channel({
...data,
settings: {
...data.settings,
psk: toByteArray(data.settings.psk ?? ""),
psk: toByteArray(pass),
moduleSettings: {
positionPrecision: data.settings.positionEnabled
? data.settings.preciseLocation
@ -36,6 +46,38 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
});
};
const clickEvent = () => {
setPass(
btoa(
cryptoRandomString({
length: bitCount ?? 0,
type: "alphanumeric",
}),
),
);
setValidationText(undefined);
};
const validatePass = (input: string, count: number) => {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
} else {
setValidationText(undefined);
}
};
const inputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const psk = e.currentTarget?.value;
setPass(psk);
validatePass(psk, bitCount);
};
const selectChangeEvent = (e: string) => {
const count = Number.parseInt(e);
setBits(count);
validatePass(pass, count);
};
return (
<DynamicForm<ChannelValidation>
onSubmit={onSubmit}
@ -46,7 +88,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
...{
settings: {
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
psk: pass,
positionEnabled:
channel?.settings?.moduleSettings?.positionPrecision !==
undefined &&
@ -76,12 +118,17 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
},
},
{
type: "password",
type: "passwordGenerator",
name: "settings.psk",
label: "pre-Shared Key",
description: "16, or 32 bytes",
description: "256, 128, or 8 bit PSKs allowed",
validationText: validationText,
devicePSKBitCount: bitCount ?? 0,
inputChange: inputChangeEvent,
selectChange: selectChangeEvent,
buttonClick: clickEvent,
properties: {
// act
value: pass,
},
},
{

2
src/components/UI/Button.tsx

@ -12,6 +12,8 @@ const buttonVariants = cva(
"bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
destructive:
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
success:
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
outline:
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
subtle:

79
src/components/UI/Generator.tsx

@ -0,0 +1,79 @@
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";
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
devicePSKBitCount?: number;
value: string;
variant: "default" | "invalid";
buttonText?: string;
selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
}
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
(
{
devicePSKBitCount,
variant,
value,
buttonText,
selectChange,
inputChange,
buttonClick,
...props
},
ref,
) => {
return (
<>
<Input
type="text"
id="pskInput"
variant={variant}
value={value}
onChange={inputChange}
/>
<Select
value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem key="bit256" value="32">
256 bit
</SelectItem>
<SelectItem key="bit128" value="16">
128 bit
</SelectItem>
<SelectItem key="bit8" value="1">
8 bit
</SelectItem>
</SelectContent>
</Select>
<Button
type="button"
variant="success"
onClick={buttonClick}
{...props}
>
{buttonText}
</Button>
</>
);
},
);
Generator.displayName = "Button";
export { Generator };

25
src/components/UI/Input.tsx

@ -1,10 +1,27 @@
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { type VariantProps, cva } from "class-variance-authority";
import type { LucideIcon } from "lucide-react";
const inputVariants = cva(
"flex h-10 w-full rounded-md border bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
{
variants: {
variant: {
default: "border-slate-300 dark:border-slate-700",
invalid: "border-red-500 dark:border-red-500",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
prefix?: string;
suffix?: string;
action?: {
@ -14,7 +31,7 @@ export interface InputProps
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, prefix, suffix, action, ...props }, ref) => {
({ className, variant, prefix, suffix, action, ...props }, ref) => {
return (
<div className="relative w-full">
{prefix && (
@ -24,9 +41,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
)}
<input
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
action && "pr-8",
className,
inputVariants({ variant }),
)}
ref={ref}
{...props}
@ -51,4 +68,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
);
Input.displayName = "Input";
export { Input };
export { Input, inputVariants };

Loading…
Cancel
Save