Show decrypt error in secure input

This commit is contained in:
Gregory Schier
2025-05-20 07:41:32 -07:00
parent 1974d61aa4
commit 4c3a02ac53
4 changed files with 82 additions and 16 deletions

View File

@@ -12,7 +12,7 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onC
color?: Color | 'custom' | 'default'; color?: Color | 'custom' | 'default';
variant?: 'border' | 'solid'; variant?: 'border' | 'solid';
isLoading?: boolean; isLoading?: boolean;
size?: '2xs' | 'xs' | 'sm' | 'md'; size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
justify?: 'start' | 'center'; justify?: 'start' | 'center';
type?: 'button' | 'submit'; type?: 'button' | 'submit';
forDropdown?: boolean; forDropdown?: boolean;
@@ -114,7 +114,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{...props} {...props}
> >
{isLoading ? ( {isLoading ? (
<LoadingIcon size={size} className="mr-1" /> <LoadingIcon size={size === 'auto' ? 'md' : size} className="mr-1" />
) : leftSlot ? ( ) : leftSlot ? (
<div className="mr-2">{leftSlot}</div> <div className="mr-2">{leftSlot}</div>
) : null} ) : null}
@@ -128,7 +128,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{children} {children}
</div> </div>
{rightSlot && <div className="ml-1">{rightSlot}</div>} {rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevron_down" size={size} className="ml-1 -mr-1" />} {forDropdown && (
<Icon icon="chevron_down" size={size === 'auto' ? 'md' : size} className="ml-1 -mr-1" />
)}
</button> </button>
); );
}); });

View File

@@ -11,6 +11,7 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { createFastMutation } from '../../hooks/useFastMutation';
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled'; import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { copyToClipboard } from '../../lib/copy'; import { copyToClipboard } from '../../lib/copy';
@@ -20,7 +21,10 @@ import {
convertTemplateToSecure, convertTemplateToSecure,
} from '../../lib/encryption'; } from '../../lib/encryption';
import { generateId } from '../../lib/generateId'; import { generateId } from '../../lib/generateId';
import { withEncryptionEnabled } from '../../lib/setupOrConfigureEncryption'; import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../../lib/setupOrConfigureEncryption';
import { Button } from './Button'; import { Button } from './Button';
import type { DropdownItem } from './Dropdown'; import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
@@ -29,6 +33,7 @@ import { Editor } from './Editor/Editor';
import type { IconProps } from './Icon'; import type { IconProps } from './Icon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { IconTooltip } from './IconTooltip';
import { Label } from './Label'; import { Label } from './Label';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
@@ -67,7 +72,7 @@ export type InputProps = Pick<
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
rightSlot?: ReactNode; rightSlot?: ReactNode;
size?: 'xs' | 'sm' | 'md' | 'auto'; size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
stateKey: EditorProps['stateKey']; stateKey: EditorProps['stateKey'];
tint?: Color; tint?: Color;
type?: 'text' | 'password'; type?: 'text' | 'password';
@@ -169,12 +174,15 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
); );
const isValid = useMemo(() => { const isValid = useMemo(() => {
console.log('CHECKING VALIDITY', validate);
if (required && !validateRequire(defaultValue ?? '')) return false; if (required && !validateRequire(defaultValue ?? '')) return false;
if (typeof validate === 'boolean') return validate; if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(defaultValue ?? '')) return false; if (typeof validate === 'function' && !validate(defaultValue ?? '')) return false;
return true; return true;
}, [required, defaultValue, validate]); }, [required, defaultValue, validate]);
console.log('IS VALID', isValid, defaultValue);
const handleChange = useCallback( const handleChange = useCallback(
(value: string) => { (value: string) => {
onChange?.(value); onChange?.(value);
@@ -231,6 +239,7 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
size === 'md' && 'min-h-md', size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm', size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs', size === 'xs' && 'min-h-xs',
size === '2xs' && 'min-h-2xs',
)} )}
> >
{tint != null && ( {tint != null && (
@@ -332,7 +341,10 @@ function EncryptionInput({
value: string | null; value: string | null;
security: ReturnType<typeof analyzeTemplate> | null; security: ReturnType<typeof analyzeTemplate> | null;
obscured: boolean; 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}`; const forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`;
@@ -345,25 +357,48 @@ function EncryptionInput({
const security = analyzeTemplate(defaultValue ?? ''); const security = analyzeTemplate(defaultValue ?? '');
if (analyzeTemplate(defaultValue ?? '') === 'global_secured') { if (analyzeTemplate(defaultValue ?? '') === 'global_secured') {
// Lazily update value to decrypted representation // Lazily update value to decrypted representation
convertTemplateToInsecure(defaultValue ?? '').then((value) => { templateToInsecure.mutate(defaultValue ?? '', {
setState({ fieldType: 'encrypted', security, value, obscured: true }); 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) { } else if (isEncryptionEnabled && !defaultValue) {
// Default to encrypted field for new encrypted inputs // 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) { } else if (isEncryptionEnabled) {
// Don't obscure plain text when encryption is enabled // 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 { } else {
// Don't obscure plain text when encryption is disabled // 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]); }, [defaultValue, isEncryptionEnabled, setState, state.value]);
const handleChange = useCallback( const handleChange = useCallback(
(value: string, fieldType: PasswordFieldType) => { (value: string, fieldType: PasswordFieldType) => {
if (fieldType === 'encrypted') { if (fieldType === 'encrypted') {
convertTemplateToSecure(value).then((value) => onChange?.(value)); templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) });
} else { } else {
onChange?.(value); onChange?.(value);
} }
@@ -372,7 +407,7 @@ function EncryptionInput({
const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value); const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value);
// Reset obscured value when the field type is being changed // Reset obscured value when the field type is being changed
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text'; const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text';
return { fieldType, value, security, obscured }; return { fieldType, value, security, obscured, error: s.error };
}); });
}, },
[onChange, setState], [onChange, setState],
@@ -477,6 +512,23 @@ function EncryptionInput({
const type = state.obscured ? 'password' : 'text'; const type = state.obscured ? 'password' : 'text';
if (state.error) {
return (
<Button
variant="border"
color="danger"
size={props.size}
className="text-sm"
rightSlot={<IconTooltip content={state.error} icon="alert_triangle" />}
onClick={() => {
setupOrConfigureEncryption();
}}
>
{state.error.replace(/^Render Error: /i, '')}
</Button>
);
}
return ( return (
<BaseInput <BaseInput
disableObscureToggle disableObscureToggle
@@ -488,8 +540,20 @@ function EncryptionInput({
tint={tint} tint={tint}
type={type} type={type}
rightSlot={rightSlot} rightSlot={rightSlot}
disabled={state.error != null}
className="pr-1.5" // To account for encryption dropdown className="pr-1.5" // To account for encryption dropdown
{...props} {...props}
/> />
); );
} }
const templateToSecure = createFastMutation({
mutationKey: ['template-to-secure'],
mutationFn: convertTemplateToSecure,
});
const templateToInsecure = createFastMutation({
mutationKey: ['template-to-insecure'],
mutationFn: convertTemplateToInsecure,
disableToastError: true,
});

View File

@@ -116,6 +116,7 @@ export function PlainInput({
size === 'md' && 'min-h-md', size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm', size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs', size === 'xs' && 'min-h-xs',
size === '2xs' && 'min-h-2xs',
)} )}
> >
{tint != null && ( {tint != null && (

View File

@@ -30,6 +30,7 @@ export function createFastMutation<TData = unknown, TError = unknown, TVariables
try { try {
const data = await mutationFn(variables); const data = await mutationFn(variables);
onSuccess?.(data); onSuccess?.(data);
onSettled?.();
return data; return data;
} catch (err: unknown) { } catch (err: unknown) {
const stringKey = mutationKey.join('.'); const stringKey = mutationKey.join('.');
@@ -44,11 +45,9 @@ export function createFastMutation<TData = unknown, TError = unknown, TVariables
}); });
} }
onError?.(e); onError?.(e);
} finally {
onSettled?.(); onSettled?.();
throw e;
} }
return null;
}; };
const mutate = ( const mutate = (