From 4c3a02ac53a3ece7d927a4846262c68f9b46473e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 20 May 2025 07:41:32 -0700 Subject: [PATCH] Show decrypt error in secure input --- src-web/components/core/Button.tsx | 8 ++- src-web/components/core/Input.tsx | 84 +++++++++++++++++++++++--- src-web/components/core/PlainInput.tsx | 1 + src-web/hooks/useFastMutation.ts | 5 +- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 35f08e33..e4382d9e 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -12,7 +12,7 @@ export type ButtonProps = Omit, 'color' | 'onC color?: Color | 'custom' | 'default'; variant?: 'border' | 'solid'; isLoading?: boolean; - size?: '2xs' | 'xs' | 'sm' | 'md'; + size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto'; justify?: 'start' | 'center'; type?: 'button' | 'submit'; forDropdown?: boolean; @@ -114,7 +114,7 @@ export const Button = forwardRef(function Button {...props} > {isLoading ? ( - + ) : leftSlot ? (
{leftSlot}
) : null} @@ -128,7 +128,9 @@ export const Button = forwardRef(function Button {children} {rightSlot &&
{rightSlot}
} - {forDropdown && } + {forDropdown && ( + + )} ); }); diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 5b806eee..a9cbaedf 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -11,6 +11,7 @@ import { useRef, useState, } from 'react'; +import { createFastMutation } from '../../hooks/useFastMutation'; import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled'; import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { copyToClipboard } from '../../lib/copy'; @@ -20,7 +21,10 @@ import { convertTemplateToSecure, } from '../../lib/encryption'; import { generateId } from '../../lib/generateId'; -import { withEncryptionEnabled } from '../../lib/setupOrConfigureEncryption'; +import { + setupOrConfigureEncryption, + withEncryptionEnabled, +} from '../../lib/setupOrConfigureEncryption'; import { Button } from './Button'; import type { DropdownItem } from './Dropdown'; import { Dropdown } from './Dropdown'; @@ -29,6 +33,7 @@ import { Editor } from './Editor/Editor'; import type { IconProps } from './Icon'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; +import { IconTooltip } from './IconTooltip'; import { Label } from './Label'; import { HStack } from './Stacks'; @@ -67,7 +72,7 @@ export type InputProps = Pick< placeholder?: string; required?: boolean; rightSlot?: ReactNode; - size?: 'xs' | 'sm' | 'md' | 'auto'; + size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto'; stateKey: EditorProps['stateKey']; tint?: Color; type?: 'text' | 'password'; @@ -169,12 +174,15 @@ const BaseInput = forwardRef(function InputBase( ); const isValid = useMemo(() => { + console.log('CHECKING VALIDITY', validate); if (required && !validateRequire(defaultValue ?? '')) return false; if (typeof validate === 'boolean') return validate; if (typeof validate === 'function' && !validate(defaultValue ?? '')) return false; return true; }, [required, defaultValue, validate]); + console.log('IS VALID', isValid, defaultValue); + const handleChange = useCallback( (value: string) => { onChange?.(value); @@ -231,6 +239,7 @@ const BaseInput = forwardRef(function InputBase( size === 'md' && 'min-h-md', size === 'sm' && 'min-h-sm', size === 'xs' && 'min-h-xs', + size === '2xs' && 'min-h-2xs', )} > {tint != null && ( @@ -332,7 +341,10 @@ function EncryptionInput({ value: string | null; security: ReturnType | null; obscured: boolean; - }>({ fieldType: 'encrypted', value: null, security: null, obscured: true }, [ogForceUpdateKey]); + error: string | null; + }>({ fieldType: 'encrypted', value: null, security: null, obscured: true, error: null }, [ + ogForceUpdateKey, + ]); const forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`; @@ -345,25 +357,48 @@ function EncryptionInput({ const security = analyzeTemplate(defaultValue ?? ''); if (analyzeTemplate(defaultValue ?? '') === 'global_secured') { // Lazily update value to decrypted representation - convertTemplateToInsecure(defaultValue ?? '').then((value) => { - setState({ fieldType: 'encrypted', security, value, obscured: true }); + templateToInsecure.mutate(defaultValue ?? '', { + onSuccess: (value) => { + setState({ fieldType: 'encrypted', security, value, obscured: true, error: null }); + }, + onError: (value) => { + setState({ + fieldType: 'encrypted', + security, + value: null, + error: String(value), + obscured: true, + }); + }, }); } else if (isEncryptionEnabled && !defaultValue) { // Default to encrypted field for new encrypted inputs - setState({ fieldType: 'encrypted', security, value: '', obscured: true }); + setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null }); } else if (isEncryptionEnabled) { // Don't obscure plain text when encryption is enabled - setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: false }); + setState({ + fieldType: 'text', + security, + value: defaultValue ?? '', + obscured: false, + error: null, + }); } else { // Don't obscure plain text when encryption is disabled - setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: true }); + setState({ + fieldType: 'text', + security, + value: defaultValue ?? '', + obscured: true, + error: null, + }); } }, [defaultValue, isEncryptionEnabled, setState, state.value]); const handleChange = useCallback( (value: string, fieldType: PasswordFieldType) => { if (fieldType === 'encrypted') { - convertTemplateToSecure(value).then((value) => onChange?.(value)); + templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) }); } else { onChange?.(value); } @@ -372,7 +407,7 @@ function EncryptionInput({ const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value); // Reset obscured value when the field type is being changed const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text'; - return { fieldType, value, security, obscured }; + return { fieldType, value, security, obscured, error: s.error }; }); }, [onChange, setState], @@ -477,6 +512,23 @@ function EncryptionInput({ const type = state.obscured ? 'password' : 'text'; + if (state.error) { + return ( + + ); + } + return ( ); } + +const templateToSecure = createFastMutation({ + mutationKey: ['template-to-secure'], + mutationFn: convertTemplateToSecure, +}); + +const templateToInsecure = createFastMutation({ + mutationKey: ['template-to-insecure'], + mutationFn: convertTemplateToInsecure, + disableToastError: true, +}); diff --git a/src-web/components/core/PlainInput.tsx b/src-web/components/core/PlainInput.tsx index 1d18a44e..c4ad205c 100644 --- a/src-web/components/core/PlainInput.tsx +++ b/src-web/components/core/PlainInput.tsx @@ -116,6 +116,7 @@ export function PlainInput({ size === 'md' && 'min-h-md', size === 'sm' && 'min-h-sm', size === 'xs' && 'min-h-xs', + size === '2xs' && 'min-h-2xs', )} > {tint != null && ( diff --git a/src-web/hooks/useFastMutation.ts b/src-web/hooks/useFastMutation.ts index 194c8eee..81b1c28e 100644 --- a/src-web/hooks/useFastMutation.ts +++ b/src-web/hooks/useFastMutation.ts @@ -30,6 +30,7 @@ export function createFastMutation